summaryrefslogtreecommitdiffstats
path: root/src/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/com')
-rw-r--r--src/com/android/camera/AndroidCameraManagerImpl.java779
-rw-r--r--src/com/android/camera/CameraActivity.java571
-rw-r--r--src/com/android/camera/CameraBackupAgent.java32
-rw-r--r--src/com/android/camera/CameraButtonIntentReceiver.java53
-rw-r--r--src/com/android/camera/CameraDisabledException.java24
-rw-r--r--src/com/android/camera/CameraErrorCallback.java35
-rw-r--r--src/com/android/camera/CameraHardwareException.java28
-rw-r--r--src/com/android/camera/CameraHolder.java299
-rw-r--r--src/com/android/camera/CameraManager.java317
-rw-r--r--src/com/android/camera/CameraManagerFactory.java37
-rw-r--r--src/com/android/camera/CameraModule.java70
-rw-r--r--src/com/android/camera/CameraPreference.java63
-rw-r--r--src/com/android/camera/CameraScreenNail.java524
-rw-r--r--src/com/android/camera/CameraSettings.java570
-rw-r--r--src/com/android/camera/CaptureAnimManager.java228
-rw-r--r--src/com/android/camera/ComboPreferences.java335
-rw-r--r--src/com/android/camera/CountDownTimerPreference.java48
-rw-r--r--src/com/android/camera/DisableCameraReceiver.java85
-rw-r--r--src/com/android/camera/EffectsRecorder.java1239
-rw-r--r--src/com/android/camera/Exif.java54
-rw-r--r--src/com/android/camera/FocusOverlayManager.java558
-rw-r--r--src/com/android/camera/IconListPreference.java117
-rw-r--r--src/com/android/camera/ImageTaskManager.java48
-rw-r--r--src/com/android/camera/IntArray.java45
-rw-r--r--src/com/android/camera/ListPreference.java202
-rw-r--r--src/com/android/camera/LocationManager.java181
-rw-r--r--src/com/android/camera/MediaSaveService.java233
-rw-r--r--src/com/android/camera/OnClickAttr.java31
-rw-r--r--src/com/android/camera/OnScreenHint.java190
-rw-r--r--src/com/android/camera/OnScreenIndicators.java190
-rw-r--r--src/com/android/camera/PhotoController.java65
-rw-r--r--src/com/android/camera/PhotoMenu.java200
-rw-r--r--src/com/android/camera/PhotoModule.java2006
-rw-r--r--src/com/android/camera/PhotoUI.java864
-rw-r--r--src/com/android/camera/PieController.java259
-rw-r--r--src/com/android/camera/PreferenceGroup.java79
-rw-r--r--src/com/android/camera/PreferenceInflater.java108
-rw-r--r--src/com/android/camera/PreviewFrameLayout.java136
-rw-r--r--src/com/android/camera/PreviewGestures.java199
-rw-r--r--src/com/android/camera/ProxyLauncher.java46
-rw-r--r--src/com/android/camera/RecordLocationPreference.java58
-rw-r--r--src/com/android/camera/RotateDialogController.java169
-rw-r--r--src/com/android/camera/SecureCameraActivity.java23
-rwxr-xr-xsrc/com/android/camera/ShutterButton.java130
-rw-r--r--src/com/android/camera/SoundClips.java197
-rw-r--r--src/com/android/camera/StaticBitmapScreenNail.java32
-rw-r--r--src/com/android/camera/Storage.java181
-rw-r--r--src/com/android/camera/SurfaceTextureRenderer.java224
-rw-r--r--src/com/android/camera/SwitchAnimManager.java146
-rw-r--r--src/com/android/camera/Thumbnail.java68
-rw-r--r--src/com/android/camera/Util.java804
-rw-r--r--src/com/android/camera/VideoController.java42
-rw-r--r--src/com/android/camera/VideoMenu.java205
-rw-r--r--src/com/android/camera/VideoModule.java2233
-rw-r--r--src/com/android/camera/VideoUI.java698
-rw-r--r--src/com/android/camera/data/AbstractLocalDataAdapterWrapper.java90
-rw-r--r--src/com/android/camera/data/CameraDataAdapter.java348
-rw-r--r--src/com/android/camera/data/CameraPreviewData.java63
-rw-r--r--src/com/android/camera/data/FixedFirstDataAdapter.java154
-rw-r--r--src/com/android/camera/data/FixedLastDataAdapter.java127
-rw-r--r--src/com/android/camera/data/LocalData.java726
-rw-r--r--src/com/android/camera/data/LocalDataAdapter.java91
-rw-r--r--src/com/android/camera/data/PanoramaMetadataLoader.java106
-rw-r--r--src/com/android/camera/drawable/TextDrawable.java121
-rw-r--r--src/com/android/camera/ui/AbstractSettingPopup.java44
-rw-r--r--src/com/android/camera/ui/CameraControls.java262
-rw-r--r--src/com/android/camera/ui/CameraRootView.java181
-rw-r--r--src/com/android/camera/ui/CameraSwitcher.java378
-rw-r--r--src/com/android/camera/ui/CheckedLinearLayout.java60
-rw-r--r--src/com/android/camera/ui/CountDownView.java131
-rw-r--r--src/com/android/camera/ui/CountdownTimerPopup.java145
-rw-r--r--src/com/android/camera/ui/EffectSettingPopup.java214
-rw-r--r--src/com/android/camera/ui/ExpandedGridView.java36
-rw-r--r--src/com/android/camera/ui/FaceView.java226
-rw-r--r--src/com/android/camera/ui/FilmStripGestureRecognizer.java112
-rw-r--r--src/com/android/camera/ui/FilmStripView.java1720
-rw-r--r--src/com/android/camera/ui/FocusIndicator.java24
-rw-r--r--src/com/android/camera/ui/InLineSettingCheckBox.java83
-rw-r--r--src/com/android/camera/ui/InLineSettingItem.java94
-rw-r--r--src/com/android/camera/ui/InLineSettingMenu.java78
-rw-r--r--src/com/android/camera/ui/LayoutChangeHelper.java43
-rw-r--r--src/com/android/camera/ui/LayoutChangeNotifier.java28
-rw-r--r--src/com/android/camera/ui/LayoutNotifyView.java48
-rw-r--r--src/com/android/camera/ui/ListPrefSettingPopup.java127
-rw-r--r--src/com/android/camera/ui/MoreSettingPopup.java203
-rw-r--r--src/com/android/camera/ui/OnIndicatorEventListener.java25
-rw-r--r--src/com/android/camera/ui/OverlayRenderer.java95
-rw-r--r--src/com/android/camera/ui/PieItem.java170
-rw-r--r--src/com/android/camera/ui/PieMenuButton.java62
-rw-r--r--src/com/android/camera/ui/PieRenderer.java1091
-rw-r--r--src/com/android/camera/ui/PopupManager.java66
-rw-r--r--src/com/android/camera/ui/PreviewSurfaceView.java50
-rw-r--r--src/com/android/camera/ui/RenderOverlay.java176
-rw-r--r--src/com/android/camera/ui/Rotatable.java22
-rw-r--r--src/com/android/camera/ui/RotatableLayout.java283
-rw-r--r--src/com/android/camera/ui/RotateImageView.java176
-rw-r--r--src/com/android/camera/ui/RotateLayout.java203
-rw-r--r--src/com/android/camera/ui/RotateTextToast.java59
-rw-r--r--src/com/android/camera/ui/Switch.java505
-rw-r--r--src/com/android/camera/ui/TimeIntervalPopup.java164
-rw-r--r--src/com/android/camera/ui/TwoStateImageView.java55
-rw-r--r--src/com/android/camera/ui/ZoomRenderer.java158
-rw-r--r--src/com/android/gallery3d/anim/AlphaAnimation.java48
-rw-r--r--src/com/android/gallery3d/anim/Animation.java92
-rw-r--r--src/com/android/gallery3d/anim/CanvasAnimation.java25
-rw-r--r--src/com/android/gallery3d/anim/FloatAnimation.java40
-rw-r--r--src/com/android/gallery3d/anim/StateTransitionAnimation.java180
-rw-r--r--src/com/android/gallery3d/app/AbstractGalleryActivity.java343
-rw-r--r--src/com/android/gallery3d/app/ActivityState.java276
-rw-r--r--src/com/android/gallery3d/app/AlbumDataLoader.java397
-rw-r--r--src/com/android/gallery3d/app/AlbumPage.java786
-rw-r--r--src/com/android/gallery3d/app/AlbumPicker.java40
-rw-r--r--src/com/android/gallery3d/app/AlbumSetDataLoader.java393
-rw-r--r--src/com/android/gallery3d/app/AlbumSetPage.java764
-rw-r--r--src/com/android/gallery3d/app/AppBridge.java72
-rw-r--r--src/com/android/gallery3d/app/BatchService.java48
-rw-r--r--src/com/android/gallery3d/app/CommonControllerOverlay.java346
-rw-r--r--src/com/android/gallery3d/app/Config.java127
-rw-r--r--src/com/android/gallery3d/app/ControllerOverlay.java56
-rw-r--r--src/com/android/gallery3d/app/DialogPicker.java41
-rw-r--r--src/com/android/gallery3d/app/EyePosition.java226
-rw-r--r--src/com/android/gallery3d/app/FilmstripPage.java21
-rw-r--r--src/com/android/gallery3d/app/FilterUtils.java257
-rw-r--r--src/com/android/gallery3d/app/Gallery.java274
-rw-r--r--src/com/android/gallery3d/app/GalleryActionBar.java438
-rw-r--r--src/com/android/gallery3d/app/GalleryApp.java41
-rw-r--r--src/com/android/gallery3d/app/GalleryAppImpl.java127
-rw-r--r--src/com/android/gallery3d/app/GalleryContext.java34
-rw-r--r--src/com/android/gallery3d/app/LoadingListener.java27
-rw-r--r--src/com/android/gallery3d/app/Log.java53
-rw-r--r--src/com/android/gallery3d/app/ManageCachePage.java419
-rw-r--r--src/com/android/gallery3d/app/MovieActivity.java263
-rw-r--r--src/com/android/gallery3d/app/MovieControllerOverlay.java185
-rw-r--r--src/com/android/gallery3d/app/MoviePlayer.java525
-rw-r--r--src/com/android/gallery3d/app/MuteVideo.java104
-rw-r--r--src/com/android/gallery3d/app/NotificationIds.java22
-rw-r--r--src/com/android/gallery3d/app/OrientationManager.java166
-rw-r--r--src/com/android/gallery3d/app/PackagesMonitor.java71
-rw-r--r--src/com/android/gallery3d/app/PanoramaMetadataSupport.java93
-rw-r--r--src/com/android/gallery3d/app/PhotoDataAdapter.java1133
-rw-r--r--src/com/android/gallery3d/app/PhotoPage.java1571
-rw-r--r--src/com/android/gallery3d/app/PhotoPageBottomControls.java137
-rw-r--r--src/com/android/gallery3d/app/PhotoPageProgressBar.java50
-rw-r--r--src/com/android/gallery3d/app/PickerActivity.java83
-rw-r--r--src/com/android/gallery3d/app/SinglePhotoDataAdapter.java263
-rw-r--r--src/com/android/gallery3d/app/SinglePhotoPage.java21
-rw-r--r--src/com/android/gallery3d/app/SlideshowDataAdapter.java204
-rw-r--r--src/com/android/gallery3d/app/SlideshowPage.java366
-rw-r--r--src/com/android/gallery3d/app/StateManager.java339
-rw-r--r--src/com/android/gallery3d/app/StitchingChangeListener.java27
-rw-r--r--src/com/android/gallery3d/app/TimeBar.java266
-rw-r--r--src/com/android/gallery3d/app/TransitionStore.java46
-rw-r--r--src/com/android/gallery3d/app/TrimControllerOverlay.java111
-rw-r--r--src/com/android/gallery3d/app/TrimTimeBar.java339
-rw-r--r--src/com/android/gallery3d/app/TrimVideo.java337
-rw-r--r--src/com/android/gallery3d/app/VideoUtils.java328
-rw-r--r--src/com/android/gallery3d/app/Wallpaper.java135
-rw-r--r--src/com/android/gallery3d/data/ActionImage.java103
-rw-r--r--src/com/android/gallery3d/data/BucketHelper.java241
-rw-r--r--src/com/android/gallery3d/data/BytesBufferPool.java91
-rw-r--r--src/com/android/gallery3d/data/CameraShortcutImage.java34
-rw-r--r--src/com/android/gallery3d/data/ChangeNotifier.java57
-rw-r--r--src/com/android/gallery3d/data/ClusterAlbum.java143
-rw-r--r--src/com/android/gallery3d/data/ClusterAlbumSet.java159
-rw-r--r--src/com/android/gallery3d/data/ClusterSource.java86
-rw-r--r--src/com/android/gallery3d/data/Clustering.java29
-rw-r--r--src/com/android/gallery3d/data/ComboAlbum.java103
-rw-r--r--src/com/android/gallery3d/data/ComboAlbumSet.java96
-rw-r--r--src/com/android/gallery3d/data/ComboSource.java55
-rw-r--r--src/com/android/gallery3d/data/ContentListener.java21
-rw-r--r--src/com/android/gallery3d/data/DataManager.java371
-rw-r--r--src/com/android/gallery3d/data/DataSourceType.java45
-rw-r--r--src/com/android/gallery3d/data/DecodeUtils.java312
-rw-r--r--src/com/android/gallery3d/data/DownloadCache.java370
-rw-r--r--src/com/android/gallery3d/data/DownloadEntry.java72
-rw-r--r--src/com/android/gallery3d/data/DownloadUtils.java79
-rw-r--r--src/com/android/gallery3d/data/EmptyAlbumImage.java34
-rw-r--r--src/com/android/gallery3d/data/Exif.java48
-rw-r--r--src/com/android/gallery3d/data/Face.java65
-rw-r--r--src/com/android/gallery3d/data/FaceClustering.java142
-rw-r--r--src/com/android/gallery3d/data/FilterDeleteSet.java256
-rw-r--r--src/com/android/gallery3d/data/FilterEmptyPromptSet.java82
-rw-r--r--src/com/android/gallery3d/data/FilterSource.java94
-rw-r--r--src/com/android/gallery3d/data/FilterTypeSet.java137
-rw-r--r--src/com/android/gallery3d/data/ImageCacheRequest.java102
-rw-r--r--src/com/android/gallery3d/data/ImageCacheService.java123
-rw-r--r--src/com/android/gallery3d/data/LocalAlbum.java325
-rw-r--r--src/com/android/gallery3d/data/LocalAlbumSet.java211
-rw-r--r--src/com/android/gallery3d/data/LocalImage.java355
-rw-r--r--src/com/android/gallery3d/data/LocalMediaItem.java109
-rw-r--r--src/com/android/gallery3d/data/LocalMergeAlbum.java257
-rw-r--r--src/com/android/gallery3d/data/LocalSource.java275
-rw-r--r--src/com/android/gallery3d/data/LocalVideo.java242
-rw-r--r--src/com/android/gallery3d/data/LocationClustering.java316
-rw-r--r--src/com/android/gallery3d/data/Log.java53
-rw-r--r--src/com/android/gallery3d/data/MediaDetails.java170
-rw-r--r--src/com/android/gallery3d/data/MediaItem.java134
-rw-r--r--src/com/android/gallery3d/data/MediaObject.java166
-rw-r--r--src/com/android/gallery3d/data/MediaSet.java348
-rw-r--r--src/com/android/gallery3d/data/MediaSource.java96
-rw-r--r--src/com/android/gallery3d/data/MtpClient.java443
-rw-r--r--src/com/android/gallery3d/data/PanoramaMetadataJob.java40
-rw-r--r--src/com/android/gallery3d/data/Path.java241
-rw-r--r--src/com/android/gallery3d/data/PathMatcher.java102
-rw-r--r--src/com/android/gallery3d/data/SecureAlbum.java206
-rw-r--r--src/com/android/gallery3d/data/SecureSource.java56
-rw-r--r--src/com/android/gallery3d/data/SingleItemAlbum.java68
-rw-r--r--src/com/android/gallery3d/data/SizeClustering.java141
-rw-r--r--src/com/android/gallery3d/data/SnailAlbum.java44
-rw-r--r--src/com/android/gallery3d/data/SnailItem.java95
-rw-r--r--src/com/android/gallery3d/data/SnailSource.java70
-rw-r--r--src/com/android/gallery3d/data/TagClustering.java95
-rw-r--r--src/com/android/gallery3d/data/TimeClustering.java439
-rw-r--r--src/com/android/gallery3d/data/UnlockImage.java34
-rw-r--r--src/com/android/gallery3d/data/UriImage.java298
-rw-r--r--src/com/android/gallery3d/data/UriSource.java95
-rw-r--r--src/com/android/gallery3d/filtershow/CenteredLinearLayout.java51
-rw-r--r--src/com/android/gallery3d/filtershow/EditorPlaceHolder.java82
-rw-r--r--src/com/android/gallery3d/filtershow/FilterShowActivity.java1121
-rw-r--r--src/com/android/gallery3d/filtershow/cache/ImageLoader.java502
-rw-r--r--src/com/android/gallery3d/filtershow/category/Action.java186
-rw-r--r--src/com/android/gallery3d/filtershow/category/CategoryAdapter.java182
-rw-r--r--src/com/android/gallery3d/filtershow/category/CategoryPanel.java108
-rw-r--r--src/com/android/gallery3d/filtershow/category/CategoryTrack.java77
-rw-r--r--src/com/android/gallery3d/filtershow/category/CategoryView.java176
-rw-r--r--src/com/android/gallery3d/filtershow/category/MainPanel.java239
-rw-r--r--src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java100
-rw-r--r--src/com/android/gallery3d/filtershow/colorpicker/ColorListener.java21
-rw-r--r--src/com/android/gallery3d/filtershow/colorpicker/ColorOpacityView.java197
-rw-r--r--src/com/android/gallery3d/filtershow/colorpicker/ColorPickerDialog.java123
-rw-r--r--src/com/android/gallery3d/filtershow/colorpicker/ColorRectView.java225
-rw-r--r--src/com/android/gallery3d/filtershow/colorpicker/ColorValueView.java180
-rw-r--r--src/com/android/gallery3d/filtershow/colorpicker/RGBListener.java21
-rw-r--r--src/com/android/gallery3d/filtershow/controller/ActionSlider.java71
-rw-r--r--src/com/android/gallery3d/filtershow/controller/BasicParameterInt.java113
-rw-r--r--src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java111
-rw-r--r--src/com/android/gallery3d/filtershow/controller/BasicSlider.java87
-rw-r--r--src/com/android/gallery3d/filtershow/controller/Control.java32
-rw-r--r--src/com/android/gallery3d/filtershow/controller/FilterView.java25
-rw-r--r--src/com/android/gallery3d/filtershow/controller/Parameter.java33
-rw-r--r--src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java29
-rw-r--r--src/com/android/gallery3d/filtershow/controller/ParameterInteger.java31
-rw-r--r--src/com/android/gallery3d/filtershow/controller/ParameterSet.java23
-rw-r--r--src/com/android/gallery3d/filtershow/controller/ParameterStyles.java37
-rw-r--r--src/com/android/gallery3d/filtershow/controller/StyleChooser.java88
-rw-r--r--src/com/android/gallery3d/filtershow/controller/TitledSlider.java106
-rw-r--r--src/com/android/gallery3d/filtershow/crop/BoundedRect.java368
-rw-r--r--src/com/android/gallery3d/filtershow/crop/CropActivity.java697
-rw-r--r--src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java168
-rw-r--r--src/com/android/gallery3d/filtershow/crop/CropExtras.java121
-rw-r--r--src/com/android/gallery3d/filtershow/crop/CropMath.java260
-rw-r--r--src/com/android/gallery3d/filtershow/crop/CropObject.java330
-rw-r--r--src/com/android/gallery3d/filtershow/crop/CropView.java378
-rw-r--r--src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java101
-rw-r--r--src/com/android/gallery3d/filtershow/data/FilterStackSource.java197
-rw-r--r--src/com/android/gallery3d/filtershow/data/UserPresetsManager.java149
-rw-r--r--src/com/android/gallery3d/filtershow/editors/BasicEditor.java138
-rw-r--r--src/com/android/gallery3d/filtershow/editors/Editor.java330
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorChanSat.java227
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorCrop.java168
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorCurves.java56
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorDraw.java158
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorGrad.java315
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorInfo.java23
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorMirror.java109
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorPanel.java143
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorRedEye.java65
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorRotate.java112
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorStraighten.java103
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java58
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorVignette.java53
-rw-r--r--src/com/android/gallery3d/filtershow/editors/EditorZoom.java27
-rw-r--r--src/com/android/gallery3d/filtershow/editors/ImageOnlyEditor.java50
-rw-r--r--src/com/android/gallery3d/filtershow/editors/ParametricEditor.java206
-rw-r--r--src/com/android/gallery3d/filtershow/editors/SwapButton.java111
-rw-r--r--src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java296
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ColorSpaceMatrix.java225
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java196
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterChanSatRepresentation.java211
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterColorBorderRepresentation.java113
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterCropRepresentation.java179
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java170
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterDirectRepresentation.java38
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java171
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java112
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterGradRepresentation.java497
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterImageBorderRepresentation.java91
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterMirrorRepresentation.java190
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterPoint.java21
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterPointRepresentation.java88
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java67
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java262
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterRotateRepresentation.java190
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterStraightenRepresentation.java154
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java101
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterUserPresetRepresentation.java53
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java173
-rw-r--r--src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java21
-rw-r--r--src/com/android/gallery3d/filtershow/filters/IconUtilities.java75
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilter.java109
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java92
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java64
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterChanSat.java161
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java58
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java112
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java83
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java278
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java53
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java56
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java111
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterGrad.java190
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java74
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java64
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java95
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java39
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java69
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java260
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterRedEye.java79
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java58
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java58
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java107
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java158
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java57
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java98
-rw-r--r--src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java59
-rw-r--r--src/com/android/gallery3d/filtershow/filters/RedEyeCandidate.java50
-rw-r--r--src/com/android/gallery3d/filtershow/filters/SimpleImageFilter.java37
-rw-r--r--src/com/android/gallery3d/filtershow/filters/SplineMath.java166
-rw-r--r--src/com/android/gallery3d/filtershow/filters/convolve3x3.rs67
-rw-r--r--src/com/android/gallery3d/filtershow/filters/grad.rs121
-rw-r--r--src/com/android/gallery3d/filtershow/filters/grey.rs22
-rw-r--r--src/com/android/gallery3d/filtershow/filters/saturation.rs161
-rw-r--r--src/com/android/gallery3d/filtershow/history/HistoryItem.java53
-rw-r--r--src/com/android/gallery3d/filtershow/history/HistoryManager.java172
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java64
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java302
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java416
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/GradControl.java274
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java307
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java445
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java139
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java215
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java78
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java89
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java137
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java81
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ImageShow.java578
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java260
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java174
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java165
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/Line.java26
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/MasterImage.java581
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/Oval.java29
-rw-r--r--src/com/android/gallery3d/filtershow/imageshow/Spline.java450
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/Buffer.java74
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/CacheProcessing.java193
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java469
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/FilterEnvironment.java178
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/HighresRenderingRequestTask.java90
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java694
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java125
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/PipelineInterface.java31
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java283
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java88
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java97
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/RenderingRequest.java174
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/RenderingRequestCaller.java21
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/RenderingRequestTask.java81
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/SharedBuffer.java77
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/SharedPreset.java42
-rw-r--r--src/com/android/gallery3d/filtershow/pipeline/UpdatePreviewTask.java79
-rw-r--r--src/com/android/gallery3d/filtershow/presets/PresetManagementDialog.java69
-rw-r--r--src/com/android/gallery3d/filtershow/presets/UserPresetsAdapter.java171
-rw-r--r--src/com/android/gallery3d/filtershow/provider/SharedImageProvider.java137
-rw-r--r--src/com/android/gallery3d/filtershow/state/DragListener.java110
-rw-r--r--src/com/android/gallery3d/filtershow/state/PanelTrack.java37
-rw-r--r--src/com/android/gallery3d/filtershow/state/State.java78
-rw-r--r--src/com/android/gallery3d/filtershow/state/StateAdapter.java115
-rw-r--r--src/com/android/gallery3d/filtershow/state/StatePanel.java44
-rw-r--r--src/com/android/gallery3d/filtershow/state/StatePanelTrack.java351
-rw-r--r--src/com/android/gallery3d/filtershow/state/StateView.java291
-rw-r--r--src/com/android/gallery3d/filtershow/tools/IconFactory.java108
-rw-r--r--src/com/android/gallery3d/filtershow/tools/MatrixFit.java200
-rw-r--r--src/com/android/gallery3d/filtershow/tools/SaveImage.java632
-rw-r--r--src/com/android/gallery3d/filtershow/tools/XmpPresets.java133
-rw-r--r--src/com/android/gallery3d/filtershow/ui/ExportDialog.java90
-rw-r--r--src/com/android/gallery3d/filtershow/ui/FramedTextButton.java137
-rw-r--r--src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java48
-rw-r--r--src/com/android/gallery3d/gadget/LocalPhotoSource.java205
-rw-r--r--src/com/android/gallery3d/gadget/MediaSetSource.java233
-rw-r--r--src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java139
-rw-r--r--src/com/android/gallery3d/gadget/WidgetClickHandler.java77
-rw-r--r--src/com/android/gallery3d/gadget/WidgetConfigure.java209
-rw-r--r--src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java309
-rw-r--r--src/com/android/gallery3d/gadget/WidgetService.java143
-rw-r--r--src/com/android/gallery3d/gadget/WidgetSource.java31
-rw-r--r--src/com/android/gallery3d/gadget/WidgetTypeChooser.java59
-rw-r--r--src/com/android/gallery3d/gadget/WidgetUtils.java80
-rw-r--r--src/com/android/gallery3d/glrenderer/BasicTexture.java212
-rw-r--r--src/com/android/gallery3d/glrenderer/BitmapTexture.java54
-rw-r--r--src/com/android/gallery3d/glrenderer/CanvasTexture.java52
-rw-r--r--src/com/android/gallery3d/glrenderer/ColorTexture.java63
-rw-r--r--src/com/android/gallery3d/glrenderer/ExtTexture.java60
-rw-r--r--src/com/android/gallery3d/glrenderer/FadeInTexture.java43
-rw-r--r--src/com/android/gallery3d/glrenderer/FadeOutTexture.java42
-rw-r--r--src/com/android/gallery3d/glrenderer/FadeTexture.java81
-rw-r--r--src/com/android/gallery3d/glrenderer/GLCanvas.java217
-rw-r--r--src/com/android/gallery3d/glrenderer/GLES11Canvas.java997
-rw-r--r--src/com/android/gallery3d/glrenderer/GLES11IdImpl.java68
-rw-r--r--src/com/android/gallery3d/glrenderer/GLES20Canvas.java1009
-rw-r--r--src/com/android/gallery3d/glrenderer/GLES20IdImpl.java42
-rw-r--r--src/com/android/gallery3d/glrenderer/GLId.java33
-rw-r--r--src/com/android/gallery3d/glrenderer/GLPaint.java41
-rw-r--r--src/com/android/gallery3d/glrenderer/MultiLineTexture.java52
-rw-r--r--src/com/android/gallery3d/glrenderer/NinePatchChunk.java82
-rw-r--r--src/com/android/gallery3d/glrenderer/NinePatchTexture.java424
-rw-r--r--src/com/android/gallery3d/glrenderer/RawTexture.java73
-rw-r--r--src/com/android/gallery3d/glrenderer/ResourceTexture.java53
-rw-r--r--src/com/android/gallery3d/glrenderer/StringTexture.java88
-rw-r--r--src/com/android/gallery3d/glrenderer/Texture.java44
-rw-r--r--src/com/android/gallery3d/glrenderer/TextureUploader.java105
-rw-r--r--src/com/android/gallery3d/glrenderer/TiledTexture.java349
-rw-r--r--src/com/android/gallery3d/glrenderer/UploadedTexture.java298
-rw-r--r--src/com/android/gallery3d/ingest/ImportTask.java95
-rw-r--r--src/com/android/gallery3d/ingest/IngestActivity.java570
-rw-r--r--src/com/android/gallery3d/ingest/IngestService.java320
-rw-r--r--src/com/android/gallery3d/ingest/MtpDeviceIndex.java596
-rw-r--r--src/com/android/gallery3d/ingest/SimpleDate.java114
-rw-r--r--src/com/android/gallery3d/ingest/adapter/CheckBroker.java56
-rw-r--r--src/com/android/gallery3d/ingest/adapter/MtpAdapter.java192
-rw-r--r--src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java102
-rw-r--r--src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java29
-rw-r--r--src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java106
-rw-r--r--src/com/android/gallery3d/ingest/ui/DateTileView.java107
-rw-r--r--src/com/android/gallery3d/ingest/ui/IngestGridView.java58
-rw-r--r--src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java115
-rw-r--r--src/com/android/gallery3d/ingest/ui/MtpImageView.java280
-rw-r--r--src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java106
-rw-r--r--src/com/android/gallery3d/onetimeinitializer/GalleryWidgetMigrator.java169
-rw-r--r--src/com/android/gallery3d/provider/GalleryProvider.java228
-rw-r--r--src/com/android/gallery3d/ui/AbstractSlotRenderer.java119
-rw-r--r--src/com/android/gallery3d/ui/ActionModeHandler.java501
-rw-r--r--src/com/android/gallery3d/ui/AlbumLabelMaker.java206
-rw-r--r--src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java549
-rw-r--r--src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java242
-rw-r--r--src/com/android/gallery3d/ui/AlbumSlidingWindow.java365
-rw-r--r--src/com/android/gallery3d/ui/AlbumSlotRenderer.java201
-rw-r--r--src/com/android/gallery3d/ui/AnimationTime.java45
-rw-r--r--src/com/android/gallery3d/ui/BitmapLoader.java108
-rw-r--r--src/com/android/gallery3d/ui/BitmapScreenNail.java61
-rw-r--r--src/com/android/gallery3d/ui/BitmapTileProvider.java103
-rw-r--r--src/com/android/gallery3d/ui/CacheStorageUsageInfo.java90
-rw-r--r--src/com/android/gallery3d/ui/CaptureAnimation.java56
-rw-r--r--src/com/android/gallery3d/ui/DetailsAddressResolver.java118
-rw-r--r--src/com/android/gallery3d/ui/DetailsHelper.java148
-rw-r--r--src/com/android/gallery3d/ui/DialogDetailsView.java288
-rw-r--r--src/com/android/gallery3d/ui/DownUpDetector.java61
-rw-r--r--src/com/android/gallery3d/ui/EdgeEffect.java443
-rw-r--r--src/com/android/gallery3d/ui/EdgeView.java132
-rw-r--r--src/com/android/gallery3d/ui/FlingScroller.java141
-rw-r--r--src/com/android/gallery3d/ui/GLRoot.java53
-rw-r--r--src/com/android/gallery3d/ui/GLRootView.java630
-rw-r--r--src/com/android/gallery3d/ui/GLView.java465
-rw-r--r--src/com/android/gallery3d/ui/GestureRecognizer.java132
-rw-r--r--src/com/android/gallery3d/ui/Log.java54
-rw-r--r--src/com/android/gallery3d/ui/ManageCacheDrawer.java116
-rw-r--r--src/com/android/gallery3d/ui/MeasureHelper.java65
-rw-r--r--src/com/android/gallery3d/ui/MenuExecutor.java448
-rw-r--r--src/com/android/gallery3d/ui/OrientationSource.java22
-rw-r--r--src/com/android/gallery3d/ui/Paper.java183
-rw-r--r--src/com/android/gallery3d/ui/PhotoFallbackEffect.java179
-rw-r--r--src/com/android/gallery3d/ui/PhotoView.java1858
-rw-r--r--src/com/android/gallery3d/ui/PopupList.java206
-rw-r--r--src/com/android/gallery3d/ui/PositionController.java1821
-rw-r--r--src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java85
-rw-r--r--src/com/android/gallery3d/ui/ProgressSpinner.java80
-rw-r--r--src/com/android/gallery3d/ui/RelativePosition.java42
-rw-r--r--src/com/android/gallery3d/ui/ScreenNail.java35
-rw-r--r--src/com/android/gallery3d/ui/ScrollBarView.java97
-rw-r--r--src/com/android/gallery3d/ui/ScrollerHelper.java97
-rw-r--r--src/com/android/gallery3d/ui/SelectionManager.java251
-rw-r--r--src/com/android/gallery3d/ui/SelectionMenu.java61
-rw-r--r--src/com/android/gallery3d/ui/SlideshowView.java163
-rw-r--r--src/com/android/gallery3d/ui/SlotView.java788
-rw-r--r--src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java142
-rw-r--r--src/com/android/gallery3d/ui/SynchronizedHandler.java41
-rw-r--r--src/com/android/gallery3d/ui/TileImageView.java786
-rw-r--r--src/com/android/gallery3d/ui/TileImageViewAdapter.java200
-rw-r--r--src/com/android/gallery3d/ui/TiledScreenNail.java218
-rw-r--r--src/com/android/gallery3d/ui/UndoBarView.java211
-rw-r--r--src/com/android/gallery3d/ui/UserInteractionListener.java26
-rw-r--r--src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java66
-rw-r--r--src/com/android/gallery3d/util/AccessibilityUtils.java54
-rw-r--r--src/com/android/gallery3d/util/BucketNames.java29
-rw-r--r--src/com/android/gallery3d/util/CacheManager.java82
-rw-r--r--src/com/android/gallery3d/util/GalleryUtils.java404
-rw-r--r--src/com/android/gallery3d/util/Holder.java29
-rw-r--r--src/com/android/gallery3d/util/IdentityCache.java78
-rw-r--r--src/com/android/gallery3d/util/IntArray.java60
-rw-r--r--src/com/android/gallery3d/util/InterruptableOutputStream.java67
-rw-r--r--src/com/android/gallery3d/util/JobLimiter.java159
-rw-r--r--src/com/android/gallery3d/util/LinkedNode.java71
-rw-r--r--src/com/android/gallery3d/util/Log.java53
-rw-r--r--src/com/android/gallery3d/util/MediaSetUtils.java66
-rw-r--r--src/com/android/gallery3d/util/MotionEventHelper.java120
-rw-r--r--src/com/android/gallery3d/util/Profile.java226
-rw-r--r--src/com/android/gallery3d/util/ProfileData.java168
-rw-r--r--src/com/android/gallery3d/util/RangeArray.java52
-rw-r--r--src/com/android/gallery3d/util/RangeBoolArray.java49
-rw-r--r--src/com/android/gallery3d/util/RangeIntArray.java49
-rw-r--r--src/com/android/gallery3d/util/ReverseGeocoder.java418
-rw-r--r--src/com/android/gallery3d/util/SaveVideoFileInfo.java29
-rw-r--r--src/com/android/gallery3d/util/SaveVideoFileUtils.java154
-rw-r--r--src/com/android/gallery3d/util/UpdateHelper.java59
-rw-r--r--src/com/android/photos/AlbumActivity.java48
-rw-r--r--src/com/android/photos/AlbumFragment.java168
-rw-r--r--src/com/android/photos/AlbumSetFragment.java135
-rw-r--r--src/com/android/photos/BitmapRegionTileSource.java183
-rw-r--r--src/com/android/photos/FullscreenViewer.java44
-rw-r--r--src/com/android/photos/GalleryActivity.java184
-rw-r--r--src/com/android/photos/MultiChoiceManager.java295
-rw-r--r--src/com/android/photos/MultiSelectGridFragment.java348
-rw-r--r--src/com/android/photos/PhotoFragment.java25
-rw-r--r--src/com/android/photos/PhotoSetFragment.java133
-rw-r--r--src/com/android/photos/SelectionManager.java184
-rw-r--r--src/com/android/photos/adapters/AlbumSetCursorAdapter.java75
-rw-r--r--src/com/android/photos/adapters/PhotoThumbnailAdapter.java75
-rw-r--r--src/com/android/photos/data/AlbumSetLoader.java54
-rw-r--r--src/com/android/photos/data/BitmapDecoder.java224
-rw-r--r--src/com/android/photos/data/FileRetriever.java109
-rw-r--r--src/com/android/photos/data/GalleryBitmapPool.java161
-rw-r--r--src/com/android/photos/data/MediaCache.java676
-rw-r--r--src/com/android/photos/data/MediaCacheDatabase.java286
-rw-r--r--src/com/android/photos/data/MediaCacheUtils.java167
-rw-r--r--src/com/android/photos/data/MediaRetriever.java129
-rw-r--r--src/com/android/photos/data/NotificationWatcher.java55
-rw-r--r--src/com/android/photos/data/PhotoDatabase.java195
-rw-r--r--src/com/android/photos/data/PhotoProvider.java536
-rw-r--r--src/com/android/photos/data/PhotoSetLoader.java115
-rw-r--r--src/com/android/photos/data/SQLiteContentProvider.java265
-rw-r--r--src/com/android/photos/data/SparseArrayBitmapPool.java212
-rw-r--r--src/com/android/photos/drawables/AutoThumbnailDrawable.java309
-rw-r--r--src/com/android/photos/drawables/DataUriThumbnailDrawable.java54
-rw-r--r--src/com/android/photos/shims/BitmapJobDrawable.java180
-rw-r--r--src/com/android/photos/shims/LoaderCompatShim.java31
-rw-r--r--src/com/android/photos/shims/MediaItemsLoader.java190
-rw-r--r--src/com/android/photos/shims/MediaSetLoader.java191
-rw-r--r--src/com/android/photos/views/BlockingGLTextureView.java438
-rw-r--r--src/com/android/photos/views/GalleryThumbnailView.java883
-rw-r--r--src/com/android/photos/views/HeaderGridView.java466
-rw-r--r--src/com/android/photos/views/SquareImageView.java53
-rw-r--r--src/com/android/photos/views/TiledImageRenderer.java825
-rw-r--r--src/com/android/photos/views/TiledImageView.java382
553 files changed, 109638 insertions, 0 deletions
diff --git a/src/com/android/camera/AndroidCameraManagerImpl.java b/src/com/android/camera/AndroidCameraManagerImpl.java
new file mode 100644
index 000000000..897aa9252
--- /dev/null
+++ b/src/com/android/camera/AndroidCameraManagerImpl.java
@@ -0,0 +1,779 @@
+/*
+ * Copyright (C) 2013 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.camera;
+
+import static com.android.camera.Util.Assert;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.hardware.Camera.AutoFocusCallback;
+import android.hardware.Camera.AutoFocusMoveCallback;
+import android.hardware.Camera.ErrorCallback;
+import android.hardware.Camera.FaceDetectionListener;
+import android.hardware.Camera.OnZoomChangeListener;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.PictureCallback;
+import android.hardware.Camera.PreviewCallback;
+import android.hardware.Camera.ShutterCallback;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.view.SurfaceHolder;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.io.IOException;
+
+/**
+ * A class to implement {@link CameraManager} of the Android camera framework.
+ */
+class AndroidCameraManagerImpl implements CameraManager {
+ private static final String TAG = "CAM_" +
+ AndroidCameraManagerImpl.class.getSimpleName();
+
+ private Parameters mParameters;
+ private boolean mParametersIsDirty;
+ private IOException mReconnectIOException;
+
+ /* Messages used in CameraHandler. */
+ // Camera initialization/finalization
+ private static final int OPEN_CAMERA = 1;
+ private static final int RELEASE = 2;
+ private static final int RECONNECT = 3;
+ private static final int UNLOCK = 4;
+ private static final int LOCK = 5;
+ // Preview
+ private static final int SET_PREVIEW_TEXTURE_ASYNC = 101;
+ private static final int START_PREVIEW_ASYNC = 102;
+ private static final int STOP_PREVIEW = 103;
+ private static final int SET_PREVIEW_CALLBACK_WITH_BUFFER = 104;
+ private static final int ADD_CALLBACK_BUFFER = 105;
+ private static final int SET_PREVIEW_DISPLAY_ASYNC = 106;
+ private static final int SET_PREVIEW_CALLBACK = 107;
+ // Parameters
+ private static final int SET_PARAMETERS = 201;
+ private static final int GET_PARAMETERS = 202;
+ private static final int REFRESH_PARAMETERS = 203;
+ // Focus, Zoom
+ private static final int AUTO_FOCUS = 301;
+ private static final int CANCEL_AUTO_FOCUS = 302;
+ private static final int SET_AUTO_FOCUS_MOVE_CALLBACK = 303;
+ private static final int SET_ZOOM_CHANGE_LISTENER = 304;
+ // Face detection
+ private static final int SET_FACE_DETECTION_LISTENER = 461;
+ private static final int START_FACE_DETECTION = 462;
+ private static final int STOP_FACE_DETECTION = 463;
+ private static final int SET_ERROR_CALLBACK = 464;
+ // Presentation
+ private static final int ENABLE_SHUTTER_SOUND = 501;
+ private static final int SET_DISPLAY_ORIENTATION = 502;
+
+ private CameraHandler mCameraHandler;
+ private android.hardware.Camera mCamera;
+
+ // Used to retain a copy of Parameters for setting parameters.
+ private Parameters mParamsToSet;
+
+ AndroidCameraManagerImpl() {
+ HandlerThread ht = new HandlerThread("Camera Handler Thread");
+ ht.start();
+ mCameraHandler = new CameraHandler(ht.getLooper());
+ }
+
+ private class CameraHandler extends Handler {
+ CameraHandler(Looper looper) {
+ super(looper);
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void startFaceDetection() {
+ mCamera.startFaceDetection();
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void stopFaceDetection() {
+ mCamera.stopFaceDetection();
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void setFaceDetectionListener(FaceDetectionListener listener) {
+ mCamera.setFaceDetectionListener(listener);
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ private void setPreviewTexture(Object surfaceTexture) {
+ try {
+ mCamera.setPreviewTexture((SurfaceTexture) surfaceTexture);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN_MR1)
+ private void enableShutterSound(boolean enable) {
+ mCamera.enableShutterSound(enable);
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ private void setAutoFocusMoveCallback(
+ android.hardware.Camera camera, Object cb) {
+ camera.setAutoFocusMoveCallback((AutoFocusMoveCallback) cb);
+ }
+
+ public void requestTakePicture(
+ final ShutterCallback shutter,
+ final PictureCallback raw,
+ final PictureCallback postView,
+ final PictureCallback jpeg) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mCamera.takePicture(shutter, raw, postView, jpeg);
+ } catch (RuntimeException e) {
+ // TODO: output camera state and focus state for debugging.
+ Log.e(TAG, "take picture failed.");
+ throw e;
+ }
+ }
+ });
+ }
+
+ /**
+ * Waits for all the {@code Message} and {@code Runnable} currently in the queue
+ * are processed.
+ *
+ * @return {@code false} if the wait was interrupted, {@code true} otherwise.
+ */
+ public boolean waitDone() {
+ final Object waitDoneLock = new Object();
+ final Runnable unlockRunnable = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (waitDoneLock) {
+ waitDoneLock.notifyAll();
+ }
+ }
+ };
+
+ synchronized (waitDoneLock) {
+ mCameraHandler.post(unlockRunnable);
+ try {
+ waitDoneLock.wait();
+ } catch (InterruptedException ex) {
+ Log.v(TAG, "waitDone interrupted");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * This method does not deal with the API level check. Everyone should
+ * check first for supported operations before sending message to this handler.
+ */
+ @Override
+ public void handleMessage(final Message msg) {
+ try {
+ switch (msg.what) {
+ case OPEN_CAMERA:
+ mCamera = android.hardware.Camera.open(msg.arg1);
+ if (mCamera != null) {
+ mParametersIsDirty = true;
+
+ // Get a instance of Camera.Parameters for later use.
+ if (mParamsToSet == null) {
+ mParamsToSet = mCamera.getParameters();
+ }
+ }
+ return;
+
+ case RELEASE:
+ mCamera.release();
+ mCamera = null;
+ return;
+
+ case RECONNECT:
+ mReconnectIOException = null;
+ try {
+ mCamera.reconnect();
+ } catch (IOException ex) {
+ mReconnectIOException = ex;
+ }
+ return;
+
+ case UNLOCK:
+ mCamera.unlock();
+ return;
+
+ case LOCK:
+ mCamera.lock();
+ return;
+
+ case SET_PREVIEW_TEXTURE_ASYNC:
+ setPreviewTexture(msg.obj);
+ return;
+
+ case SET_PREVIEW_DISPLAY_ASYNC:
+ try {
+ mCamera.setPreviewDisplay((SurfaceHolder) msg.obj);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return;
+
+ case START_PREVIEW_ASYNC:
+ mCamera.startPreview();
+ return;
+
+ case STOP_PREVIEW:
+ mCamera.stopPreview();
+ return;
+
+ case SET_PREVIEW_CALLBACK_WITH_BUFFER:
+ mCamera.setPreviewCallbackWithBuffer(
+ (PreviewCallback) msg.obj);
+ return;
+
+ case ADD_CALLBACK_BUFFER:
+ mCamera.addCallbackBuffer((byte[]) msg.obj);
+ return;
+
+ case AUTO_FOCUS:
+ mCamera.autoFocus((AutoFocusCallback) msg.obj);
+ return;
+
+ case CANCEL_AUTO_FOCUS:
+ mCamera.cancelAutoFocus();
+ return;
+
+ case SET_AUTO_FOCUS_MOVE_CALLBACK:
+ setAutoFocusMoveCallback(mCamera, msg.obj);
+ return;
+
+ case SET_DISPLAY_ORIENTATION:
+ mCamera.setDisplayOrientation(msg.arg1);
+ return;
+
+ case SET_ZOOM_CHANGE_LISTENER:
+ mCamera.setZoomChangeListener(
+ (OnZoomChangeListener) msg.obj);
+ return;
+
+ case SET_FACE_DETECTION_LISTENER:
+ setFaceDetectionListener((FaceDetectionListener) msg.obj);
+ return;
+
+ case START_FACE_DETECTION:
+ startFaceDetection();
+ return;
+
+ case STOP_FACE_DETECTION:
+ stopFaceDetection();
+ return;
+
+ case SET_ERROR_CALLBACK:
+ mCamera.setErrorCallback((ErrorCallback) msg.obj);
+ return;
+
+ case SET_PARAMETERS:
+ mParametersIsDirty = true;
+ mParamsToSet.unflatten((String) msg.obj);
+ mCamera.setParameters(mParamsToSet);
+ return;
+
+ case GET_PARAMETERS:
+ if (mParametersIsDirty) {
+ mParameters = mCamera.getParameters();
+ mParametersIsDirty = false;
+ }
+ return;
+
+ case SET_PREVIEW_CALLBACK:
+ mCamera.setPreviewCallback((PreviewCallback) msg.obj);
+ return;
+
+ case ENABLE_SHUTTER_SOUND:
+ enableShutterSound((msg.arg1 == 1) ? true : false);
+ return;
+
+ case REFRESH_PARAMETERS:
+ mParametersIsDirty = true;
+ return;
+
+ default:
+ throw new RuntimeException("Invalid CameraProxy message=" + msg.what);
+ }
+ } catch (RuntimeException e) {
+ if (msg.what != RELEASE && mCamera != null) {
+ try {
+ mCamera.release();
+ } catch (Exception ex) {
+ Log.e(TAG, "Fail to release the camera.");
+ }
+ mCamera = null;
+ }
+ throw e;
+ }
+ }
+ }
+
+ @Override
+ public CameraManager.CameraProxy cameraOpen(int cameraId) {
+ mCameraHandler.obtainMessage(OPEN_CAMERA, cameraId, 0).sendToTarget();
+ mCameraHandler.waitDone();
+ if (mCamera != null) {
+ return new AndroidCameraProxyImpl();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * A class which implements {@link CameraManager.CameraProxy} and
+ * camera handler thread.
+ */
+ public class AndroidCameraProxyImpl implements CameraManager.CameraProxy {
+
+ private AndroidCameraProxyImpl() {
+ Assert(mCamera != null);
+ }
+
+ @Override
+ public android.hardware.Camera getCamera() {
+ return mCamera;
+ }
+
+ @Override
+ public void release() {
+ // release() must be synchronous so we know exactly when the camera
+ // is released and can continue on.
+ mCameraHandler.sendEmptyMessage(RELEASE);
+ mCameraHandler.waitDone();
+ }
+
+ @Override
+ public void reconnect() throws IOException {
+ mCameraHandler.sendEmptyMessage(RECONNECT);
+ mCameraHandler.waitDone();
+ if (mReconnectIOException != null) {
+ throw mReconnectIOException;
+ }
+ }
+
+ @Override
+ public void unlock() {
+ mCameraHandler.sendEmptyMessage(UNLOCK);
+ mCameraHandler.waitDone();
+ }
+
+ @Override
+ public void lock() {
+ mCameraHandler.sendEmptyMessage(LOCK);
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ @Override
+ public void setPreviewTexture(SurfaceTexture surfaceTexture) {
+ mCameraHandler.obtainMessage(SET_PREVIEW_TEXTURE_ASYNC, surfaceTexture).sendToTarget();
+ }
+
+ @Override
+ public void setPreviewDisplay(SurfaceHolder surfaceHolder) {
+ mCameraHandler.obtainMessage(SET_PREVIEW_DISPLAY_ASYNC, surfaceHolder).sendToTarget();
+ }
+
+ @Override
+ public void startPreview() {
+ mCameraHandler.sendEmptyMessage(START_PREVIEW_ASYNC);
+ }
+
+ @Override
+ public void stopPreview() {
+ mCameraHandler.sendEmptyMessage(STOP_PREVIEW);
+ mCameraHandler.waitDone();
+ }
+
+ @Override
+ public void setPreviewDataCallback(
+ Handler handler, CameraPreviewDataCallback cb) {
+ mCameraHandler.obtainMessage(
+ SET_PREVIEW_CALLBACK,
+ PreviewCallbackForward.getNewInstance(handler, this, cb)).sendToTarget();
+ }
+
+ @Override
+ public void setPreviewDataCallbackWithBuffer(
+ Handler handler, CameraPreviewDataCallback cb) {
+ mCameraHandler.obtainMessage(
+ SET_PREVIEW_CALLBACK_WITH_BUFFER,
+ PreviewCallbackForward.getNewInstance(handler, this, cb)).sendToTarget();
+ }
+
+ @Override
+ public void addCallbackBuffer(byte[] callbackBuffer) {
+ mCameraHandler.obtainMessage(ADD_CALLBACK_BUFFER, callbackBuffer).sendToTarget();
+ }
+
+ @Override
+ public void autoFocus(Handler handler, CameraAFCallback cb) {
+ mCameraHandler.obtainMessage(
+ AUTO_FOCUS,
+ AFCallbackForward.getNewInstance(handler, this, cb)).sendToTarget();
+ }
+
+ @Override
+ public void cancelAutoFocus() {
+ mCameraHandler.removeMessages(AUTO_FOCUS);
+ mCameraHandler.sendEmptyMessage(CANCEL_AUTO_FOCUS);
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ @Override
+ public void setAutoFocusMoveCallback(
+ Handler handler, CameraAFMoveCallback cb) {
+ mCameraHandler.obtainMessage(
+ SET_AUTO_FOCUS_MOVE_CALLBACK,
+ AFMoveCallbackForward.getNewInstance(handler, this, cb)).sendToTarget();
+ }
+
+ @Override
+ public void takePicture(
+ Handler handler,
+ CameraShutterCallback shutter,
+ CameraPictureCallback raw,
+ CameraPictureCallback post,
+ CameraPictureCallback jpeg) {
+ mCameraHandler.requestTakePicture(
+ ShutterCallbackForward.getNewInstance(handler, this, shutter),
+ PictureCallbackForward.getNewInstance(handler, this, raw),
+ PictureCallbackForward.getNewInstance(handler, this, post),
+ PictureCallbackForward.getNewInstance(handler, this, jpeg));
+ }
+
+ @Override
+ public void setDisplayOrientation(int degrees) {
+ mCameraHandler.obtainMessage(SET_DISPLAY_ORIENTATION, degrees, 0)
+ .sendToTarget();
+ }
+
+ @Override
+ public void setZoomChangeListener(OnZoomChangeListener listener) {
+ mCameraHandler.obtainMessage(SET_ZOOM_CHANGE_LISTENER, listener).sendToTarget();
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ public void setFaceDetectionCallback(
+ Handler handler, CameraFaceDetectionCallback cb) {
+ mCameraHandler.obtainMessage(
+ SET_FACE_DETECTION_LISTENER,
+ FaceDetectionCallbackForward.getNewInstance(handler, this, cb)).sendToTarget();
+ }
+
+ @Override
+ public void startFaceDetection() {
+ mCameraHandler.sendEmptyMessage(START_FACE_DETECTION);
+ }
+
+ @Override
+ public void stopFaceDetection() {
+ mCameraHandler.sendEmptyMessage(STOP_FACE_DETECTION);
+ }
+
+ @Override
+ public void setErrorCallback(ErrorCallback cb) {
+ mCameraHandler.obtainMessage(SET_ERROR_CALLBACK, cb).sendToTarget();
+ }
+
+ @Override
+ public void setParameters(Parameters params) {
+ if (params == null) {
+ Log.v(TAG, "null parameters in setParameters()");
+ return;
+ }
+ mCameraHandler.obtainMessage(SET_PARAMETERS, params.flatten())
+ .sendToTarget();
+ }
+
+ @Override
+ public Parameters getParameters() {
+ mCameraHandler.sendEmptyMessage(GET_PARAMETERS);
+ mCameraHandler.waitDone();
+ return mParameters;
+ }
+
+ @Override
+ public void refreshParameters() {
+ mCameraHandler.sendEmptyMessage(REFRESH_PARAMETERS);
+ }
+
+ @Override
+ public void enableShutterSound(boolean enable) {
+ mCameraHandler.obtainMessage(
+ ENABLE_SHUTTER_SOUND, (enable ? 1 : 0), 0).sendToTarget();
+ }
+ }
+
+ /**
+ * A helper class to forward AutoFocusCallback to another thread.
+ */
+ private static class AFCallbackForward implements AutoFocusCallback {
+ private final Handler mHandler;
+ private final CameraProxy mCamera;
+ private final CameraAFCallback mCallback;
+
+ /**
+ * Returns a new instance of {@link AFCallbackForward}.
+ *
+ * @param handler The handler in which the callback will be invoked in.
+ * @param camera The {@link CameraProxy} which the callback is from.
+ * @param cb The callback to be invoked.
+ * @return The instance of the {@link AFCallbackForward},
+ * or null if any parameter is null.
+ */
+ public static AFCallbackForward getNewInstance(
+ Handler handler, CameraProxy camera, CameraAFCallback cb) {
+ if (handler == null || camera == null || cb == null) return null;
+ return new AFCallbackForward(handler, camera, cb);
+ }
+
+ private AFCallbackForward(
+ Handler h, CameraProxy camera, CameraAFCallback cb) {
+ mHandler = h;
+ mCamera = camera;
+ mCallback = cb;
+ }
+
+ @Override
+ public void onAutoFocus(final boolean b, Camera camera) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onAutoFocus(b, mCamera);
+ }
+ });
+ }
+ }
+
+ /** A helper class to forward AutoFocusMoveCallback to another thread. */
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ private static class AFMoveCallbackForward implements AutoFocusMoveCallback {
+ private final Handler mHandler;
+ private final CameraAFMoveCallback mCallback;
+ private final CameraProxy mCamera;
+
+ /**
+ * Returns a new instance of {@link AFMoveCallbackForward}.
+ *
+ * @param handler The handler in which the callback will be invoked in.
+ * @param camera The {@link CameraProxy} which the callback is from.
+ * @param cb The callback to be invoked.
+ * @return The instance of the {@link AFMoveCallbackForward},
+ * or null if any parameter is null.
+ */
+ public static AFMoveCallbackForward getNewInstance(
+ Handler handler, CameraProxy camera, CameraAFMoveCallback cb) {
+ if (handler == null || camera == null || cb == null) return null;
+ return new AFMoveCallbackForward(handler, camera, cb);
+ }
+
+ private AFMoveCallbackForward(
+ Handler h, CameraProxy camera, CameraAFMoveCallback cb) {
+ mHandler = h;
+ mCamera = camera;
+ mCallback = cb;
+ }
+
+ @Override
+ public void onAutoFocusMoving(
+ final boolean moving, android.hardware.Camera camera) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onAutoFocusMoving(moving, mCamera);
+ }
+ });
+ }
+ }
+
+ /**
+ * A helper class to forward ShutterCallback to to another thread.
+ */
+ private static class ShutterCallbackForward implements ShutterCallback {
+ private final Handler mHandler;
+ private final CameraShutterCallback mCallback;
+ private final CameraProxy mCamera;
+
+ /**
+ * Returns a new instance of {@link ShutterCallbackForward}.
+ *
+ * @param handler The handler in which the callback will be invoked in.
+ * @param camera The {@link CameraProxy} which the callback is from.
+ * @param cb The callback to be invoked.
+ * @return The instance of the {@link ShutterCallbackForward},
+ * or null if any parameter is null.
+ */
+ public static ShutterCallbackForward getNewInstance(
+ Handler handler, CameraProxy camera, CameraShutterCallback cb) {
+ if (handler == null || camera == null || cb == null) return null;
+ return new ShutterCallbackForward(handler, camera, cb);
+ }
+
+ private ShutterCallbackForward(
+ Handler h, CameraProxy camera, CameraShutterCallback cb) {
+ mHandler = h;
+ mCamera = camera;
+ mCallback = cb;
+ }
+
+ @Override
+ public void onShutter() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onShutter(mCamera);
+ }
+ });
+ }
+ }
+
+ /**
+ * A helper class to forward PictureCallback to another thread.
+ */
+ private static class PictureCallbackForward implements PictureCallback {
+ private final Handler mHandler;
+ private final CameraPictureCallback mCallback;
+ private final CameraProxy mCamera;
+
+ /**
+ * Returns a new instance of {@link PictureCallbackForward}.
+ *
+ * @param handler The handler in which the callback will be invoked in.
+ * @param camera The {@link CameraProxy} which the callback is from.
+ * @param cb The callback to be invoked.
+ * @return The instance of the {@link PictureCallbackForward},
+ * or null if any parameters is null.
+ */
+ public static PictureCallbackForward getNewInstance(
+ Handler handler, CameraProxy camera, CameraPictureCallback cb) {
+ if (handler == null || camera == null || cb == null) return null;
+ return new PictureCallbackForward(handler, camera, cb);
+ }
+
+ private PictureCallbackForward(
+ Handler h, CameraProxy camera, CameraPictureCallback cb) {
+ mHandler = h;
+ mCamera = camera;
+ mCallback = cb;
+ }
+
+ @Override
+ public void onPictureTaken(
+ final byte[] data, android.hardware.Camera camera) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onPictureTaken(data, mCamera);
+ }
+ });
+ }
+ }
+
+ /**
+ * A helper class to forward PreviewCallback to another thread.
+ */
+ private static class PreviewCallbackForward implements PreviewCallback {
+ private final Handler mHandler;
+ private final CameraPreviewDataCallback mCallback;
+ private final CameraProxy mCamera;
+
+ /**
+ * Returns a new instance of {@link PreviewCallbackForward}.
+ *
+ * @param handler The handler in which the callback will be invoked in.
+ * @param camera The {@link CameraProxy} which the callback is from.
+ * @param cb The callback to be invoked.
+ * @return The instance of the {@link PreviewCallbackForward},
+ * or null if any parameters is null.
+ */
+ public static PreviewCallbackForward getNewInstance(
+ Handler handler, CameraProxy camera, CameraPreviewDataCallback cb) {
+ if (handler == null || camera == null || cb == null) return null;
+ return new PreviewCallbackForward(handler, camera, cb);
+ }
+
+ private PreviewCallbackForward(
+ Handler h, CameraProxy camera, CameraPreviewDataCallback cb) {
+ mHandler = h;
+ mCamera = camera;
+ mCallback = cb;
+ }
+
+ @Override
+ public void onPreviewFrame(
+ final byte[] data, android.hardware.Camera camera) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onPreviewFrame(data, mCamera);
+ }
+ });
+ }
+ }
+
+ private static class FaceDetectionCallbackForward implements FaceDetectionListener {
+ private final Handler mHandler;
+ private final CameraFaceDetectionCallback mCallback;
+ private final CameraProxy mCamera;
+
+ /**
+ * Returns a new instance of {@link FaceDetectionCallbackForward}.
+ *
+ * @param handler The handler in which the callback will be invoked in.
+ * @param camera The {@link CameraProxy} which the callback is from.
+ * @param cb The callback to be invoked.
+ * @return The instance of the {@link FaceDetectionCallbackForward},
+ * or null if any parameter is null.
+ */
+ public static FaceDetectionCallbackForward getNewInstance(
+ Handler handler, CameraProxy camera, CameraFaceDetectionCallback cb) {
+ if (handler == null || camera == null || cb == null) return null;
+ return new FaceDetectionCallbackForward(handler, camera, cb);
+ }
+
+ private FaceDetectionCallbackForward(
+ Handler h, CameraProxy camera, CameraFaceDetectionCallback cb) {
+ mHandler = h;
+ mCamera = camera;
+ mCallback = cb;
+ }
+
+ @Override
+ public void onFaceDetection(
+ final Camera.Face[] faces, Camera camera) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onFaceDetection(faces, mCamera);
+ }
+ });
+ }
+ }
+}
diff --git a/src/com/android/camera/CameraActivity.java b/src/com/android/camera/CameraActivity.java
new file mode 100644
index 000000000..7f71d5f31
--- /dev/null
+++ b/src/com/android/camera/CameraActivity.java
@@ -0,0 +1,571 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.graphics.drawable.ColorDrawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.provider.Settings;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.OrientationEventListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.ImageView;
+
+import com.android.camera.data.CameraDataAdapter;
+import com.android.camera.data.CameraPreviewData;
+import com.android.camera.data.FixedFirstDataAdapter;
+import com.android.camera.data.FixedLastDataAdapter;
+import com.android.camera.data.LocalData;
+import com.android.camera.data.LocalDataAdapter;
+import com.android.camera.ui.CameraSwitcher;
+import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
+import com.android.camera.ui.FilmStripView;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.RefocusHelper;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper;
+
+public class CameraActivity extends Activity
+ implements CameraSwitchListener {
+
+ private static final String TAG = "CAM_Activity";
+
+ private static final String INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE =
+ "android.media.action.STILL_IMAGE_CAMERA_SECURE";
+ public static final String ACTION_IMAGE_CAPTURE_SECURE =
+ "android.media.action.IMAGE_CAPTURE_SECURE";
+
+ // The intent extra for camera from secure lock screen. True if the gallery
+ // should only show newly captured pictures. sSecureAlbumId does not
+ // increment. This is used when switching between camera, camcorder, and
+ // panorama. If the extra is not set, it is in the normal camera mode.
+ public static final String SECURE_CAMERA_EXTRA = "secure_camera";
+
+ /** This data adapter is used by FilmStirpView. */
+ private LocalDataAdapter mDataAdapter;
+ /** This data adapter represents the real local camera data. */
+ private LocalDataAdapter mWrappedDataAdapter;
+
+ private PanoramaStitchingManager mPanoramaManager;
+ private int mCurrentModuleIndex;
+ private CameraModule mCurrentModule;
+ private View mRootView;
+ private FilmStripView mFilmStripView;
+ private int mResultCodeForTesting;
+ private Intent mResultDataForTesting;
+ private OnScreenHint mStorageHint;
+ private long mStorageSpace = Storage.LOW_STORAGE_THRESHOLD;
+ private boolean mAutoRotateScreen;
+ private boolean mSecureCamera;
+ // This is a hack to speed up the start of SecureCamera.
+ private static boolean sFirstStartAfterScreenOn = true;
+ private boolean mShowCameraPreview;
+ private int mLastRawOrientation;
+ private MyOrientationEventListener mOrientationListener;
+ private Handler mMainHandler;
+ private PanoramaViewHelper mPanoramaViewHelper;
+ private CameraPreviewData mCameraPreviewData;
+
+ private class MyOrientationEventListener
+ extends OrientationEventListener {
+ public MyOrientationEventListener(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onOrientationChanged(int orientation) {
+ // We keep the last known orientation. So if the user first orient
+ // the camera then point the camera to floor or sky, we still have
+ // the correct orientation.
+ if (orientation == ORIENTATION_UNKNOWN) return;
+ mLastRawOrientation = orientation;
+ mCurrentModule.onOrientationChanged(orientation);
+ }
+ }
+
+ private MediaSaveService mMediaSaveService;
+ private ServiceConnection mConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder b) {
+ mMediaSaveService = ((MediaSaveService.LocalBinder) b).getService();
+ mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService);
+ }
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ mMediaSaveService = null;
+ }};
+
+ // close activity when screen turns off
+ private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ finish();
+ }
+ };
+
+ private static BroadcastReceiver sScreenOffReceiver;
+ private static class ScreenOffReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ sFirstStartAfterScreenOn = true;
+ }
+ }
+
+ public static boolean isFirstStartAfterScreenOn() {
+ return sFirstStartAfterScreenOn;
+ }
+
+ public static void resetFirstStartAfterScreenOn() {
+ sFirstStartAfterScreenOn = false;
+ }
+
+ private FilmStripView.Listener mFilmStripListener = new FilmStripView.Listener() {
+ @Override
+ public void onDataPromoted(int dataID) {
+ removeData(dataID);
+ }
+
+ @Override
+ public void onDataDemoted(int dataID) {
+ removeData(dataID);
+ }
+
+ @Override
+ public void onDataFullScreenChange(int dataID, boolean full) {
+ }
+
+ @Override
+ public void onSwitchMode(boolean toCamera) {
+ mCurrentModule.onSwitchMode(toCamera);
+ }
+ };
+
+ private Runnable mDeletionRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mDataAdapter.executeDeletion(CameraActivity.this);
+ }
+ };
+
+ private ImageTaskManager.TaskListener mStitchingListener =
+ new ImageTaskManager.TaskListener() {
+ @Override
+ public void onTaskQueued(String filePath, Uri imageUri) {
+ }
+
+ @Override
+ public void onTaskDone(String filePath, Uri imageUri) {
+ }
+
+ @Override
+ public void onTaskProgress(
+ String filePath, Uri imageUri, int progress) {
+ }
+ };
+
+ public MediaSaveService getMediaSaveService() {
+ return mMediaSaveService;
+ }
+
+ public void notifyNewMedia(Uri uri) {
+ ContentResolver cr = getContentResolver();
+ String mimeType = cr.getType(uri);
+ if (mimeType.startsWith("video/")) {
+ sendBroadcast(new Intent(Util.ACTION_NEW_VIDEO, uri));
+ mDataAdapter.addNewVideo(cr, uri);
+ } else if (mimeType.startsWith("image/")) {
+ Util.broadcastNewPicture(this, uri);
+ mDataAdapter.addNewPhoto(cr, uri);
+ } else {
+ android.util.Log.w(TAG, "Unknown new media with MIME type:"
+ + mimeType + ", uri:" + uri);
+ }
+ }
+
+ private void removeData(int dataID) {
+ mDataAdapter.removeData(CameraActivity.this, dataID);
+ mMainHandler.removeCallbacks(mDeletionRunnable);
+ mMainHandler.postDelayed(mDeletionRunnable, 3000);
+ }
+
+ private void bindMediaSaveService() {
+ Intent intent = new Intent(this, MediaSaveService.class);
+ startService(intent); // start service before binding it so the
+ // service won't be killed if we unbind it.
+ bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ private void unbindMediaSaveService() {
+ if (mMediaSaveService != null) {
+ mMediaSaveService.setListener(null);
+ }
+ if (mConnection != null) {
+ unbindService(mConnection);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+ setContentView(R.layout.camera_filmstrip);
+ if (ApiHelper.HAS_ROTATION_ANIMATION) {
+ setRotationAnimation();
+ }
+ // Check if this is in the secure camera mode.
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ if (INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE.equals(action)
+ || ACTION_IMAGE_CAPTURE_SECURE.equals(action)) {
+ mSecureCamera = true;
+ } else {
+ mSecureCamera = intent.getBooleanExtra(SECURE_CAMERA_EXTRA, false);
+ }
+
+ if (mSecureCamera) {
+ // Change the window flags so that secure camera can show when locked
+ Window win = getWindow();
+ WindowManager.LayoutParams params = win.getAttributes();
+ params.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
+ win.setAttributes(params);
+
+ // Filter for screen off so that we can finish secure camera activity
+ // when screen is off.
+ IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
+ registerReceiver(mScreenOffReceiver, filter);
+ // TODO: This static screen off event receiver is a workaround to the
+ // double onResume() invocation (onResume->onPause->onResume). We should
+ // find a better solution to this.
+ if (sScreenOffReceiver == null) {
+ sScreenOffReceiver = new ScreenOffReceiver();
+ registerReceiver(sScreenOffReceiver, filter);
+ }
+ }
+ mPanoramaManager = new PanoramaStitchingManager(CameraActivity.this);
+ mPanoramaManager.addTaskListener(mStitchingListener);
+ LayoutInflater inflater = getLayoutInflater();
+ View rootLayout = inflater.inflate(R.layout.camera, null, false);
+ mRootView = rootLayout.findViewById(R.id.camera_app_root);
+ mCameraPreviewData = new CameraPreviewData(rootLayout,
+ FilmStripView.ImageData.SIZE_FULL,
+ FilmStripView.ImageData.SIZE_FULL);
+ // Put a CameraPreviewData at the first position.
+ mWrappedDataAdapter = new FixedFirstDataAdapter(
+ new CameraDataAdapter(new ColorDrawable(
+ getResources().getColor(R.color.photo_placeholder))),
+ mCameraPreviewData);
+ mFilmStripView = (FilmStripView) findViewById(R.id.filmstrip_view);
+ mFilmStripView.setViewGap(
+ getResources().getDimensionPixelSize(R.dimen.camera_film_strip_gap));
+ mPanoramaViewHelper = new PanoramaViewHelper(this);
+ mPanoramaViewHelper.onCreate();
+ mFilmStripView.setPanoramaViewHelper(mPanoramaViewHelper);
+ // Set up the camera preview first so the preview shows up ASAP.
+ mFilmStripView.setListener(mFilmStripListener);
+ mCurrentModule = new PhotoModule();
+ mCurrentModule.init(this, mRootView);
+ mOrientationListener = new MyOrientationEventListener(this);
+ mMainHandler = new Handler(getMainLooper());
+ bindMediaSaveService();
+
+ if (!mSecureCamera) {
+ mDataAdapter = mWrappedDataAdapter;
+ mDataAdapter.requestLoad(getContentResolver());
+ } else {
+ // Put a lock placeholder as the last image by setting its date to 0.
+ ImageView v = (ImageView) getLayoutInflater().inflate(
+ R.layout.secure_album_placeholder, null);
+ mDataAdapter = new FixedLastDataAdapter(
+ mWrappedDataAdapter,
+ new LocalData.LocalViewData(
+ v,
+ v.getDrawable().getIntrinsicWidth(),
+ v.getDrawable().getIntrinsicHeight(),
+ 0, 0));
+ // Flush out all the original data.
+ mDataAdapter.flush();
+ }
+ mFilmStripView.setDataAdapter(mDataAdapter);
+ }
+
+ private void setRotationAnimation() {
+ int rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
+ rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_CROSSFADE;
+ Window win = getWindow();
+ WindowManager.LayoutParams winParams = win.getAttributes();
+ winParams.rotationAnimation = rotationAnimation;
+ win.setAttributes(winParams);
+ }
+
+ @Override
+ public void onUserInteraction() {
+ super.onUserInteraction();
+ mCurrentModule.onUserInteraction();
+ }
+
+ @Override
+ public void onPause() {
+ mOrientationListener.disable();
+ mCurrentModule.onPauseBeforeSuper();
+ super.onPause();
+ mCurrentModule.onPauseAfterSuper();
+ }
+
+ @Override
+ public void onResume() {
+ if (Settings.System.getInt(getContentResolver(),
+ Settings.System.ACCELEROMETER_ROTATION, 0) == 0) {// auto-rotate off
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+ mAutoRotateScreen = false;
+ } else {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
+ mAutoRotateScreen = true;
+ }
+ mOrientationListener.enable();
+ mCurrentModule.onResumeBeforeSuper();
+ super.onResume();
+ mCurrentModule.onResumeAfterSuper();
+
+ setSwipingEnabled(true);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ mPanoramaViewHelper.onStart();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mPanoramaViewHelper.onStop();
+ }
+
+ @Override
+ public void onDestroy() {
+ unbindMediaSaveService();
+ if (mSecureCamera) unregisterReceiver(mScreenOffReceiver);
+ super.onDestroy();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration config) {
+ super.onConfigurationChanged(config);
+ mCurrentModule.onConfigurationChanged(config);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (mCurrentModule.onKeyDown(keyCode, event)) return true;
+ // Prevent software keyboard or voice search from showing up.
+ if (keyCode == KeyEvent.KEYCODE_SEARCH
+ || keyCode == KeyEvent.KEYCODE_MENU) {
+ if (event.isLongPress()) return true;
+ }
+ if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraPreview) {
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (mCurrentModule.onKeyUp(keyCode, event)) return true;
+ if (keyCode == KeyEvent.KEYCODE_MENU && mShowCameraPreview) {
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ public boolean isAutoRotateScreen() {
+ return mAutoRotateScreen;
+ }
+
+ protected void updateStorageSpace() {
+ mStorageSpace = Storage.getAvailableSpace();
+ }
+
+ protected long getStorageSpace() {
+ return mStorageSpace;
+ }
+
+ protected void updateStorageSpaceAndHint() {
+ updateStorageSpace();
+ updateStorageHint(mStorageSpace);
+ }
+
+ protected void updateStorageHint() {
+ updateStorageHint(mStorageSpace);
+ }
+
+ protected boolean updateStorageHintOnResume() {
+ return true;
+ }
+
+ protected void updateStorageHint(long storageSpace) {
+ String message = null;
+ if (storageSpace == Storage.UNAVAILABLE) {
+ message = getString(R.string.no_storage);
+ } else if (storageSpace == Storage.PREPARING) {
+ message = getString(R.string.preparing_sd);
+ } else if (storageSpace == Storage.UNKNOWN_SIZE) {
+ message = getString(R.string.access_sd_fail);
+ } else if (storageSpace <= Storage.LOW_STORAGE_THRESHOLD) {
+ message = getString(R.string.spaceIsLow_content);
+ }
+
+ if (message != null) {
+ if (mStorageHint == null) {
+ mStorageHint = OnScreenHint.makeText(this, message);
+ } else {
+ mStorageHint.setText(message);
+ }
+ mStorageHint.show();
+ } else if (mStorageHint != null) {
+ mStorageHint.cancel();
+ mStorageHint = null;
+ }
+ }
+
+ protected void setResultEx(int resultCode) {
+ mResultCodeForTesting = resultCode;
+ setResult(resultCode);
+ }
+
+ protected void setResultEx(int resultCode, Intent data) {
+ mResultCodeForTesting = resultCode;
+ mResultDataForTesting = data;
+ setResult(resultCode, data);
+ }
+
+ public int getResultCode() {
+ return mResultCodeForTesting;
+ }
+
+ public Intent getResultData() {
+ return mResultDataForTesting;
+ }
+
+ public boolean isSecureCamera() {
+ return mSecureCamera;
+ }
+
+ @Override
+ public void onCameraSelected(int i) {
+ if (mCurrentModuleIndex == i) return;
+
+ CameraHolder.instance().keep();
+ closeModule(mCurrentModule);
+ mCurrentModuleIndex = i;
+ switch (i) {
+ case CameraSwitcher.VIDEO_MODULE_INDEX:
+ mCurrentModule = new VideoModule();
+ break;
+ case CameraSwitcher.PHOTO_MODULE_INDEX:
+ mCurrentModule = new PhotoModule();
+ break;
+ case CameraSwitcher.LIGHTCYCLE_MODULE_INDEX:
+ mCurrentModule = LightCycleHelper.createPanoramaModule();
+ break;
+ case CameraSwitcher.REFOCUS_MODULE_INDEX:
+ mCurrentModule = RefocusHelper.createRefocusModule();
+ break;
+ default:
+ break;
+ }
+
+ openModule(mCurrentModule);
+ mCurrentModule.onOrientationChanged(mLastRawOrientation);
+ if (mMediaSaveService != null) {
+ mCurrentModule.onMediaSaveServiceConnected(mMediaSaveService);
+ }
+ }
+
+ private void openModule(CameraModule module) {
+ module.init(this, mRootView);
+ module.onResumeBeforeSuper();
+ module.onResumeAfterSuper();
+ }
+
+ private void closeModule(CameraModule module) {
+ module.onPauseBeforeSuper();
+ module.onPauseAfterSuper();
+ ((ViewGroup) mRootView).removeAllViews();
+ }
+
+ @Override
+ public void onShowSwitcherPopup() {
+ }
+
+ public void setSwipingEnabled(boolean enable) {
+ mCameraPreviewData.lockPreview(!enable);
+ }
+
+ // Accessor methods for getting latency times used in performance testing
+ public long getAutoFocusTime() {
+ return (mCurrentModule instanceof PhotoModule) ?
+ ((PhotoModule) mCurrentModule).mAutoFocusTime : -1;
+ }
+
+ public long getShutterLag() {
+ return (mCurrentModule instanceof PhotoModule) ?
+ ((PhotoModule) mCurrentModule).mShutterLag : -1;
+ }
+
+ public long getShutterToPictureDisplayedTime() {
+ return (mCurrentModule instanceof PhotoModule) ?
+ ((PhotoModule) mCurrentModule).mShutterToPictureDisplayedTime : -1;
+ }
+
+ public long getPictureDisplayedToJpegCallbackTime() {
+ return (mCurrentModule instanceof PhotoModule) ?
+ ((PhotoModule) mCurrentModule).mPictureDisplayedToJpegCallbackTime : -1;
+ }
+
+ public long getJpegCallbackFinishTime() {
+ return (mCurrentModule instanceof PhotoModule) ?
+ ((PhotoModule) mCurrentModule).mJpegCallbackFinishTime : -1;
+ }
+
+ public long getCaptureStartTime() {
+ return (mCurrentModule instanceof PhotoModule) ?
+ ((PhotoModule) mCurrentModule).mCaptureStartTime : -1;
+ }
+
+ public boolean isRecording() {
+ return (mCurrentModule instanceof VideoModule) ?
+ ((VideoModule) mCurrentModule).isRecording() : false;
+ }
+}
diff --git a/src/com/android/camera/CameraBackupAgent.java b/src/com/android/camera/CameraBackupAgent.java
new file mode 100644
index 000000000..30ba212df
--- /dev/null
+++ b/src/com/android/camera/CameraBackupAgent.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.app.backup.BackupAgentHelper;
+import android.app.backup.SharedPreferencesBackupHelper;
+import android.content.Context;
+
+public class CameraBackupAgent extends BackupAgentHelper {
+ private static final String CAMERA_BACKUP_KEY = "camera_prefs";
+
+ public void onCreate () {
+ Context context = getApplicationContext();
+ String prefNames[] = ComboPreferences.getSharedPreferencesNames(context);
+
+ addHelper(CAMERA_BACKUP_KEY, new SharedPreferencesBackupHelper(context, prefNames));
+ }
+}
diff --git a/src/com/android/camera/CameraButtonIntentReceiver.java b/src/com/android/camera/CameraButtonIntentReceiver.java
new file mode 100644
index 000000000..a65942d57
--- /dev/null
+++ b/src/com/android/camera/CameraButtonIntentReceiver.java
@@ -0,0 +1,53 @@
+/*
+ * 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.camera;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * {@code CameraButtonIntentReceiver} is invoked when the camera button is
+ * long-pressed.
+ *
+ * It is declared in {@code AndroidManifest.xml} to receive the
+ * {@code android.intent.action.CAMERA_BUTTON} intent.
+ *
+ * After making sure we can use the camera hardware, it starts the Camera
+ * activity.
+ */
+public class CameraButtonIntentReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Try to get the camera hardware
+ CameraHolder holder = CameraHolder.instance();
+ ComboPreferences pref = new ComboPreferences(context);
+ int cameraId = CameraSettings.readPreferredCameraId(pref);
+ if (holder.tryOpen(cameraId) == null) return;
+
+ // We are going to launch the camera, so hold the camera for later use
+ holder.keep();
+ holder.release();
+ Intent i = new Intent(Intent.ACTION_MAIN);
+ i.setClass(context, CameraActivity.class);
+ i.addCategory(Intent.CATEGORY_LAUNCHER);
+ i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ context.startActivity(i);
+ }
+}
diff --git a/src/com/android/camera/CameraDisabledException.java b/src/com/android/camera/CameraDisabledException.java
new file mode 100644
index 000000000..512809be6
--- /dev/null
+++ b/src/com/android/camera/CameraDisabledException.java
@@ -0,0 +1,24 @@
+/*
+ * 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.camera;
+
+/**
+ * This class represents the condition that device policy manager has disabled
+ * the camera.
+ */
+public class CameraDisabledException extends Exception {
+}
diff --git a/src/com/android/camera/CameraErrorCallback.java b/src/com/android/camera/CameraErrorCallback.java
new file mode 100644
index 000000000..22f800ef9
--- /dev/null
+++ b/src/com/android/camera/CameraErrorCallback.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.camera;
+
+import android.util.Log;
+
+public class CameraErrorCallback
+ implements android.hardware.Camera.ErrorCallback {
+ private static final String TAG = "CameraErrorCallback";
+
+ @Override
+ public void onError(int error, android.hardware.Camera camera) {
+ Log.e(TAG, "Got camera error callback. error=" + error);
+ if (error == android.hardware.Camera.CAMERA_ERROR_SERVER_DIED) {
+ // We are not sure about the current state of the app (in preview or
+ // snapshot or recording). Closing the app is better than creating a
+ // new Camera object.
+ throw new RuntimeException("Media server died.");
+ }
+ }
+}
diff --git a/src/com/android/camera/CameraHardwareException.java b/src/com/android/camera/CameraHardwareException.java
new file mode 100644
index 000000000..82090554d
--- /dev/null
+++ b/src/com/android/camera/CameraHardwareException.java
@@ -0,0 +1,28 @@
+/*
+ * 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.camera;
+
+/**
+ * This class represents the condition that we cannot open the camera hardware
+ * successfully. For example, another process is using the camera.
+ */
+public class CameraHardwareException extends Exception {
+
+ public CameraHardwareException(Throwable t) {
+ super(t);
+ }
+}
diff --git a/src/com/android/camera/CameraHolder.java b/src/com/android/camera/CameraHolder.java
new file mode 100644
index 000000000..d913df709
--- /dev/null
+++ b/src/com/android/camera/CameraHolder.java
@@ -0,0 +1,299 @@
+/*
+ * 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.camera;
+
+import static com.android.camera.Util.Assert;
+
+import android.hardware.Camera.CameraInfo;
+import android.hardware.Camera.Parameters;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.camera.CameraManager.CameraProxy;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+
+/**
+ * The class is used to hold an {@code android.hardware.Camera} instance.
+ *
+ * <p>The {@code open()} and {@code release()} calls are similar to the ones
+ * in {@code android.hardware.Camera}. The difference is if {@code keep()} is
+ * called before {@code release()}, CameraHolder will try to hold the {@code
+ * android.hardware.Camera} instance for a while, so if {@code open()} is
+ * called soon after, we can avoid the cost of {@code open()} in {@code
+ * android.hardware.Camera}.
+ *
+ * <p>This is used in switching between different modules.
+ */
+public class CameraHolder {
+ private static final String TAG = "CameraHolder";
+ private static final int KEEP_CAMERA_TIMEOUT = 3000; // 3 seconds
+ private CameraProxy mCameraDevice;
+ private long mKeepBeforeTime; // Keep the Camera before this time.
+ private final Handler mHandler;
+ private boolean mCameraOpened; // true if camera is opened
+ private final int mNumberOfCameras;
+ private int mCameraId = -1; // current camera id
+ private int mBackCameraId = -1;
+ private int mFrontCameraId = -1;
+ private final CameraInfo[] mInfo;
+ private static CameraProxy mMockCamera[];
+ private static CameraInfo mMockCameraInfo[];
+
+ /* Debug double-open issue */
+ private static final boolean DEBUG_OPEN_RELEASE = true;
+ private static class OpenReleaseState {
+ long time;
+ int id;
+ String device;
+ String[] stack;
+ }
+ private static ArrayList<OpenReleaseState> sOpenReleaseStates =
+ new ArrayList<OpenReleaseState>();
+ private static SimpleDateFormat sDateFormat = new SimpleDateFormat(
+ "yyyy-MM-dd HH:mm:ss.SSS");
+
+ private static synchronized void collectState(int id, CameraProxy device) {
+ OpenReleaseState s = new OpenReleaseState();
+ s.time = System.currentTimeMillis();
+ s.id = id;
+ if (device == null) {
+ s.device = "(null)";
+ } else {
+ s.device = device.toString();
+ }
+
+ StackTraceElement[] stack = Thread.currentThread().getStackTrace();
+ String[] lines = new String[stack.length];
+ for (int i = 0; i < stack.length; i++) {
+ lines[i] = stack[i].toString();
+ }
+ s.stack = lines;
+
+ if (sOpenReleaseStates.size() > 10) {
+ sOpenReleaseStates.remove(0);
+ }
+ sOpenReleaseStates.add(s);
+ }
+
+ private static synchronized void dumpStates() {
+ for (int i = sOpenReleaseStates.size() - 1; i >= 0; i--) {
+ OpenReleaseState s = sOpenReleaseStates.get(i);
+ String date = sDateFormat.format(new Date(s.time));
+ Log.d(TAG, "State " + i + " at " + date);
+ Log.d(TAG, "mCameraId = " + s.id + ", mCameraDevice = " + s.device);
+ Log.d(TAG, "Stack:");
+ for (int j = 0; j < s.stack.length; j++) {
+ Log.d(TAG, " " + s.stack[j]);
+ }
+ }
+ }
+
+ // We store the camera parameters when we actually open the device,
+ // so we can restore them in the subsequent open() requests by the user.
+ // This prevents the parameters set by PhotoModule used by VideoModule
+ // inadvertently.
+ private Parameters mParameters;
+
+ // Use a singleton.
+ private static CameraHolder sHolder;
+ public static synchronized CameraHolder instance() {
+ if (sHolder == null) {
+ sHolder = new CameraHolder();
+ }
+ return sHolder;
+ }
+
+ private static final int RELEASE_CAMERA = 1;
+ private class MyHandler extends Handler {
+ MyHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case RELEASE_CAMERA:
+ synchronized (CameraHolder.this) {
+ // In 'CameraHolder.open', the 'RELEASE_CAMERA' message
+ // will be removed if it is found in the queue. However,
+ // there is a chance that this message has been handled
+ // before being removed. So, we need to add a check
+ // here:
+ if (!mCameraOpened) release();
+ }
+ break;
+ }
+ }
+ }
+
+ public static void injectMockCamera(CameraInfo[] info, CameraProxy[] camera) {
+ mMockCameraInfo = info;
+ mMockCamera = camera;
+ sHolder = new CameraHolder();
+ }
+
+ private CameraHolder() {
+ HandlerThread ht = new HandlerThread("CameraHolder");
+ ht.start();
+ mHandler = new MyHandler(ht.getLooper());
+ if (mMockCameraInfo != null) {
+ mNumberOfCameras = mMockCameraInfo.length;
+ mInfo = mMockCameraInfo;
+ } else {
+ mNumberOfCameras = android.hardware.Camera.getNumberOfCameras();
+ mInfo = new CameraInfo[mNumberOfCameras];
+ for (int i = 0; i < mNumberOfCameras; i++) {
+ mInfo[i] = new CameraInfo();
+ android.hardware.Camera.getCameraInfo(i, mInfo[i]);
+ }
+ }
+
+ // get the first (smallest) back and first front camera id
+ for (int i = 0; i < mNumberOfCameras; i++) {
+ if (mBackCameraId == -1 && mInfo[i].facing == CameraInfo.CAMERA_FACING_BACK) {
+ mBackCameraId = i;
+ } else if (mFrontCameraId == -1 && mInfo[i].facing == CameraInfo.CAMERA_FACING_FRONT) {
+ mFrontCameraId = i;
+ }
+ }
+ }
+
+ public int getNumberOfCameras() {
+ return mNumberOfCameras;
+ }
+
+ public CameraInfo[] getCameraInfo() {
+ return mInfo;
+ }
+
+ public synchronized CameraProxy open(int cameraId)
+ throws CameraHardwareException {
+ if (DEBUG_OPEN_RELEASE) {
+ collectState(cameraId, mCameraDevice);
+ if (mCameraOpened) {
+ Log.e(TAG, "double open");
+ dumpStates();
+ }
+ }
+ Assert(!mCameraOpened);
+ if (mCameraDevice != null && mCameraId != cameraId) {
+ mCameraDevice.release();
+ mCameraDevice = null;
+ mCameraId = -1;
+ }
+ if (mCameraDevice == null) {
+ try {
+ Log.v(TAG, "open camera " + cameraId);
+ if (mMockCameraInfo == null) {
+ mCameraDevice = CameraManagerFactory
+ .getAndroidCameraManager().cameraOpen(cameraId);
+ } else {
+ if (mMockCamera == null)
+ throw new RuntimeException();
+ mCameraDevice = mMockCamera[cameraId];
+ }
+ mCameraId = cameraId;
+ } catch (RuntimeException e) {
+ Log.e(TAG, "fail to connect Camera", e);
+ throw new CameraHardwareException(e);
+ }
+ mParameters = mCameraDevice.getParameters();
+ } else {
+ try {
+ mCameraDevice.reconnect();
+ } catch (IOException e) {
+ Log.e(TAG, "reconnect failed.");
+ throw new CameraHardwareException(e);
+ }
+ mCameraDevice.setParameters(mParameters);
+ }
+ mCameraOpened = true;
+ mHandler.removeMessages(RELEASE_CAMERA);
+ mKeepBeforeTime = 0;
+ return mCameraDevice;
+ }
+
+ /**
+ * Tries to open the hardware camera. If the camera is being used or
+ * unavailable then return {@code null}.
+ */
+ public synchronized CameraProxy tryOpen(int cameraId) {
+ try {
+ return !mCameraOpened ? open(cameraId) : null;
+ } catch (CameraHardwareException e) {
+ // In eng build, we throw the exception so that test tool
+ // can detect it and report it
+ if ("eng".equals(Build.TYPE)) {
+ throw new RuntimeException(e);
+ }
+ return null;
+ }
+ }
+
+ public synchronized void release() {
+ if (DEBUG_OPEN_RELEASE) {
+ collectState(mCameraId, mCameraDevice);
+ }
+
+ if (mCameraDevice == null) return;
+
+ long now = System.currentTimeMillis();
+ if (now < mKeepBeforeTime) {
+ if (mCameraOpened) {
+ mCameraOpened = false;
+ mCameraDevice.stopPreview();
+ }
+ mHandler.sendEmptyMessageDelayed(RELEASE_CAMERA,
+ mKeepBeforeTime - now);
+ return;
+ }
+ mCameraOpened = false;
+ mCameraDevice.release();
+ mCameraDevice = null;
+ // We must set this to null because it has a reference to Camera.
+ // Camera has references to the listeners.
+ mParameters = null;
+ mCameraId = -1;
+ }
+
+ public void keep() {
+ keep(KEEP_CAMERA_TIMEOUT);
+ }
+
+ public synchronized void keep(int time) {
+ // We allow mCameraOpened in either state for the convenience of the
+ // calling activity. The activity may not have a chance to call open()
+ // before the user switches to another activity.
+ mKeepBeforeTime = System.currentTimeMillis() + time;
+ }
+
+ public int getBackCameraId() {
+ return mBackCameraId;
+ }
+
+ public int getFrontCameraId() {
+ return mFrontCameraId;
+ }
+}
diff --git a/src/com/android/camera/CameraManager.java b/src/com/android/camera/CameraManager.java
new file mode 100644
index 000000000..90a838ca6
--- /dev/null
+++ b/src/com/android/camera/CameraManager.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.annotation.TargetApi;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.hardware.Camera.ErrorCallback;
+import android.hardware.Camera.OnZoomChangeListener;
+import android.hardware.Camera.Parameters;
+import android.os.Handler;
+import android.view.SurfaceHolder;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.io.IOException;
+
+/**
+ * An interface which provides possible camera device operations.
+ *
+ * The client should call {@code CameraManager.cameraOpen} to get an instance
+ * of {@link CameraManager.CameraProxy} to control the camera. Classes
+ * implementing this interface should have its own one unique {@code Thread}
+ * other than the main thread for camera operations. Camera device callbacks
+ * are wrapped since the client should not deal with
+ * {@code android.hardware.Camera} directly.
+ *
+ * TODO: provide callback interfaces for:
+ * {@code android.hardware.Camera.ErrorCallback},
+ * {@code android.hardware.Camera.OnZoomChangeListener}, and
+ * {@code android.hardware.Camera.Parameters}.
+ */
+public interface CameraManager {
+
+ /**
+ * An interface which wraps
+ * {@link android.hardware.Camera.AutoFocusCallback}.
+ */
+ public interface CameraAFCallback {
+ public void onAutoFocus(boolean focused, CameraProxy camera);
+ }
+
+ /**
+ * An interface which wraps
+ * {@link android.hardware.Camera.AutoFocusMoveCallback}.
+ */
+ public interface CameraAFMoveCallback {
+ public void onAutoFocusMoving(boolean moving, CameraProxy camera);
+ }
+
+ /**
+ * An interface which wraps
+ * {@link android.hardware.Camera.ShutterCallback}.
+ */
+ public interface CameraShutterCallback {
+ public void onShutter(CameraProxy camera);
+ }
+
+ /**
+ * An interface which wraps
+ * {@link android.hardware.Camera.PictureCallback}.
+ */
+ public interface CameraPictureCallback {
+ public void onPictureTaken(byte[] data, CameraProxy camera);
+ }
+
+ /**
+ * An interface which wraps
+ * {@link android.hardware.Camera.PreviewCallback}.
+ */
+ public interface CameraPreviewDataCallback {
+ public void onPreviewFrame(byte[] data, CameraProxy camera);
+ }
+
+ /**
+ * An interface which wraps
+ * {@link android.hardware.Camera.FaceDetectionListener}.
+ */
+ public interface CameraFaceDetectionCallback {
+ /**
+ * Callback for face detection.
+ *
+ * @param faces Recognized face in the preview.
+ * @param camera The camera which the preview image comes from.
+ */
+ public void onFaceDetection(Camera.Face[] faces, CameraProxy camera);
+ }
+
+ /**
+ * Opens the camera of the specified ID synchronously.
+ *
+ * @param cameraId The camera ID to open.
+ * @return An instance of {@link CameraProxy} on success. null on failure.
+ */
+ public CameraProxy cameraOpen(int cameraId);
+
+ /**
+ * An interface that takes camera operation requests and post messages to the
+ * camera handler thread. All camera operations made through this interface is
+ * asynchronous by default except those mentioned specifically.
+ */
+ public interface CameraProxy {
+
+ /**
+ * Returns the underlying {@link android.hardware.Camera} object used
+ * by this proxy. This method should only be used when handing the
+ * camera device over to {@link android.media.MediaRecorder} for
+ * recording.
+ */
+ public android.hardware.Camera getCamera();
+
+ /**
+ * Releases the camera device synchronously.
+ * This function must be synchronous so the caller knows exactly when the camera
+ * is released and can continue on.
+ */
+ public void release();
+
+ /**
+ * Reconnects to the camera device.
+ *
+ * @see android.hardware.Camera#reconnect()
+ */
+ public void reconnect() throws IOException;
+
+ /**
+ * Unlocks the camera device.
+ *
+ * @see android.hardware.Camera#unlock()
+ */
+ public void unlock();
+
+ /**
+ * Locks the camera device.
+ * @see android.hardware.Camera#lock()
+ */
+ public void lock();
+
+ /**
+ * Sets the {@link android.graphics.SurfaceTexture} for preview.
+ *
+ * @param surfaceTexture The {@link SurfaceTexture} for preview.
+ */
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ public void setPreviewTexture(final SurfaceTexture surfaceTexture);
+
+ /**
+ * Sets the {@link android.view.SurfaceHolder} for preview.
+ *
+ * @param surfaceHolder The {@link SurfaceHolder} for preview.
+ */
+ public void setPreviewDisplay(final SurfaceHolder surfaceHolder);
+
+ /**
+ * Starts the camera preview.
+ */
+ public void startPreview();
+
+ /**
+ * Stops the camera preview synchronously.
+ * {@code stopPreview()} must be synchronous to ensure that the caller can
+ * continues to release resources related to camera preview.
+ */
+ public void stopPreview();
+
+ /**
+ * Sets the callback for preview data.
+ *
+ * @param handler handler in which the callback was handled.
+ * @param cb The callback to be invoked when the preview data is available.
+ * @see android.hardware.Camera#setPreviewCallback(android.hardware.Camera.PreviewCallback)
+ */
+ public void setPreviewDataCallback(Handler handler, CameraPreviewDataCallback cb);
+
+ /**
+ * Sets the callback for preview data.
+ *
+ * @param handler The handler in which the callback will be invoked.
+ * @param cb The callback to be invoked when the preview data is available.
+ * @see android.hardware.Camera#setPreviewCallbackWithBuffer(android.hardware.Camera.PreviewCallback)
+ */
+ public void setPreviewDataCallbackWithBuffer(Handler handler, CameraPreviewDataCallback cb);
+
+ /**
+ * Adds buffer for the preview callback.
+ *
+ * @param callbackBuffer The buffer allocated for the preview data.
+ */
+ public void addCallbackBuffer(byte[] callbackBuffer);
+
+ /**
+ * Starts the auto-focus process. The result will be returned through the callback.
+ *
+ * @param handler The handler in which the callback will be invoked.
+ * @param cb The auto-focus callback.
+ */
+ public void autoFocus(Handler handler, CameraAFCallback cb);
+
+ /**
+ * Cancels the auto-focus process.
+ */
+ public void cancelAutoFocus();
+
+ /**
+ * Sets the auto-focus callback
+ *
+ * @param handler The handler in which the callback will be invoked.
+ * @param cb The callback to be invoked when the preview data is available.
+ */
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ public void setAutoFocusMoveCallback(Handler handler, CameraAFMoveCallback cb);
+
+ /**
+ * Instrument the camera to take a picture.
+ *
+ * @param handler The handler in which the callback will be invoked.
+ * @param shutter The callback for shutter action, may be null.
+ * @param raw The callback for uncompressed data, may be null.
+ * @param postview The callback for postview image data, may be null.
+ * @param jpeg The callback for jpeg image data, may be null.
+ * @see android.hardware.Camera#takePicture(
+ * android.hardware.Camera.ShutterCallback,
+ * android.hardware.Camera.PictureCallback,
+ * android.hardware.Camera.PictureCallback)
+ */
+ public void takePicture(
+ Handler handler,
+ CameraShutterCallback shutter,
+ CameraPictureCallback raw,
+ CameraPictureCallback postview,
+ CameraPictureCallback jpeg);
+
+ /**
+ * Sets the display orientation for camera to adjust the preview orientation.
+ *
+ * @param degrees The rotation in degrees. Should be 0, 90, 180 or 270.
+ */
+ public void setDisplayOrientation(int degrees);
+
+ /**
+ * Sets the listener for zoom change.
+ *
+ * @param listener The listener.
+ */
+ public void setZoomChangeListener(OnZoomChangeListener listener);
+
+ /**
+ * Sets the face detection listener.
+ *
+ * @param handler The handler in which the callback will be invoked.
+ * @param callback The callback for face detection results.
+ */
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ public void setFaceDetectionCallback(Handler handler, CameraFaceDetectionCallback callback);
+
+ /**
+ * Starts the face detection.
+ */
+ public void startFaceDetection();
+
+ /**
+ * Stops the face detection.
+ */
+ public void stopFaceDetection();
+
+ /**
+ * Registers an error callback.
+ *
+ * @param cb The error callback.
+ * @see android.hardware.Camera#setErrorCallback(android.hardware.Camera.ErrorCallback)
+ */
+ public void setErrorCallback(ErrorCallback cb);
+
+ /**
+ * Sets the camera parameters.
+ *
+ * @param params The camera parameters to use.
+ */
+ public void setParameters(Parameters params);
+
+ /**
+ * Gets the current camera parameters synchronously. This method is
+ * synchronous since the caller has to wait for the camera to return
+ * the parameters. If the parameters are already cached, it returns
+ * immediately.
+ */
+ public Parameters getParameters();
+
+ /**
+ * Forces {@code CameraProxy} to update the cached version of the camera
+ * parameters regardless of the dirty bit.
+ */
+ public void refreshParameters();
+
+ /**
+ * Enables/Disables the camera shutter sound.
+ *
+ * @param enable {@code true} to enable the shutter sound,
+ * {@code false} to disable it.
+ */
+ public void enableShutterSound(boolean enable);
+ }
+}
diff --git a/src/com/android/camera/CameraManagerFactory.java b/src/com/android/camera/CameraManagerFactory.java
new file mode 100644
index 000000000..914ebb265
--- /dev/null
+++ b/src/com/android/camera/CameraManagerFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2013 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.camera;
+
+/**
+ * A factory class for {@link CameraManager}.
+ */
+public class CameraManagerFactory {
+
+ private static AndroidCameraManagerImpl sAndroidCameraManager;
+
+ /**
+ * Returns the android camera implementation of {@link CameraManager}.
+ *
+ * @return The {@link CameraManager} to control the camera device.
+ */
+ public static synchronized CameraManager getAndroidCameraManager() {
+ if (sAndroidCameraManager == null) {
+ sAndroidCameraManager = new AndroidCameraManagerImpl();
+ }
+ return sAndroidCameraManager;
+ }
+}
diff --git a/src/com/android/camera/CameraModule.java b/src/com/android/camera/CameraModule.java
new file mode 100644
index 000000000..bcfe98d65
--- /dev/null
+++ b/src/com/android/camera/CameraModule.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+public interface CameraModule {
+
+ public void init(CameraActivity activity, View frame);
+
+ public void onSwitchMode(boolean toCamera);
+
+ public void onPauseBeforeSuper();
+
+ public void onPauseAfterSuper();
+
+ public void onResumeBeforeSuper();
+
+ public void onResumeAfterSuper();
+
+ public void onConfigurationChanged(Configuration config);
+
+ public void onStop();
+
+ public void installIntentFilter();
+
+ public void onActivityResult(int requestCode, int resultCode, Intent data);
+
+ public boolean onBackPressed();
+
+ public boolean onKeyDown(int keyCode, KeyEvent event);
+
+ public boolean onKeyUp(int keyCode, KeyEvent event);
+
+ public void onSingleTapUp(View view, int x, int y);
+
+ public void onPreviewTextureCopied();
+
+ public void onCaptureTextureCopied();
+
+ public void onUserInteraction();
+
+ public boolean updateStorageHintOnResume();
+
+ public void updateCameraAppView();
+
+ public void onOrientationChanged(int orientation);
+
+ public void onShowSwitcherPopup();
+
+ public void onMediaSaveServiceConnected(MediaSaveService s);
+}
diff --git a/src/com/android/camera/CameraPreference.java b/src/com/android/camera/CameraPreference.java
new file mode 100644
index 000000000..5ddd86dbc
--- /dev/null
+++ b/src/com/android/camera/CameraPreference.java
@@ -0,0 +1,63 @@
+/*
+ * 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.camera;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.R;
+
+/**
+ * The base class of all Preferences used in Camera. The preferences can be
+ * loaded from XML resource by <code>PreferenceInflater</code>.
+ */
+public abstract class CameraPreference {
+
+ private final String mTitle;
+ private SharedPreferences mSharedPreferences;
+ private final Context mContext;
+
+ static public interface OnPreferenceChangedListener {
+ public void onSharedPreferenceChanged();
+ public void onRestorePreferencesClicked();
+ public void onOverriddenPreferencesClicked();
+ public void onCameraPickerClicked(int cameraId);
+ }
+
+ public CameraPreference(Context context, AttributeSet attrs) {
+ mContext = context;
+ TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.CameraPreference, 0, 0);
+ mTitle = a.getString(R.styleable.CameraPreference_title);
+ a.recycle();
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public SharedPreferences getSharedPreferences() {
+ if (mSharedPreferences == null) {
+ mSharedPreferences = ComboPreferences.get(mContext);
+ }
+ return mSharedPreferences;
+ }
+
+ public abstract void reloadValue();
+}
diff --git a/src/com/android/camera/CameraScreenNail.java b/src/com/android/camera/CameraScreenNail.java
new file mode 100644
index 000000000..993a7d336
--- /dev/null
+++ b/src/com/android/camera/CameraScreenNail.java
@@ -0,0 +1,524 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.opengl.Matrix;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.SurfaceTextureScreenNail;
+
+/*
+ * This is a ScreenNail which can display camera's preview.
+ */
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+public class CameraScreenNail extends SurfaceTextureScreenNail {
+ private static final String TAG = "CAM_ScreenNail";
+ private static final int ANIM_NONE = 0;
+ // Capture animation is about to start.
+ private static final int ANIM_CAPTURE_START = 1;
+ // Capture animation is running.
+ private static final int ANIM_CAPTURE_RUNNING = 2;
+ // Switch camera animation needs to copy texture.
+ private static final int ANIM_SWITCH_COPY_TEXTURE = 3;
+ // Switch camera animation shows the initial feedback by darkening the
+ // preview.
+ private static final int ANIM_SWITCH_DARK_PREVIEW = 4;
+ // Switch camera animation is waiting for the first frame.
+ private static final int ANIM_SWITCH_WAITING_FIRST_FRAME = 5;
+ // Switch camera animation is about to start.
+ private static final int ANIM_SWITCH_START = 6;
+ // Switch camera animation is running.
+ private static final int ANIM_SWITCH_RUNNING = 7;
+
+ private boolean mVisible;
+ // True if first onFrameAvailable has been called. If screen nail is drawn
+ // too early, it will be all white.
+ private boolean mFirstFrameArrived;
+ private Listener mListener;
+ private final float[] mTextureTransformMatrix = new float[16];
+
+ // Animation.
+ private CaptureAnimManager mCaptureAnimManager;
+ private SwitchAnimManager mSwitchAnimManager = new SwitchAnimManager();
+ private int mAnimState = ANIM_NONE;
+ private RawTexture mAnimTexture;
+ // Some methods are called by GL thread and some are called by main thread.
+ // This protects mAnimState, mVisible, and surface texture. This also makes
+ // sure some code are atomic. For example, requestRender and setting
+ // mAnimState.
+ private Object mLock = new Object();
+
+ private OnFrameDrawnListener mOneTimeFrameDrawnListener;
+ private int mRenderWidth;
+ private int mRenderHeight;
+ // This represents the scaled, uncropped size of the texture
+ // Needed for FaceView
+ private int mUncroppedRenderWidth;
+ private int mUncroppedRenderHeight;
+ private float mScaleX = 1f, mScaleY = 1f;
+ private boolean mFullScreen;
+ private boolean mEnableAspectRatioClamping = false;
+ private boolean mAcquireTexture = false;
+ private final DrawClient mDefaultDraw = new DrawClient() {
+ @Override
+ public void onDraw(GLCanvas canvas, int x, int y, int width, int height) {
+ CameraScreenNail.super.draw(canvas, x, y, width, height);
+ }
+
+ @Override
+ public boolean requiresSurfaceTexture() {
+ return true;
+ }
+
+ @Override
+ public RawTexture copyToTexture(GLCanvas c, RawTexture texture, int w, int h) {
+ // We shouldn't be here since requireSurfaceTexture() returns true.
+ return null;
+ }
+ };
+ private DrawClient mDraw = mDefaultDraw;
+ private float mAlpha = 1f;
+ private Runnable mOnFrameDrawnListener;
+
+ public interface Listener {
+ void requestRender();
+ // Preview has been copied to a texture.
+ void onPreviewTextureCopied();
+
+ void onCaptureTextureCopied();
+ }
+
+ public interface OnFrameDrawnListener {
+ void onFrameDrawn(CameraScreenNail c);
+ }
+
+ public interface DrawClient {
+ void onDraw(GLCanvas canvas, int x, int y, int width, int height);
+
+ boolean requiresSurfaceTexture();
+ // The client should implement this if requiresSurfaceTexture() is false;
+ RawTexture copyToTexture(GLCanvas c, RawTexture texture, int width, int height);
+ }
+
+ public CameraScreenNail(Listener listener, Context ctx) {
+ mListener = listener;
+ mCaptureAnimManager = new CaptureAnimManager(ctx);
+ }
+
+ public void setFullScreen(boolean full) {
+ synchronized (mLock) {
+ mFullScreen = full;
+ }
+ }
+
+ /**
+ * returns the uncropped, but scaled, width of the rendered texture
+ */
+ public int getUncroppedRenderWidth() {
+ return mUncroppedRenderWidth;
+ }
+
+ /**
+ * returns the uncropped, but scaled, width of the rendered texture
+ */
+ public int getUncroppedRenderHeight() {
+ return mUncroppedRenderHeight;
+ }
+
+ @Override
+ public int getWidth() {
+ return mEnableAspectRatioClamping ? mRenderWidth : getTextureWidth();
+ }
+
+ @Override
+ public int getHeight() {
+ return mEnableAspectRatioClamping ? mRenderHeight : getTextureHeight();
+ }
+
+ private int getTextureWidth() {
+ return super.getWidth();
+ }
+
+ private int getTextureHeight() {
+ return super.getHeight();
+ }
+
+ @Override
+ public void setSize(int w, int h) {
+ super.setSize(w, h);
+ mEnableAspectRatioClamping = false;
+ if (mRenderWidth == 0) {
+ mRenderWidth = w;
+ mRenderHeight = h;
+ }
+ updateRenderSize();
+ }
+
+ /**
+ * Tells the ScreenNail to override the default aspect ratio scaling
+ * and instead perform custom scaling to basically do a centerCrop instead
+ * of the default centerInside
+ *
+ * Note that calls to setSize will disable this
+ */
+ public void enableAspectRatioClamping() {
+ mEnableAspectRatioClamping = true;
+ updateRenderSize();
+ }
+
+ private void setPreviewLayoutSize(int w, int h) {
+ Log.i(TAG, "preview layout size: "+w+"/"+h);
+ mRenderWidth = w;
+ mRenderHeight = h;
+ updateRenderSize();
+ }
+
+ private void updateRenderSize() {
+ if (!mEnableAspectRatioClamping) {
+ mScaleX = mScaleY = 1f;
+ mUncroppedRenderWidth = getTextureWidth();
+ mUncroppedRenderHeight = getTextureHeight();
+ Log.i(TAG, "aspect ratio clamping disabled");
+ return;
+ }
+
+ float aspectRatio;
+ if (getTextureWidth() > getTextureHeight()) {
+ aspectRatio = (float) getTextureWidth() / (float) getTextureHeight();
+ } else {
+ aspectRatio = (float) getTextureHeight() / (float) getTextureWidth();
+ }
+ float scaledTextureWidth, scaledTextureHeight;
+ if (mRenderWidth > mRenderHeight) {
+ scaledTextureWidth = Math.max(mRenderWidth,
+ (int) (mRenderHeight * aspectRatio));
+ scaledTextureHeight = Math.max(mRenderHeight,
+ (int)(mRenderWidth / aspectRatio));
+ } else {
+ scaledTextureWidth = Math.max(mRenderWidth,
+ (int) (mRenderHeight / aspectRatio));
+ scaledTextureHeight = Math.max(mRenderHeight,
+ (int) (mRenderWidth * aspectRatio));
+ }
+ mScaleX = mRenderWidth / scaledTextureWidth;
+ mScaleY = mRenderHeight / scaledTextureHeight;
+ mUncroppedRenderWidth = Math.round(scaledTextureWidth);
+ mUncroppedRenderHeight = Math.round(scaledTextureHeight);
+ Log.i(TAG, "aspect ratio clamping enabled, surfaceTexture scale: " + mScaleX + ", " + mScaleY);
+ }
+
+ public void acquireSurfaceTexture() {
+ synchronized (mLock) {
+ mFirstFrameArrived = false;
+ mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true);
+ mAcquireTexture = true;
+ }
+ mListener.requestRender();
+ }
+
+ @Override
+ public void releaseSurfaceTexture() {
+ synchronized (mLock) {
+ if (mAcquireTexture) {
+ mAcquireTexture = false;
+ mLock.notifyAll();
+ } else {
+ if (super.getSurfaceTexture() != null) {
+ super.releaseSurfaceTexture();
+ }
+ mAnimState = ANIM_NONE; // stop the animation
+ }
+ }
+ }
+
+ public void copyTexture() {
+ synchronized (mLock) {
+ mListener.requestRender();
+ mAnimState = ANIM_SWITCH_COPY_TEXTURE;
+ }
+ }
+
+ public void animateSwitchCamera() {
+ Log.v(TAG, "animateSwitchCamera");
+ synchronized (mLock) {
+ if (mAnimState == ANIM_SWITCH_DARK_PREVIEW) {
+ // Do not request render here because camera has been just
+ // started. We do not want to draw black frames.
+ mAnimState = ANIM_SWITCH_WAITING_FIRST_FRAME;
+ }
+ }
+ }
+
+ public void animateCapture(int displayRotation) {
+ synchronized (mLock) {
+ mCaptureAnimManager.setOrientation(displayRotation);
+ mCaptureAnimManager.animateFlashAndSlide();
+ mListener.requestRender();
+ mAnimState = ANIM_CAPTURE_START;
+ }
+ }
+
+ public RawTexture getAnimationTexture() {
+ return mAnimTexture;
+ }
+
+ public void animateFlash(int displayRotation) {
+ synchronized (mLock) {
+ mCaptureAnimManager.setOrientation(displayRotation);
+ mCaptureAnimManager.animateFlash();
+ mListener.requestRender();
+ mAnimState = ANIM_CAPTURE_START;
+ }
+ }
+
+ public void animateSlide() {
+ synchronized (mLock) {
+ mCaptureAnimManager.animateSlide();
+ mListener.requestRender();
+ }
+ }
+
+ private void callbackIfNeeded() {
+ if (mOneTimeFrameDrawnListener != null) {
+ mOneTimeFrameDrawnListener.onFrameDrawn(this);
+ mOneTimeFrameDrawnListener = null;
+ }
+ }
+
+ @Override
+ protected void updateTransformMatrix(float[] matrix) {
+ super.updateTransformMatrix(matrix);
+ Matrix.translateM(matrix, 0, .5f, .5f, 0);
+ Matrix.scaleM(matrix, 0, mScaleX, mScaleY, 1f);
+ Matrix.translateM(matrix, 0, -.5f, -.5f, 0);
+ }
+
+ public void directDraw(GLCanvas canvas, int x, int y, int width, int height) {
+ DrawClient draw;
+ synchronized (mLock) {
+ draw = mDraw;
+ }
+ draw.onDraw(canvas, x, y, width, height);
+ }
+
+ public void setDraw(DrawClient draw) {
+ synchronized (mLock) {
+ if (draw == null) {
+ mDraw = mDefaultDraw;
+ } else {
+ mDraw = draw;
+ }
+ }
+ mListener.requestRender();
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+ synchronized (mLock) {
+ allocateTextureIfRequested(canvas);
+ if (!mVisible) mVisible = true;
+ SurfaceTexture surfaceTexture = getSurfaceTexture();
+ if (mDraw.requiresSurfaceTexture() && (surfaceTexture == null || !mFirstFrameArrived)) {
+ return;
+ }
+ if (mOnFrameDrawnListener != null) {
+ mOnFrameDrawnListener.run();
+ mOnFrameDrawnListener = null;
+ }
+ float oldAlpha = canvas.getAlpha();
+ canvas.setAlpha(mAlpha);
+
+ switch (mAnimState) {
+ case ANIM_NONE:
+ directDraw(canvas, x, y, width, height);
+ break;
+ case ANIM_SWITCH_COPY_TEXTURE:
+ copyPreviewTexture(canvas);
+ mSwitchAnimManager.setReviewDrawingSize(width, height);
+ mListener.onPreviewTextureCopied();
+ mAnimState = ANIM_SWITCH_DARK_PREVIEW;
+ // The texture is ready. Fall through to draw darkened
+ // preview.
+ case ANIM_SWITCH_DARK_PREVIEW:
+ case ANIM_SWITCH_WAITING_FIRST_FRAME:
+ // Consume the frame. If the buffers are full,
+ // onFrameAvailable will not be called. Animation state
+ // relies on onFrameAvailable.
+ surfaceTexture.updateTexImage();
+ mSwitchAnimManager.drawDarkPreview(canvas, x, y, width,
+ height, mAnimTexture);
+ break;
+ case ANIM_SWITCH_START:
+ mSwitchAnimManager.startAnimation();
+ mAnimState = ANIM_SWITCH_RUNNING;
+ break;
+ case ANIM_CAPTURE_START:
+ copyPreviewTexture(canvas);
+ mListener.onCaptureTextureCopied();
+ mCaptureAnimManager.startAnimation();
+ mAnimState = ANIM_CAPTURE_RUNNING;
+ break;
+ }
+
+ if (mAnimState == ANIM_CAPTURE_RUNNING || mAnimState == ANIM_SWITCH_RUNNING) {
+ boolean drawn;
+ if (mAnimState == ANIM_CAPTURE_RUNNING) {
+ if (!mFullScreen) {
+ // Skip the animation if no longer in full screen mode
+ drawn = false;
+ } else {
+ drawn = mCaptureAnimManager.drawAnimation(canvas, this, mAnimTexture,
+ x, y, width, height);
+ }
+ } else {
+ drawn = mSwitchAnimManager.drawAnimation(canvas, x, y,
+ width, height, this, mAnimTexture);
+ }
+ if (drawn) {
+ mListener.requestRender();
+ } else {
+ // Continue to the normal draw procedure if the animation is
+ // not drawn.
+ mAnimState = ANIM_NONE;
+ directDraw(canvas, x, y, width, height);
+ }
+ }
+ canvas.setAlpha(oldAlpha);
+ callbackIfNeeded();
+ } // mLock
+ }
+
+ private void copyPreviewTexture(GLCanvas canvas) {
+ if (!mDraw.requiresSurfaceTexture()) {
+ mAnimTexture = mDraw.copyToTexture(
+ canvas, mAnimTexture, getTextureWidth(), getTextureHeight());
+ } else {
+ int width = mAnimTexture.getWidth();
+ int height = mAnimTexture.getHeight();
+ canvas.beginRenderTarget(mAnimTexture);
+ // Flip preview texture vertically. OpenGL uses bottom left point
+ // as the origin (0, 0).
+ canvas.translate(0, height);
+ canvas.scale(1, -1, 1);
+ getSurfaceTexture().getTransformMatrix(mTextureTransformMatrix);
+ updateTransformMatrix(mTextureTransformMatrix);
+ canvas.drawTexture(mExtTexture, mTextureTransformMatrix, 0, 0, width, height);
+ canvas.endRenderTarget();
+ }
+ }
+
+ @Override
+ public void noDraw() {
+ synchronized (mLock) {
+ mVisible = false;
+ }
+ }
+
+ @Override
+ public void recycle() {
+ synchronized (mLock) {
+ mVisible = false;
+ }
+ }
+
+ @Override
+ public void onFrameAvailable(SurfaceTexture surfaceTexture) {
+ synchronized (mLock) {
+ if (getSurfaceTexture() != surfaceTexture) {
+ return;
+ }
+ mFirstFrameArrived = true;
+ if (mVisible) {
+ if (mAnimState == ANIM_SWITCH_WAITING_FIRST_FRAME) {
+ mAnimState = ANIM_SWITCH_START;
+ }
+ // We need to ask for re-render if the SurfaceTexture receives a new
+ // frame.
+ mListener.requestRender();
+ }
+ }
+ }
+
+ // We need to keep track of the size of preview frame on the screen because
+ // it's needed when we do switch-camera animation. See comments in
+ // SwitchAnimManager.java. This is based on the natural orientation, not the
+ // view system orientation.
+ public void setPreviewFrameLayoutSize(int width, int height) {
+ synchronized (mLock) {
+ mSwitchAnimManager.setPreviewFrameLayoutSize(width, height);
+ setPreviewLayoutSize(width, height);
+ }
+ }
+
+ public void setOneTimeOnFrameDrawnListener(OnFrameDrawnListener l) {
+ synchronized (mLock) {
+ mFirstFrameArrived = false;
+ mOneTimeFrameDrawnListener = l;
+ }
+ }
+
+ @Override
+ public SurfaceTexture getSurfaceTexture() {
+ synchronized (mLock) {
+ SurfaceTexture surfaceTexture = super.getSurfaceTexture();
+ if (surfaceTexture == null && mAcquireTexture) {
+ try {
+ mLock.wait();
+ surfaceTexture = super.getSurfaceTexture();
+ } catch (InterruptedException e) {
+ Log.w(TAG, "unexpected interruption");
+ }
+ }
+ return surfaceTexture;
+ }
+ }
+
+ private void allocateTextureIfRequested(GLCanvas canvas) {
+ synchronized (mLock) {
+ if (mAcquireTexture) {
+ super.acquireSurfaceTexture(canvas);
+ mAcquireTexture = false;
+ mLock.notifyAll();
+ }
+ }
+ }
+
+ public void setOnFrameDrawnOneShot(Runnable run) {
+ synchronized (mLock) {
+ mOnFrameDrawnListener = run;
+ }
+ }
+
+ public float getAlpha() {
+ synchronized (mLock) {
+ return mAlpha;
+ }
+ }
+
+ public void setAlpha(float alpha) {
+ synchronized (mLock) {
+ mAlpha = alpha;
+ mListener.requestRender();
+ }
+ }
+}
diff --git a/src/com/android/camera/CameraSettings.java b/src/com/android/camera/CameraSettings.java
new file mode 100644
index 000000000..3558014cc
--- /dev/null
+++ b/src/com/android/camera/CameraSettings.java
@@ -0,0 +1,570 @@
+/*
+ * 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.camera;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.hardware.Camera.CameraInfo;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.media.CamcorderProfile;
+import android.util.FloatMath;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Provides utilities and keys for Camera settings.
+ */
+public class CameraSettings {
+ private static final int NOT_FOUND = -1;
+
+ public static final String KEY_VERSION = "pref_version_key";
+ public static final String KEY_LOCAL_VERSION = "pref_local_version_key";
+ public static final String KEY_RECORD_LOCATION = "pref_camera_recordlocation_key";
+ public static final String KEY_VIDEO_QUALITY = "pref_video_quality_key";
+ public static final String KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL = "pref_video_time_lapse_frame_interval_key";
+ public static final String KEY_PICTURE_SIZE = "pref_camera_picturesize_key";
+ public static final String KEY_JPEG_QUALITY = "pref_camera_jpegquality_key";
+ public static final String KEY_FOCUS_MODE = "pref_camera_focusmode_key";
+ public static final String KEY_FLASH_MODE = "pref_camera_flashmode_key";
+ public static final String KEY_VIDEOCAMERA_FLASH_MODE = "pref_camera_video_flashmode_key";
+ public static final String KEY_WHITE_BALANCE = "pref_camera_whitebalance_key";
+ public static final String KEY_SCENE_MODE = "pref_camera_scenemode_key";
+ public static final String KEY_EXPOSURE = "pref_camera_exposure_key";
+ public static final String KEY_TIMER = "pref_camera_timer_key";
+ public static final String KEY_TIMER_SOUND_EFFECTS = "pref_camera_timer_sound_key";
+ public static final String KEY_VIDEO_EFFECT = "pref_video_effect_key";
+ public static final String KEY_CAMERA_ID = "pref_camera_id_key";
+ public static final String KEY_CAMERA_HDR = "pref_camera_hdr_key";
+ public static final String KEY_CAMERA_FIRST_USE_HINT_SHOWN = "pref_camera_first_use_hint_shown_key";
+ public static final String KEY_VIDEO_FIRST_USE_HINT_SHOWN = "pref_video_first_use_hint_shown_key";
+ public static final String KEY_PHOTOSPHERE_PICTURESIZE = "pref_photosphere_picturesize_key";
+
+ public static final String EXPOSURE_DEFAULT_VALUE = "0";
+
+ public static final int CURRENT_VERSION = 5;
+ public static final int CURRENT_LOCAL_VERSION = 2;
+
+ private static final String TAG = "CameraSettings";
+
+ private final Context mContext;
+ private final Parameters mParameters;
+ private final CameraInfo[] mCameraInfo;
+ private final int mCameraId;
+
+ public CameraSettings(Activity activity, Parameters parameters,
+ int cameraId, CameraInfo[] cameraInfo) {
+ mContext = activity;
+ mParameters = parameters;
+ mCameraId = cameraId;
+ mCameraInfo = cameraInfo;
+ }
+
+ public PreferenceGroup getPreferenceGroup(int preferenceRes) {
+ PreferenceInflater inflater = new PreferenceInflater(mContext);
+ PreferenceGroup group =
+ (PreferenceGroup) inflater.inflate(preferenceRes);
+ if (mParameters != null) initPreference(group);
+ return group;
+ }
+
+ public static String getSupportedHighestVideoQuality(int cameraId,
+ String defaultQuality) {
+ // When launching the camera app first time, we will set the video quality
+ // to the first one (i.e. highest quality) in the supported list
+ List<String> supported = getSupportedVideoQuality(cameraId);
+ if (supported == null) {
+ Log.e(TAG, "No supported video quality is found");
+ return defaultQuality;
+ }
+ return supported.get(0);
+ }
+
+ public static void initialCameraPictureSize(
+ Context context, Parameters parameters) {
+ // When launching the camera app first time, we will set the picture
+ // size to the first one in the list defined in "arrays.xml" and is also
+ // supported by the driver.
+ List<Size> supported = parameters.getSupportedPictureSizes();
+ if (supported == null) return;
+ for (String candidate : context.getResources().getStringArray(
+ R.array.pref_camera_picturesize_entryvalues)) {
+ if (setCameraPictureSize(candidate, supported, parameters)) {
+ SharedPreferences.Editor editor = ComboPreferences
+ .get(context).edit();
+ editor.putString(KEY_PICTURE_SIZE, candidate);
+ editor.apply();
+ return;
+ }
+ }
+ Log.e(TAG, "No supported picture size found");
+ }
+
+ public static void removePreferenceFromScreen(
+ PreferenceGroup group, String key) {
+ removePreference(group, key);
+ }
+
+ public static boolean setCameraPictureSize(
+ String candidate, List<Size> supported, Parameters parameters) {
+ int index = candidate.indexOf('x');
+ if (index == NOT_FOUND) return false;
+ int width = Integer.parseInt(candidate.substring(0, index));
+ int height = Integer.parseInt(candidate.substring(index + 1));
+ for (Size size : supported) {
+ if (size.width == width && size.height == height) {
+ parameters.setPictureSize(width, height);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static int getMaxVideoDuration(Context context) {
+ int duration = 0; // in milliseconds, 0 means unlimited.
+ try {
+ duration = context.getResources().getInteger(R.integer.max_video_recording_length);
+ } catch (Resources.NotFoundException ex) {
+ }
+ return duration;
+ }
+
+ private void initPreference(PreferenceGroup group) {
+ ListPreference videoQuality = group.findPreference(KEY_VIDEO_QUALITY);
+ ListPreference timeLapseInterval = group.findPreference(KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL);
+ ListPreference pictureSize = group.findPreference(KEY_PICTURE_SIZE);
+ ListPreference whiteBalance = group.findPreference(KEY_WHITE_BALANCE);
+ ListPreference sceneMode = group.findPreference(KEY_SCENE_MODE);
+ ListPreference flashMode = group.findPreference(KEY_FLASH_MODE);
+ ListPreference focusMode = group.findPreference(KEY_FOCUS_MODE);
+ IconListPreference exposure =
+ (IconListPreference) group.findPreference(KEY_EXPOSURE);
+ CountDownTimerPreference timer =
+ (CountDownTimerPreference) group.findPreference(KEY_TIMER);
+ ListPreference countDownSoundEffects = group.findPreference(KEY_TIMER_SOUND_EFFECTS);
+ IconListPreference cameraIdPref =
+ (IconListPreference) group.findPreference(KEY_CAMERA_ID);
+ ListPreference videoFlashMode =
+ group.findPreference(KEY_VIDEOCAMERA_FLASH_MODE);
+ ListPreference videoEffect = group.findPreference(KEY_VIDEO_EFFECT);
+ ListPreference cameraHdr = group.findPreference(KEY_CAMERA_HDR);
+
+ // Since the screen could be loaded from different resources, we need
+ // to check if the preference is available here
+ if (videoQuality != null) {
+ filterUnsupportedOptions(group, videoQuality, getSupportedVideoQuality(mCameraId));
+ }
+
+ if (pictureSize != null) {
+ filterUnsupportedOptions(group, pictureSize, sizeListToStringList(
+ mParameters.getSupportedPictureSizes()));
+ filterSimilarPictureSize(group, pictureSize);
+ }
+ if (whiteBalance != null) {
+ filterUnsupportedOptions(group,
+ whiteBalance, mParameters.getSupportedWhiteBalance());
+ }
+ if (sceneMode != null) {
+ filterUnsupportedOptions(group,
+ sceneMode, mParameters.getSupportedSceneModes());
+ }
+ if (flashMode != null) {
+ filterUnsupportedOptions(group,
+ flashMode, mParameters.getSupportedFlashModes());
+ }
+ if (focusMode != null) {
+ if (!Util.isFocusAreaSupported(mParameters)) {
+ filterUnsupportedOptions(group,
+ focusMode, mParameters.getSupportedFocusModes());
+ } else {
+ // Remove the focus mode if we can use tap-to-focus.
+ removePreference(group, focusMode.getKey());
+ }
+ }
+ if (videoFlashMode != null) {
+ filterUnsupportedOptions(group,
+ videoFlashMode, mParameters.getSupportedFlashModes());
+ }
+ if (exposure != null) buildExposureCompensation(group, exposure);
+ if (cameraIdPref != null) buildCameraId(group, cameraIdPref);
+
+ if (timeLapseInterval != null) {
+ if (ApiHelper.HAS_TIME_LAPSE_RECORDING) {
+ resetIfInvalid(timeLapseInterval);
+ } else {
+ removePreference(group, timeLapseInterval.getKey());
+ }
+ }
+ if (videoEffect != null) {
+ if (ApiHelper.HAS_EFFECTS_RECORDING) {
+ initVideoEffect(group, videoEffect);
+ resetIfInvalid(videoEffect);
+ } else {
+ filterUnsupportedOptions(group, videoEffect, null);
+ }
+ }
+ if (cameraHdr != null && (!ApiHelper.HAS_CAMERA_HDR
+ || !Util.isCameraHdrSupported(mParameters))) {
+ removePreference(group, cameraHdr.getKey());
+ }
+ }
+
+ private void buildExposureCompensation(
+ PreferenceGroup group, IconListPreference exposure) {
+ int max = mParameters.getMaxExposureCompensation();
+ int min = mParameters.getMinExposureCompensation();
+ if (max == 0 && min == 0) {
+ removePreference(group, exposure.getKey());
+ return;
+ }
+ float step = mParameters.getExposureCompensationStep();
+
+ // show only integer values for exposure compensation
+ int maxValue = Math.min(3, (int) FloatMath.floor(max * step));
+ int minValue = Math.max(-3, (int) FloatMath.ceil(min * step));
+ String explabel = mContext.getResources().getString(R.string.pref_exposure_label);
+ CharSequence entries[] = new CharSequence[maxValue - minValue + 1];
+ CharSequence entryValues[] = new CharSequence[maxValue - minValue + 1];
+ CharSequence labels[] = new CharSequence[maxValue - minValue + 1];
+ int[] icons = new int[maxValue - minValue + 1];
+ TypedArray iconIds = mContext.getResources().obtainTypedArray(
+ R.array.pref_camera_exposure_icons);
+ for (int i = minValue; i <= maxValue; ++i) {
+ entryValues[i - minValue] = Integer.toString(Math.round(i / step));
+ StringBuilder builder = new StringBuilder();
+ if (i > 0) builder.append('+');
+ entries[i - minValue] = builder.append(i).toString();
+ labels[i - minValue] = explabel + " " + builder.toString();
+ icons[i - minValue] = iconIds.getResourceId(3 + i, 0);
+ }
+ exposure.setUseSingleIcon(true);
+ exposure.setEntries(entries);
+ exposure.setLabels(labels);
+ exposure.setEntryValues(entryValues);
+ exposure.setLargeIconIds(icons);
+ }
+
+ private void buildCameraId(
+ PreferenceGroup group, IconListPreference preference) {
+ int numOfCameras = mCameraInfo.length;
+ if (numOfCameras < 2) {
+ removePreference(group, preference.getKey());
+ return;
+ }
+
+ CharSequence[] entryValues = new CharSequence[numOfCameras];
+ for (int i = 0; i < numOfCameras; ++i) {
+ entryValues[i] = "" + i;
+ }
+ preference.setEntryValues(entryValues);
+ }
+
+ private static boolean removePreference(PreferenceGroup group, String key) {
+ for (int i = 0, n = group.size(); i < n; i++) {
+ CameraPreference child = group.get(i);
+ if (child instanceof PreferenceGroup) {
+ if (removePreference((PreferenceGroup) child, key)) {
+ return true;
+ }
+ }
+ if (child instanceof ListPreference &&
+ ((ListPreference) child).getKey().equals(key)) {
+ group.removePreference(i);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void filterUnsupportedOptions(PreferenceGroup group,
+ ListPreference pref, List<String> supported) {
+
+ // Remove the preference if the parameter is not supported or there is
+ // only one options for the settings.
+ if (supported == null || supported.size() <= 1) {
+ removePreference(group, pref.getKey());
+ return;
+ }
+
+ pref.filterUnsupported(supported);
+ if (pref.getEntries().length <= 1) {
+ removePreference(group, pref.getKey());
+ return;
+ }
+
+ resetIfInvalid(pref);
+ }
+
+ private void filterSimilarPictureSize(PreferenceGroup group,
+ ListPreference pref) {
+ pref.filterDuplicated();
+ if (pref.getEntries().length <= 1) {
+ removePreference(group, pref.getKey());
+ return;
+ }
+ resetIfInvalid(pref);
+ }
+
+ private void resetIfInvalid(ListPreference pref) {
+ // Set the value to the first entry if it is invalid.
+ String value = pref.getValue();
+ if (pref.findIndexOfValue(value) == NOT_FOUND) {
+ pref.setValueIndex(0);
+ }
+ }
+
+ private static List<String> sizeListToStringList(List<Size> sizes) {
+ ArrayList<String> list = new ArrayList<String>();
+ for (Size size : sizes) {
+ list.add(String.format(Locale.ENGLISH, "%dx%d", size.width, size.height));
+ }
+ return list;
+ }
+
+ public static void upgradeLocalPreferences(SharedPreferences pref) {
+ int version;
+ try {
+ version = pref.getInt(KEY_LOCAL_VERSION, 0);
+ } catch (Exception ex) {
+ version = 0;
+ }
+ if (version == CURRENT_LOCAL_VERSION) return;
+
+ SharedPreferences.Editor editor = pref.edit();
+ if (version == 1) {
+ // We use numbers to represent the quality now. The quality definition is identical to
+ // that of CamcorderProfile.java.
+ editor.remove("pref_video_quality_key");
+ }
+ editor.putInt(KEY_LOCAL_VERSION, CURRENT_LOCAL_VERSION);
+ editor.apply();
+ }
+
+ public static void upgradeGlobalPreferences(SharedPreferences pref) {
+ upgradeOldVersion(pref);
+ upgradeCameraId(pref);
+ }
+
+ private static void upgradeOldVersion(SharedPreferences pref) {
+ int version;
+ try {
+ version = pref.getInt(KEY_VERSION, 0);
+ } catch (Exception ex) {
+ version = 0;
+ }
+ if (version == CURRENT_VERSION) return;
+
+ SharedPreferences.Editor editor = pref.edit();
+ if (version == 0) {
+ // We won't use the preference which change in version 1.
+ // So, just upgrade to version 1 directly
+ version = 1;
+ }
+ if (version == 1) {
+ // Change jpeg quality {65,75,85} to {normal,fine,superfine}
+ String quality = pref.getString(KEY_JPEG_QUALITY, "85");
+ if (quality.equals("65")) {
+ quality = "normal";
+ } else if (quality.equals("75")) {
+ quality = "fine";
+ } else {
+ quality = "superfine";
+ }
+ editor.putString(KEY_JPEG_QUALITY, quality);
+ version = 2;
+ }
+ if (version == 2) {
+ editor.putString(KEY_RECORD_LOCATION,
+ pref.getBoolean(KEY_RECORD_LOCATION, false)
+ ? RecordLocationPreference.VALUE_ON
+ : RecordLocationPreference.VALUE_NONE);
+ version = 3;
+ }
+ if (version == 3) {
+ // Just use video quality to replace it and
+ // ignore the current settings.
+ editor.remove("pref_camera_videoquality_key");
+ editor.remove("pref_camera_video_duration_key");
+ }
+
+ editor.putInt(KEY_VERSION, CURRENT_VERSION);
+ editor.apply();
+ }
+
+ private static void upgradeCameraId(SharedPreferences pref) {
+ // The id stored in the preference may be out of range if we are running
+ // inside the emulator and a webcam is removed.
+ // Note: This method accesses the global preferences directly, not the
+ // combo preferences.
+ int cameraId = readPreferredCameraId(pref);
+ if (cameraId == 0) return; // fast path
+
+ int n = CameraHolder.instance().getNumberOfCameras();
+ if (cameraId < 0 || cameraId >= n) {
+ writePreferredCameraId(pref, 0);
+ }
+ }
+
+ public static int readPreferredCameraId(SharedPreferences pref) {
+ return Integer.parseInt(pref.getString(KEY_CAMERA_ID, "0"));
+ }
+
+ public static void writePreferredCameraId(SharedPreferences pref,
+ int cameraId) {
+ Editor editor = pref.edit();
+ editor.putString(KEY_CAMERA_ID, Integer.toString(cameraId));
+ editor.apply();
+ }
+
+ public static int readExposure(ComboPreferences preferences) {
+ String exposure = preferences.getString(
+ CameraSettings.KEY_EXPOSURE,
+ EXPOSURE_DEFAULT_VALUE);
+ try {
+ return Integer.parseInt(exposure);
+ } catch (Exception ex) {
+ Log.e(TAG, "Invalid exposure: " + exposure);
+ }
+ return 0;
+ }
+
+ public static int readEffectType(SharedPreferences pref) {
+ String effectSelection = pref.getString(KEY_VIDEO_EFFECT, "none");
+ if (effectSelection.equals("none")) {
+ return EffectsRecorder.EFFECT_NONE;
+ } else if (effectSelection.startsWith("goofy_face")) {
+ return EffectsRecorder.EFFECT_GOOFY_FACE;
+ } else if (effectSelection.startsWith("backdropper")) {
+ return EffectsRecorder.EFFECT_BACKDROPPER;
+ }
+ Log.e(TAG, "Invalid effect selection: " + effectSelection);
+ return EffectsRecorder.EFFECT_NONE;
+ }
+
+ public static Object readEffectParameter(SharedPreferences pref) {
+ String effectSelection = pref.getString(KEY_VIDEO_EFFECT, "none");
+ if (effectSelection.equals("none")) {
+ return null;
+ }
+ int separatorIndex = effectSelection.indexOf('/');
+ String effectParameter =
+ effectSelection.substring(separatorIndex + 1);
+ if (effectSelection.startsWith("goofy_face")) {
+ if (effectParameter.equals("squeeze")) {
+ return EffectsRecorder.EFFECT_GF_SQUEEZE;
+ } else if (effectParameter.equals("big_eyes")) {
+ return EffectsRecorder.EFFECT_GF_BIG_EYES;
+ } else if (effectParameter.equals("big_mouth")) {
+ return EffectsRecorder.EFFECT_GF_BIG_MOUTH;
+ } else if (effectParameter.equals("small_mouth")) {
+ return EffectsRecorder.EFFECT_GF_SMALL_MOUTH;
+ } else if (effectParameter.equals("big_nose")) {
+ return EffectsRecorder.EFFECT_GF_BIG_NOSE;
+ } else if (effectParameter.equals("small_eyes")) {
+ return EffectsRecorder.EFFECT_GF_SMALL_EYES;
+ }
+ } else if (effectSelection.startsWith("backdropper")) {
+ // Parameter is a string that either encodes the URI to use,
+ // or specifies 'gallery'.
+ return effectParameter;
+ }
+
+ Log.e(TAG, "Invalid effect selection: " + effectSelection);
+ return null;
+ }
+
+ public static void restorePreferences(Context context,
+ ComboPreferences preferences, Parameters parameters) {
+ int currentCameraId = readPreferredCameraId(preferences);
+
+ // Clear the preferences of both cameras.
+ int backCameraId = CameraHolder.instance().getBackCameraId();
+ if (backCameraId != -1) {
+ preferences.setLocalId(context, backCameraId);
+ Editor editor = preferences.edit();
+ editor.clear();
+ editor.apply();
+ }
+ int frontCameraId = CameraHolder.instance().getFrontCameraId();
+ if (frontCameraId != -1) {
+ preferences.setLocalId(context, frontCameraId);
+ Editor editor = preferences.edit();
+ editor.clear();
+ editor.apply();
+ }
+
+ // Switch back to the preferences of the current camera. Otherwise,
+ // we may write the preference to wrong camera later.
+ preferences.setLocalId(context, currentCameraId);
+
+ upgradeGlobalPreferences(preferences.getGlobal());
+ upgradeLocalPreferences(preferences.getLocal());
+
+ // Write back the current camera id because parameters are related to
+ // the camera. Otherwise, we may switch to the front camera but the
+ // initial picture size is that of the back camera.
+ initialCameraPictureSize(context, parameters);
+ writePreferredCameraId(preferences, currentCameraId);
+ }
+
+ private static ArrayList<String> getSupportedVideoQuality(int cameraId) {
+ ArrayList<String> supported = new ArrayList<String>();
+ // Check for supported quality
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) {
+ supported.add(Integer.toString(CamcorderProfile.QUALITY_1080P));
+ }
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) {
+ supported.add(Integer.toString(CamcorderProfile.QUALITY_720P));
+ }
+ if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) {
+ supported.add(Integer.toString(CamcorderProfile.QUALITY_480P));
+ }
+ return supported;
+ }
+
+ private void initVideoEffect(PreferenceGroup group, ListPreference videoEffect) {
+ CharSequence[] values = videoEffect.getEntryValues();
+
+ boolean goofyFaceSupported =
+ EffectsRecorder.isEffectSupported(EffectsRecorder.EFFECT_GOOFY_FACE);
+ boolean backdropperSupported =
+ EffectsRecorder.isEffectSupported(EffectsRecorder.EFFECT_BACKDROPPER) &&
+ Util.isAutoExposureLockSupported(mParameters) &&
+ Util.isAutoWhiteBalanceLockSupported(mParameters);
+
+ ArrayList<String> supported = new ArrayList<String>();
+ for (CharSequence value : values) {
+ String effectSelection = value.toString();
+ if (!goofyFaceSupported && effectSelection.startsWith("goofy_face")) continue;
+ if (!backdropperSupported && effectSelection.startsWith("backdropper")) continue;
+ supported.add(effectSelection);
+ }
+
+ filterUnsupportedOptions(group, videoEffect, supported);
+ }
+}
diff --git a/src/com/android/camera/CaptureAnimManager.java b/src/com/android/camera/CaptureAnimManager.java
new file mode 100644
index 000000000..6e8092566
--- /dev/null
+++ b/src/com/android/camera/CaptureAnimManager.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.os.SystemClock;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.NinePatchTexture;
+import com.android.gallery3d.glrenderer.RawTexture;
+
+/**
+ * Class to handle the capture animation.
+ */
+public class CaptureAnimManager {
+ @SuppressWarnings("unused")
+ private static final String TAG = "CAM_Capture";
+ // times mark endpoint of animation phase
+ private static final int TIME_FLASH = 200;
+ private static final int TIME_HOLD = 400;
+ private static final int TIME_SLIDE = 800;
+ private static final int TIME_HOLD2 = 3300;
+ private static final int TIME_SLIDE2 = 4100;
+
+ private static final int ANIM_BOTH = 0;
+ private static final int ANIM_FLASH = 1;
+ private static final int ANIM_SLIDE = 2;
+ private static final int ANIM_HOLD2 = 3;
+ private static final int ANIM_SLIDE2 = 4;
+
+ private final Interpolator mSlideInterpolator = new DecelerateInterpolator();
+
+ private volatile int mAnimOrientation; // Could be 0, 90, 180 or 270 degrees.
+ private long mAnimStartTime; // milliseconds.
+ private float mX; // The center of the whole view including preview and review.
+ private float mY;
+ private int mDrawWidth;
+ private int mDrawHeight;
+ private int mAnimType;
+
+ private int mHoldX;
+ private int mHoldY;
+ private int mHoldW;
+ private int mHoldH;
+
+ private int mOffset;
+
+ private int mMarginRight;
+ private int mMarginTop;
+ private int mSize;
+ private Resources mResources;
+ private NinePatchTexture mBorder;
+ private int mShadowSize;
+
+ public static int getAnimationDuration() {
+ return TIME_SLIDE2;
+ }
+
+ /* preview: camera preview view.
+ * review: view of picture just taken.
+ */
+ public CaptureAnimManager(Context ctx) {
+ mBorder = new NinePatchTexture(ctx, R.drawable.capture_thumbnail_shadow);
+ mResources = ctx.getResources();
+ }
+
+ public void setOrientation(int displayRotation) {
+ mAnimOrientation = (360 - displayRotation) % 360;
+ }
+
+ public void animateSlide() {
+ if (mAnimType != ANIM_FLASH) {
+ return;
+ }
+ mAnimType = ANIM_SLIDE;
+ mAnimStartTime = SystemClock.uptimeMillis();
+ }
+
+ public void animateFlash() {
+ mAnimType = ANIM_FLASH;
+ }
+
+ public void animateFlashAndSlide() {
+ mAnimType = ANIM_BOTH;
+ }
+
+ public void startAnimation() {
+ mAnimStartTime = SystemClock.uptimeMillis();
+ }
+
+ private void setAnimationGeometry(int x, int y, int w, int h) {
+ mMarginRight = mResources.getDimensionPixelSize(R.dimen.capture_margin_right);
+ mMarginTop = mResources.getDimensionPixelSize(R.dimen.capture_margin_top);
+ mSize = mResources.getDimensionPixelSize(R.dimen.capture_size);
+ mShadowSize = mResources.getDimensionPixelSize(R.dimen.capture_border);
+ mOffset = mMarginRight + mSize;
+ // Set the views to the initial positions.
+ mDrawWidth = w;
+ mDrawHeight = h;
+ mX = x;
+ mY = y;
+ mHoldW = mSize;
+ mHoldH = mSize;
+ switch (mAnimOrientation) {
+ case 0: // Preview is on the left.
+ mHoldX = x + w - mMarginRight - mSize;
+ mHoldY = y + mMarginTop;
+ break;
+ case 90: // Preview is below.
+ mHoldX = x + mMarginTop;
+ mHoldY = y + mMarginRight;
+ break;
+ case 180: // Preview on the right.
+ mHoldX = x + mMarginRight;
+ mHoldY = y + h - mMarginTop - mSize;
+ break;
+ case 270: // Preview is above.
+ mHoldX = x + w - mMarginTop - mSize;
+ mHoldY = y + h - mMarginRight - mSize;
+ break;
+ }
+ }
+
+ // Returns true if the animation has been drawn.
+ public boolean drawAnimation(GLCanvas canvas, CameraScreenNail preview,
+ RawTexture review, int lx, int ly, int lw, int lh) {
+ setAnimationGeometry(lx, ly, lw, lh);
+ long timeDiff = SystemClock.uptimeMillis() - mAnimStartTime;
+ // Check if the animation is over
+ if (mAnimType == ANIM_SLIDE && timeDiff > TIME_SLIDE2 - TIME_HOLD) return false;
+ if (mAnimType == ANIM_BOTH && timeDiff > TIME_SLIDE2) return false;
+
+ // determine phase and time in phase
+ int animStep = mAnimType;
+ if (mAnimType == ANIM_SLIDE) {
+ timeDiff += TIME_HOLD;
+ }
+ if (mAnimType == ANIM_SLIDE || mAnimType == ANIM_BOTH) {
+ if (timeDiff < TIME_HOLD) {
+ animStep = ANIM_FLASH;
+ } else if (timeDiff < TIME_SLIDE) {
+ animStep = ANIM_SLIDE;
+ timeDiff -= TIME_HOLD;
+ } else if (timeDiff < TIME_HOLD2) {
+ animStep = ANIM_HOLD2;
+ timeDiff -= TIME_SLIDE;
+ } else {
+ // SLIDE2
+ animStep = ANIM_SLIDE2;
+ timeDiff -= TIME_HOLD2;
+ }
+ }
+
+ if (animStep == ANIM_FLASH) {
+ review.draw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight);
+ if (timeDiff < TIME_FLASH) {
+ float f = 0.3f - 0.3f * timeDiff / TIME_FLASH;
+ int color = Color.argb((int) (255 * f), 255, 255, 255);
+ canvas.fillRect(mX, mY, mDrawWidth, mDrawHeight, color);
+ }
+ } else if (animStep == ANIM_SLIDE) {
+ float fraction = mSlideInterpolator.getInterpolation((float) (timeDiff) / (TIME_SLIDE - TIME_HOLD));
+ float x = mX;
+ float y = mY;
+ float w = 0;
+ float h = 0;
+ x = interpolate(mX, mHoldX, fraction);
+ y = interpolate(mY, mHoldY, fraction);
+ w = interpolate(mDrawWidth, mHoldW, fraction);
+ h = interpolate(mDrawHeight, mHoldH, fraction);
+ preview.directDraw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight);
+ review.draw(canvas, (int) x, (int) y, (int) w, (int) h);
+ } else if (animStep == ANIM_HOLD2) {
+ preview.directDraw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight);
+ review.draw(canvas, mHoldX, mHoldY, mHoldW, mHoldH);
+ mBorder.draw(canvas, (int) mHoldX - mShadowSize, (int) mHoldY - mShadowSize,
+ (int) mHoldW + 2 * mShadowSize, (int) mHoldH + 2 * mShadowSize);
+ } else if (animStep == ANIM_SLIDE2) {
+ float fraction = (float)(timeDiff) / (TIME_SLIDE2 - TIME_HOLD2);
+ float x = mHoldX;
+ float y = mHoldY;
+ float d = mOffset * fraction;
+ switch (mAnimOrientation) {
+ case 0:
+ x = mHoldX + d;
+ break;
+ case 180:
+ x = mHoldX - d;
+ break;
+ case 90:
+ y = mHoldY - d;
+ break;
+ case 270:
+ y = mHoldY + d;
+ break;
+ }
+ preview.directDraw(canvas, (int) mX, (int) mY, mDrawWidth, mDrawHeight);
+ mBorder.draw(canvas, (int) x - mShadowSize, (int) y - mShadowSize,
+ (int) mHoldW + 2 * mShadowSize, (int) mHoldH + 2 * mShadowSize);
+ review.draw(canvas, (int) x, (int) y, mHoldW, mHoldH);
+ }
+ return true;
+ }
+
+ private static float interpolate(float start, float end, float fraction) {
+ return start + (end - start) * fraction;
+ }
+
+}
diff --git a/src/com/android/camera/ComboPreferences.java b/src/com/android/camera/ComboPreferences.java
new file mode 100644
index 000000000..e17e47aa8
--- /dev/null
+++ b/src/com/android/camera/ComboPreferences.java
@@ -0,0 +1,335 @@
+/*
+ * 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.camera;
+
+import android.app.backup.BackupManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.preference.PreferenceManager;
+
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class ComboPreferences implements
+ SharedPreferences,
+ OnSharedPreferenceChangeListener {
+ private SharedPreferences mPrefGlobal; // global preferences
+ private SharedPreferences mPrefLocal; // per-camera preferences
+ private String mPackageName;
+ private CopyOnWriteArrayList<OnSharedPreferenceChangeListener> mListeners;
+ // TODO: Remove this WeakHashMap in the camera code refactoring
+ private static WeakHashMap<Context, ComboPreferences> sMap =
+ new WeakHashMap<Context, ComboPreferences>();
+
+ public ComboPreferences(Context context) {
+ mPackageName = context.getPackageName();
+ mPrefGlobal = context.getSharedPreferences(
+ getGlobalSharedPreferencesName(context), Context.MODE_PRIVATE);
+ mPrefGlobal.registerOnSharedPreferenceChangeListener(this);
+
+ synchronized (sMap) {
+ sMap.put(context, this);
+ }
+ mListeners = new CopyOnWriteArrayList<OnSharedPreferenceChangeListener>();
+
+ // The global preferences was previously stored in the default
+ // shared preferences file. They should be stored in the camera-specific
+ // shared preferences file so we can backup them solely.
+ SharedPreferences oldprefs =
+ PreferenceManager.getDefaultSharedPreferences(context);
+ if (!mPrefGlobal.contains(CameraSettings.KEY_VERSION)
+ && oldprefs.contains(CameraSettings.KEY_VERSION)) {
+ moveGlobalPrefsFrom(oldprefs);
+ }
+ }
+
+ public static ComboPreferences get(Context context) {
+ synchronized (sMap) {
+ return sMap.get(context);
+ }
+ }
+
+ private static String getLocalSharedPreferencesName(
+ Context context, int cameraId) {
+ return context.getPackageName() + "_preferences_" + cameraId;
+ }
+
+ private static String getGlobalSharedPreferencesName(Context context) {
+ return context.getPackageName() + "_preferences_camera";
+ }
+
+ private void movePrefFrom(
+ Map<String, ?> m, String key, SharedPreferences src) {
+ if (m.containsKey(key)) {
+ Object v = m.get(key);
+ if (v instanceof String) {
+ mPrefGlobal.edit().putString(key, (String) v).apply();
+ } else if (v instanceof Integer) {
+ mPrefGlobal.edit().putInt(key, (Integer) v).apply();
+ } else if (v instanceof Long) {
+ mPrefGlobal.edit().putLong(key, (Long) v).apply();
+ } else if (v instanceof Float) {
+ mPrefGlobal.edit().putFloat(key, (Float) v).apply();
+ } else if (v instanceof Boolean) {
+ mPrefGlobal.edit().putBoolean(key, (Boolean) v).apply();
+ }
+ src.edit().remove(key).apply();
+ }
+ }
+
+ private void moveGlobalPrefsFrom(SharedPreferences src) {
+ Map<String, ?> prefMap = src.getAll();
+ movePrefFrom(prefMap, CameraSettings.KEY_VERSION, src);
+ movePrefFrom(prefMap, CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL, src);
+ movePrefFrom(prefMap, CameraSettings.KEY_CAMERA_ID, src);
+ movePrefFrom(prefMap, CameraSettings.KEY_RECORD_LOCATION, src);
+ movePrefFrom(prefMap, CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, src);
+ movePrefFrom(prefMap, CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, src);
+ movePrefFrom(prefMap, CameraSettings.KEY_VIDEO_EFFECT, src);
+ }
+
+ public static String[] getSharedPreferencesNames(Context context) {
+ int numOfCameras = CameraHolder.instance().getNumberOfCameras();
+ String prefNames[] = new String[numOfCameras + 1];
+ prefNames[0] = getGlobalSharedPreferencesName(context);
+ for (int i = 0; i < numOfCameras; i++) {
+ prefNames[i + 1] = getLocalSharedPreferencesName(context, i);
+ }
+ return prefNames;
+ }
+
+ // Sets the camera id and reads its preferences. Each camera has its own
+ // preferences.
+ public void setLocalId(Context context, int cameraId) {
+ String prefName = getLocalSharedPreferencesName(context, cameraId);
+ if (mPrefLocal != null) {
+ mPrefLocal.unregisterOnSharedPreferenceChangeListener(this);
+ }
+ mPrefLocal = context.getSharedPreferences(
+ prefName, Context.MODE_PRIVATE);
+ mPrefLocal.registerOnSharedPreferenceChangeListener(this);
+ }
+
+ public SharedPreferences getGlobal() {
+ return mPrefGlobal;
+ }
+
+ public SharedPreferences getLocal() {
+ return mPrefLocal;
+ }
+
+ @Override
+ public Map<String, ?> getAll() {
+ throw new UnsupportedOperationException(); // Can be implemented if needed.
+ }
+
+ private static boolean isGlobal(String key) {
+ return key.equals(CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL)
+ || key.equals(CameraSettings.KEY_CAMERA_ID)
+ || key.equals(CameraSettings.KEY_RECORD_LOCATION)
+ || key.equals(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN)
+ || key.equals(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN)
+ || key.equals(CameraSettings.KEY_VIDEO_EFFECT)
+ || key.equals(CameraSettings.KEY_TIMER)
+ || key.equals(CameraSettings.KEY_TIMER_SOUND_EFFECTS)
+ || key.equals(CameraSettings.KEY_PHOTOSPHERE_PICTURESIZE);
+ }
+
+ @Override
+ public String getString(String key, String defValue) {
+ if (isGlobal(key) || !mPrefLocal.contains(key)) {
+ return mPrefGlobal.getString(key, defValue);
+ } else {
+ return mPrefLocal.getString(key, defValue);
+ }
+ }
+
+ @Override
+ public int getInt(String key, int defValue) {
+ if (isGlobal(key) || !mPrefLocal.contains(key)) {
+ return mPrefGlobal.getInt(key, defValue);
+ } else {
+ return mPrefLocal.getInt(key, defValue);
+ }
+ }
+
+ @Override
+ public long getLong(String key, long defValue) {
+ if (isGlobal(key) || !mPrefLocal.contains(key)) {
+ return mPrefGlobal.getLong(key, defValue);
+ } else {
+ return mPrefLocal.getLong(key, defValue);
+ }
+ }
+
+ @Override
+ public float getFloat(String key, float defValue) {
+ if (isGlobal(key) || !mPrefLocal.contains(key)) {
+ return mPrefGlobal.getFloat(key, defValue);
+ } else {
+ return mPrefLocal.getFloat(key, defValue);
+ }
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defValue) {
+ if (isGlobal(key) || !mPrefLocal.contains(key)) {
+ return mPrefGlobal.getBoolean(key, defValue);
+ } else {
+ return mPrefLocal.getBoolean(key, defValue);
+ }
+ }
+
+ // This method is not used.
+ @Override
+ public Set<String> getStringSet(String key, Set<String> defValues) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean contains(String key) {
+ return mPrefLocal.contains(key) || mPrefGlobal.contains(key);
+ }
+
+ private class MyEditor implements Editor {
+ private Editor mEditorGlobal;
+ private Editor mEditorLocal;
+
+ MyEditor() {
+ mEditorGlobal = mPrefGlobal.edit();
+ mEditorLocal = mPrefLocal.edit();
+ }
+
+ @Override
+ public boolean commit() {
+ boolean result1 = mEditorGlobal.commit();
+ boolean result2 = mEditorLocal.commit();
+ return result1 && result2;
+ }
+
+ @Override
+ public void apply() {
+ mEditorGlobal.apply();
+ mEditorLocal.apply();
+ }
+
+ // Note: clear() and remove() affects both local and global preferences.
+ @Override
+ public Editor clear() {
+ mEditorGlobal.clear();
+ mEditorLocal.clear();
+ return this;
+ }
+
+ @Override
+ public Editor remove(String key) {
+ mEditorGlobal.remove(key);
+ mEditorLocal.remove(key);
+ return this;
+ }
+
+ @Override
+ public Editor putString(String key, String value) {
+ if (isGlobal(key)) {
+ mEditorGlobal.putString(key, value);
+ } else {
+ mEditorLocal.putString(key, value);
+ }
+ return this;
+ }
+
+ @Override
+ public Editor putInt(String key, int value) {
+ if (isGlobal(key)) {
+ mEditorGlobal.putInt(key, value);
+ } else {
+ mEditorLocal.putInt(key, value);
+ }
+ return this;
+ }
+
+ @Override
+ public Editor putLong(String key, long value) {
+ if (isGlobal(key)) {
+ mEditorGlobal.putLong(key, value);
+ } else {
+ mEditorLocal.putLong(key, value);
+ }
+ return this;
+ }
+
+ @Override
+ public Editor putFloat(String key, float value) {
+ if (isGlobal(key)) {
+ mEditorGlobal.putFloat(key, value);
+ } else {
+ mEditorLocal.putFloat(key, value);
+ }
+ return this;
+ }
+
+ @Override
+ public Editor putBoolean(String key, boolean value) {
+ if (isGlobal(key)) {
+ mEditorGlobal.putBoolean(key, value);
+ } else {
+ mEditorLocal.putBoolean(key, value);
+ }
+ return this;
+ }
+
+ // This method is not used.
+ @Override
+ public Editor putStringSet(String key, Set<String> values) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ // Note the remove() and clear() of the returned Editor may not work as
+ // expected because it doesn't touch the global preferences at all.
+ @Override
+ public Editor edit() {
+ return new MyEditor();
+ }
+
+ @Override
+ public void registerOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ mListeners.add(listener);
+ }
+
+ @Override
+ public void unregisterOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ mListeners.remove(listener);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+ for (OnSharedPreferenceChangeListener listener : mListeners) {
+ listener.onSharedPreferenceChanged(this, key);
+ }
+ BackupManager.dataChanged(mPackageName);
+ UsageStatistics.onEvent("CameraSettingsChange", null, key);
+ }
+}
diff --git a/src/com/android/camera/CountDownTimerPreference.java b/src/com/android/camera/CountDownTimerPreference.java
new file mode 100644
index 000000000..9c66dda8c
--- /dev/null
+++ b/src/com/android/camera/CountDownTimerPreference.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 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.camera;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.R;
+
+public class CountDownTimerPreference extends ListPreference {
+ private static final int[] DURATIONS = {
+ 0, 1, 2, 3, 4, 5, 10, 15, 20, 30, 60
+ };
+ public CountDownTimerPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initCountDownDurationChoices(context);
+ }
+
+ private void initCountDownDurationChoices(Context context) {
+ CharSequence[] entryValues = new CharSequence[DURATIONS.length];
+ CharSequence[] entries = new CharSequence[DURATIONS.length];
+ for (int i = 0; i < DURATIONS.length; i++) {
+ entryValues[i] = Integer.toString(DURATIONS[i]);
+ if (i == 0) {
+ entries[0] = context.getString(R.string.setting_off); // Off
+ } else {
+ entries[i] = context.getResources()
+ .getQuantityString(R.plurals.pref_camera_timer_entry, i, i);
+ }
+ }
+ setEntries(entries);
+ setEntryValues(entryValues);
+ }
+}
diff --git a/src/com/android/camera/DisableCameraReceiver.java b/src/com/android/camera/DisableCameraReceiver.java
new file mode 100644
index 000000000..351740541
--- /dev/null
+++ b/src/com/android/camera/DisableCameraReceiver.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.hardware.Camera.CameraInfo;
+import android.util.Log;
+
+// We want to disable camera-related activities if there is no camera. This
+// receiver runs when BOOT_COMPLETED intent is received. After running once
+// this receiver will be disabled, so it will not run again.
+public class DisableCameraReceiver extends BroadcastReceiver {
+ private static final String TAG = "DisableCameraReceiver";
+ private static final boolean CHECK_BACK_CAMERA_ONLY = true;
+ private static final String ACTIVITIES[] = {
+ "com.android.camera.CameraLauncher",
+ };
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Disable camera-related activities if there is no camera.
+ boolean needCameraActivity = CHECK_BACK_CAMERA_ONLY
+ ? hasBackCamera()
+ : hasCamera();
+
+ if (!needCameraActivity) {
+ Log.i(TAG, "disable all camera activities");
+ for (int i = 0; i < ACTIVITIES.length; i++) {
+ disableComponent(context, ACTIVITIES[i]);
+ }
+ }
+
+ // Disable this receiver so it won't run again.
+ disableComponent(context, "com.android.camera.DisableCameraReceiver");
+ }
+
+ private boolean hasCamera() {
+ int n = android.hardware.Camera.getNumberOfCameras();
+ Log.i(TAG, "number of camera: " + n);
+ return (n > 0);
+ }
+
+ private boolean hasBackCamera() {
+ int n = android.hardware.Camera.getNumberOfCameras();
+ CameraInfo info = new CameraInfo();
+ for (int i = 0; i < n; i++) {
+ android.hardware.Camera.getCameraInfo(i, info);
+ if (info.facing == CameraInfo.CAMERA_FACING_BACK) {
+ Log.i(TAG, "back camera found: " + i);
+ return true;
+ }
+ }
+ Log.i(TAG, "no back camera");
+ return false;
+ }
+
+ private void disableComponent(Context context, String klass) {
+ ComponentName name = new ComponentName(context, klass);
+ PackageManager pm = context.getPackageManager();
+
+ // We need the DONT_KILL_APP flag, otherwise we will be killed
+ // immediately because we are in the same app.
+ pm.setComponentEnabledSetting(name,
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
+ PackageManager.DONT_KILL_APP);
+ }
+}
diff --git a/src/com/android/camera/EffectsRecorder.java b/src/com/android/camera/EffectsRecorder.java
new file mode 100644
index 000000000..4bf8d411e
--- /dev/null
+++ b/src/com/android/camera/EffectsRecorder.java
@@ -0,0 +1,1239 @@
+/*
+ * 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.camera;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.media.CamcorderProfile;
+import android.media.MediaRecorder;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.io.FileDescriptor;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+
+/**
+ * Encapsulates the mobile filter framework components needed to record video
+ * with effects applied. Modeled after MediaRecorder.
+ */
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) // uses SurfaceTexture
+public class EffectsRecorder {
+ private static final String TAG = "EffectsRecorder";
+
+ private static Class<?> sClassFilter;
+ private static Method sFilterIsAvailable;
+ private static EffectsRecorder sEffectsRecorder;
+ // The index of the current effects recorder.
+ private static int sEffectsRecorderIndex;
+
+ private static boolean sReflectionInited = false;
+
+ private static Class<?> sClsLearningDoneListener;
+ private static Class<?> sClsOnRunnerDoneListener;
+ private static Class<?> sClsOnRecordingDoneListener;
+ private static Class<?> sClsSurfaceTextureSourceListener;
+
+ private static Method sFilterSetInputValue;
+
+ private static Constructor<?> sCtPoint;
+ private static Constructor<?> sCtQuad;
+
+ private static Method sLearningDoneListenerOnLearningDone;
+
+ private static Method sObjectEquals;
+ private static Method sObjectToString;
+
+ private static Class<?> sClsGraphRunner;
+ private static Method sGraphRunnerGetGraph;
+ private static Method sGraphRunnerSetDoneCallback;
+ private static Method sGraphRunnerRun;
+ private static Method sGraphRunnerGetError;
+ private static Method sGraphRunnerStop;
+
+ private static Method sFilterGraphGetFilter;
+ private static Method sFilterGraphTearDown;
+
+ private static Method sOnRunnerDoneListenerOnRunnerDone;
+
+ private static Class<?> sClsGraphEnvironment;
+ private static Constructor<?> sCtGraphEnvironment;
+ private static Method sGraphEnvironmentCreateGLEnvironment;
+ private static Method sGraphEnvironmentGetRunner;
+ private static Method sGraphEnvironmentAddReferences;
+ private static Method sGraphEnvironmentLoadGraph;
+ private static Method sGraphEnvironmentGetContext;
+
+ private static Method sFilterContextGetGLEnvironment;
+ private static Method sGLEnvironmentIsActive;
+ private static Method sGLEnvironmentActivate;
+ private static Method sGLEnvironmentDeactivate;
+ private static Method sSurfaceTextureTargetDisconnect;
+ private static Method sOnRecordingDoneListenerOnRecordingDone;
+ private static Method sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady;
+
+ private Object mLearningDoneListener;
+ private Object mRunnerDoneCallback;
+ private Object mSourceReadyCallback;
+ // A callback to finalize the media after the recording is done.
+ private Object mRecordingDoneListener;
+
+ static {
+ try {
+ sClassFilter = Class.forName("android.filterfw.core.Filter");
+ sFilterIsAvailable = sClassFilter.getMethod("isAvailable",
+ String.class);
+ } catch (ClassNotFoundException ex) {
+ Log.v(TAG, "Can't find the class android.filterfw.core.Filter");
+ } catch (NoSuchMethodException e) {
+ Log.v(TAG, "Can't find the method Filter.isAvailable");
+ }
+ }
+
+ public static final int EFFECT_NONE = 0;
+ public static final int EFFECT_GOOFY_FACE = 1;
+ public static final int EFFECT_BACKDROPPER = 2;
+
+ public static final int EFFECT_GF_SQUEEZE = 0;
+ public static final int EFFECT_GF_BIG_EYES = 1;
+ public static final int EFFECT_GF_BIG_MOUTH = 2;
+ public static final int EFFECT_GF_SMALL_MOUTH = 3;
+ public static final int EFFECT_GF_BIG_NOSE = 4;
+ public static final int EFFECT_GF_SMALL_EYES = 5;
+ public static final int NUM_OF_GF_EFFECTS = EFFECT_GF_SMALL_EYES + 1;
+
+ public static final int EFFECT_MSG_STARTED_LEARNING = 0;
+ public static final int EFFECT_MSG_DONE_LEARNING = 1;
+ public static final int EFFECT_MSG_SWITCHING_EFFECT = 2;
+ public static final int EFFECT_MSG_EFFECTS_STOPPED = 3;
+ public static final int EFFECT_MSG_RECORDING_DONE = 4;
+ public static final int EFFECT_MSG_PREVIEW_RUNNING = 5;
+
+ private Context mContext;
+ private Handler mHandler;
+
+ private CameraManager.CameraProxy mCameraDevice;
+ private CamcorderProfile mProfile;
+ private double mCaptureRate = 0;
+ private SurfaceTexture mPreviewSurfaceTexture;
+ private int mPreviewWidth;
+ private int mPreviewHeight;
+ private MediaRecorder.OnInfoListener mInfoListener;
+ private MediaRecorder.OnErrorListener mErrorListener;
+
+ private String mOutputFile;
+ private FileDescriptor mFd;
+ private int mOrientationHint = 0;
+ private long mMaxFileSize = 0;
+ private int mMaxDurationMs = 0;
+ private int mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK;
+ private int mCameraDisplayOrientation;
+
+ private int mEffect = EFFECT_NONE;
+ private int mCurrentEffect = EFFECT_NONE;
+ private EffectsListener mEffectsListener;
+
+ private Object mEffectParameter;
+
+ private Object mGraphEnv;
+ private int mGraphId;
+ private Object mRunner = null;
+ private Object mOldRunner = null;
+
+ private SurfaceTexture mTextureSource;
+
+ private static final int STATE_CONFIGURE = 0;
+ private static final int STATE_WAITING_FOR_SURFACE = 1;
+ private static final int STATE_STARTING_PREVIEW = 2;
+ private static final int STATE_PREVIEW = 3;
+ private static final int STATE_RECORD = 4;
+ private static final int STATE_RELEASED = 5;
+ private int mState = STATE_CONFIGURE;
+
+ private boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE);
+ private SoundClips.Player mSoundPlayer;
+
+ /** Determine if a given effect is supported at runtime
+ * Some effects require libraries not available on all devices
+ */
+ public static boolean isEffectSupported(int effectId) {
+ if (sFilterIsAvailable == null) return false;
+
+ try {
+ switch (effectId) {
+ case EFFECT_GOOFY_FACE:
+ return (Boolean) sFilterIsAvailable.invoke(null,
+ "com.google.android.filterpacks.facedetect.GoofyRenderFilter");
+ case EFFECT_BACKDROPPER:
+ return (Boolean) sFilterIsAvailable.invoke(null,
+ "android.filterpacks.videoproc.BackDropperFilter");
+ default:
+ return false;
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "Fail to check filter", ex);
+ }
+ return false;
+ }
+
+ public EffectsRecorder(Context context) {
+ if (mLogVerbose) Log.v(TAG, "EffectsRecorder created (" + this + ")");
+
+ if (!sReflectionInited) {
+ try {
+ sFilterSetInputValue = sClassFilter.getMethod("setInputValue",
+ new Class[] {String.class, Object.class});
+
+ Class<?> clsPoint = Class.forName("android.filterfw.geometry.Point");
+ sCtPoint = clsPoint.getConstructor(new Class[] {float.class,
+ float.class});
+
+ Class<?> clsQuad = Class.forName("android.filterfw.geometry.Quad");
+ sCtQuad = clsQuad.getConstructor(new Class[] {clsPoint, clsPoint,
+ clsPoint, clsPoint});
+
+ Class<?> clsBackDropperFilter = Class.forName(
+ "android.filterpacks.videoproc.BackDropperFilter");
+ sClsLearningDoneListener = Class.forName(
+ "android.filterpacks.videoproc.BackDropperFilter$LearningDoneListener");
+ sLearningDoneListenerOnLearningDone = sClsLearningDoneListener
+ .getMethod("onLearningDone", new Class[] {clsBackDropperFilter});
+
+ sObjectEquals = Object.class.getMethod("equals", new Class[] {Object.class});
+ sObjectToString = Object.class.getMethod("toString");
+
+ sClsOnRunnerDoneListener = Class.forName(
+ "android.filterfw.core.GraphRunner$OnRunnerDoneListener");
+ sOnRunnerDoneListenerOnRunnerDone = sClsOnRunnerDoneListener.getMethod(
+ "onRunnerDone", new Class[] {int.class});
+
+ sClsGraphRunner = Class.forName("android.filterfw.core.GraphRunner");
+ sGraphRunnerGetGraph = sClsGraphRunner.getMethod("getGraph");
+ sGraphRunnerSetDoneCallback = sClsGraphRunner.getMethod(
+ "setDoneCallback", new Class[] {sClsOnRunnerDoneListener});
+ sGraphRunnerRun = sClsGraphRunner.getMethod("run");
+ sGraphRunnerGetError = sClsGraphRunner.getMethod("getError");
+ sGraphRunnerStop = sClsGraphRunner.getMethod("stop");
+
+ Class<?> clsFilterContext = Class.forName("android.filterfw.core.FilterContext");
+ sFilterContextGetGLEnvironment = clsFilterContext.getMethod(
+ "getGLEnvironment");
+
+ Class<?> clsFilterGraph = Class.forName("android.filterfw.core.FilterGraph");
+ sFilterGraphGetFilter = clsFilterGraph.getMethod("getFilter",
+ new Class[] {String.class});
+ sFilterGraphTearDown = clsFilterGraph.getMethod("tearDown",
+ new Class[] {clsFilterContext});
+
+ sClsGraphEnvironment = Class.forName("android.filterfw.GraphEnvironment");
+ sCtGraphEnvironment = sClsGraphEnvironment.getConstructor();
+ sGraphEnvironmentCreateGLEnvironment = sClsGraphEnvironment.getMethod(
+ "createGLEnvironment");
+ sGraphEnvironmentGetRunner = sClsGraphEnvironment.getMethod(
+ "getRunner", new Class[] {int.class, int.class});
+ sGraphEnvironmentAddReferences = sClsGraphEnvironment.getMethod(
+ "addReferences", new Class[] {Object[].class});
+ sGraphEnvironmentLoadGraph = sClsGraphEnvironment.getMethod(
+ "loadGraph", new Class[] {Context.class, int.class});
+ sGraphEnvironmentGetContext = sClsGraphEnvironment.getMethod(
+ "getContext");
+
+ Class<?> clsGLEnvironment = Class.forName("android.filterfw.core.GLEnvironment");
+ sGLEnvironmentIsActive = clsGLEnvironment.getMethod("isActive");
+ sGLEnvironmentActivate = clsGLEnvironment.getMethod("activate");
+ sGLEnvironmentDeactivate = clsGLEnvironment.getMethod("deactivate");
+
+ Class<?> clsSurfaceTextureTarget = Class.forName(
+ "android.filterpacks.videosrc.SurfaceTextureTarget");
+ sSurfaceTextureTargetDisconnect = clsSurfaceTextureTarget.getMethod(
+ "disconnect", new Class[] {clsFilterContext});
+
+ sClsOnRecordingDoneListener = Class.forName(
+ "android.filterpacks.videosink.MediaEncoderFilter$OnRecordingDoneListener");
+ sOnRecordingDoneListenerOnRecordingDone =
+ sClsOnRecordingDoneListener.getMethod("onRecordingDone");
+
+ sClsSurfaceTextureSourceListener = Class.forName(
+ "android.filterpacks.videosrc.SurfaceTextureSource$SurfaceTextureSourceListener");
+ sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady =
+ sClsSurfaceTextureSourceListener.getMethod(
+ "onSurfaceTextureSourceReady",
+ new Class[] {SurfaceTexture.class});
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+
+ sReflectionInited = true;
+ }
+
+ sEffectsRecorderIndex++;
+ Log.v(TAG, "Current effects recorder index is " + sEffectsRecorderIndex);
+ sEffectsRecorder = this;
+ SerializableInvocationHandler sih = new SerializableInvocationHandler(
+ sEffectsRecorderIndex);
+ mLearningDoneListener = Proxy.newProxyInstance(
+ sClsLearningDoneListener.getClassLoader(),
+ new Class[] {sClsLearningDoneListener}, sih);
+ mRunnerDoneCallback = Proxy.newProxyInstance(
+ sClsOnRunnerDoneListener.getClassLoader(),
+ new Class[] {sClsOnRunnerDoneListener}, sih);
+ mSourceReadyCallback = Proxy.newProxyInstance(
+ sClsSurfaceTextureSourceListener.getClassLoader(),
+ new Class[] {sClsSurfaceTextureSourceListener}, sih);
+ mRecordingDoneListener = Proxy.newProxyInstance(
+ sClsOnRecordingDoneListener.getClassLoader(),
+ new Class[] {sClsOnRecordingDoneListener}, sih);
+
+ mContext = context;
+ mHandler = new Handler(Looper.getMainLooper());
+ mSoundPlayer = SoundClips.getPlayer(context);
+ }
+
+ public synchronized void setCamera(CameraManager.CameraProxy cameraDevice) {
+ switch (mState) {
+ case STATE_PREVIEW:
+ throw new RuntimeException("setCamera cannot be called while previewing!");
+ case STATE_RECORD:
+ throw new RuntimeException("setCamera cannot be called while recording!");
+ case STATE_RELEASED:
+ throw new RuntimeException("setCamera called on an already released recorder!");
+ default:
+ break;
+ }
+
+ mCameraDevice = cameraDevice;
+ }
+
+ public void setProfile(CamcorderProfile profile) {
+ switch (mState) {
+ case STATE_RECORD:
+ throw new RuntimeException("setProfile cannot be called while recording!");
+ case STATE_RELEASED:
+ throw new RuntimeException("setProfile called on an already released recorder!");
+ default:
+ break;
+ }
+ mProfile = profile;
+ }
+
+ public void setOutputFile(String outputFile) {
+ switch (mState) {
+ case STATE_RECORD:
+ throw new RuntimeException("setOutputFile cannot be called while recording!");
+ case STATE_RELEASED:
+ throw new RuntimeException("setOutputFile called on an already released recorder!");
+ default:
+ break;
+ }
+
+ mOutputFile = outputFile;
+ mFd = null;
+ }
+
+ public void setOutputFile(FileDescriptor fd) {
+ switch (mState) {
+ case STATE_RECORD:
+ throw new RuntimeException("setOutputFile cannot be called while recording!");
+ case STATE_RELEASED:
+ throw new RuntimeException("setOutputFile called on an already released recorder!");
+ default:
+ break;
+ }
+
+ mOutputFile = null;
+ mFd = fd;
+ }
+
+ /**
+ * Sets the maximum filesize (in bytes) of the recording session.
+ * This will be passed on to the MediaEncoderFilter and then to the
+ * MediaRecorder ultimately. If zero or negative, the MediaRecorder will
+ * disable the limit
+ */
+ public synchronized void setMaxFileSize(long maxFileSize) {
+ switch (mState) {
+ case STATE_RECORD:
+ throw new RuntimeException("setMaxFileSize cannot be called while recording!");
+ case STATE_RELEASED:
+ throw new RuntimeException(
+ "setMaxFileSize called on an already released recorder!");
+ default:
+ break;
+ }
+ mMaxFileSize = maxFileSize;
+ }
+
+ /**
+ * Sets the maximum recording duration (in ms) for the next recording session
+ * Setting it to zero (the default) disables the limit.
+ */
+ public synchronized void setMaxDuration(int maxDurationMs) {
+ switch (mState) {
+ case STATE_RECORD:
+ throw new RuntimeException("setMaxDuration cannot be called while recording!");
+ case STATE_RELEASED:
+ throw new RuntimeException(
+ "setMaxDuration called on an already released recorder!");
+ default:
+ break;
+ }
+ mMaxDurationMs = maxDurationMs;
+ }
+
+
+ public void setCaptureRate(double fps) {
+ switch (mState) {
+ case STATE_RECORD:
+ throw new RuntimeException("setCaptureRate cannot be called while recording!");
+ case STATE_RELEASED:
+ throw new RuntimeException(
+ "setCaptureRate called on an already released recorder!");
+ default:
+ break;
+ }
+
+ if (mLogVerbose) Log.v(TAG, "Setting time lapse capture rate to " + fps + " fps");
+ mCaptureRate = fps;
+ }
+
+ public void setPreviewSurfaceTexture(SurfaceTexture previewSurfaceTexture,
+ int previewWidth,
+ int previewHeight) {
+ if (mLogVerbose) Log.v(TAG, "setPreviewSurfaceTexture(" + this + ")");
+ switch (mState) {
+ case STATE_RECORD:
+ throw new RuntimeException(
+ "setPreviewSurfaceTexture cannot be called while recording!");
+ case STATE_RELEASED:
+ throw new RuntimeException(
+ "setPreviewSurfaceTexture called on an already released recorder!");
+ default:
+ break;
+ }
+
+ mPreviewSurfaceTexture = previewSurfaceTexture;
+ mPreviewWidth = previewWidth;
+ mPreviewHeight = previewHeight;
+
+ switch (mState) {
+ case STATE_WAITING_FOR_SURFACE:
+ startPreview();
+ break;
+ case STATE_STARTING_PREVIEW:
+ case STATE_PREVIEW:
+ initializeEffect(true);
+ break;
+ }
+ }
+
+ public void setEffect(int effect, Object effectParameter) {
+ if (mLogVerbose) Log.v(TAG,
+ "setEffect: effect ID " + effect +
+ ", parameter " + effectParameter.toString());
+ switch (mState) {
+ case STATE_RECORD:
+ throw new RuntimeException("setEffect cannot be called while recording!");
+ case STATE_RELEASED:
+ throw new RuntimeException("setEffect called on an already released recorder!");
+ default:
+ break;
+ }
+
+ mEffect = effect;
+ mEffectParameter = effectParameter;
+
+ if (mState == STATE_PREVIEW ||
+ mState == STATE_STARTING_PREVIEW) {
+ initializeEffect(false);
+ }
+ }
+
+ public interface EffectsListener {
+ public void onEffectsUpdate(int effectId, int effectMsg);
+ public void onEffectsError(Exception exception, String filePath);
+ }
+
+ public void setEffectsListener(EffectsListener listener) {
+ mEffectsListener = listener;
+ }
+
+ private void setFaceDetectOrientation() {
+ if (mCurrentEffect == EFFECT_GOOFY_FACE) {
+ Object rotateFilter = getGraphFilter(mRunner, "rotate");
+ Object metaRotateFilter = getGraphFilter(mRunner, "metarotate");
+ setInputValue(rotateFilter, "rotation", mOrientationHint);
+ int reverseDegrees = (360 - mOrientationHint) % 360;
+ setInputValue(metaRotateFilter, "rotation", reverseDegrees);
+ }
+ }
+
+ private void setRecordingOrientation() {
+ if (mState != STATE_RECORD && mRunner != null) {
+ Object bl = newInstance(sCtPoint, new Object[] {0, 0});
+ Object br = newInstance(sCtPoint, new Object[] {1, 0});
+ Object tl = newInstance(sCtPoint, new Object[] {0, 1});
+ Object tr = newInstance(sCtPoint, new Object[] {1, 1});
+ Object recordingRegion;
+ if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_BACK) {
+ // The back camera is not mirrored, so use a identity transform
+ recordingRegion = newInstance(sCtQuad, new Object[] {bl, br, tl, tr});
+ } else {
+ // Recording region needs to be tweaked for front cameras, since they
+ // mirror their preview
+ if (mOrientationHint == 0 || mOrientationHint == 180) {
+ // Horizontal flip in landscape
+ recordingRegion = newInstance(sCtQuad, new Object[] {br, bl, tr, tl});
+ } else {
+ // Horizontal flip in portrait
+ recordingRegion = newInstance(sCtQuad, new Object[] {tl, tr, bl, br});
+ }
+ }
+ Object recorder = getGraphFilter(mRunner, "recorder");
+ setInputValue(recorder, "inputRegion", recordingRegion);
+ }
+ }
+ public void setOrientationHint(int degrees) {
+ switch (mState) {
+ case STATE_RELEASED:
+ throw new RuntimeException(
+ "setOrientationHint called on an already released recorder!");
+ default:
+ break;
+ }
+ if (mLogVerbose) Log.v(TAG, "Setting orientation hint to: " + degrees);
+ mOrientationHint = degrees;
+ setFaceDetectOrientation();
+ setRecordingOrientation();
+ }
+
+ public void setCameraDisplayOrientation(int orientation) {
+ if (mState != STATE_CONFIGURE) {
+ throw new RuntimeException(
+ "setCameraDisplayOrientation called after configuration!");
+ }
+ mCameraDisplayOrientation = orientation;
+ }
+
+ public void setCameraFacing(int facing) {
+ switch (mState) {
+ case STATE_RELEASED:
+ throw new RuntimeException(
+ "setCameraFacing called on alrady released recorder!");
+ default:
+ break;
+ }
+ mCameraFacing = facing;
+ setRecordingOrientation();
+ }
+
+ public void setOnInfoListener(MediaRecorder.OnInfoListener infoListener) {
+ switch (mState) {
+ case STATE_RECORD:
+ throw new RuntimeException("setInfoListener cannot be called while recording!");
+ case STATE_RELEASED:
+ throw new RuntimeException(
+ "setInfoListener called on an already released recorder!");
+ default:
+ break;
+ }
+ mInfoListener = infoListener;
+ }
+
+ public void setOnErrorListener(MediaRecorder.OnErrorListener errorListener) {
+ switch (mState) {
+ case STATE_RECORD:
+ throw new RuntimeException("setErrorListener cannot be called while recording!");
+ case STATE_RELEASED:
+ throw new RuntimeException(
+ "setErrorListener called on an already released recorder!");
+ default:
+ break;
+ }
+ mErrorListener = errorListener;
+ }
+
+ private void initializeFilterFramework() {
+ mGraphEnv = newInstance(sCtGraphEnvironment);
+ invoke(mGraphEnv, sGraphEnvironmentCreateGLEnvironment);
+
+ int videoFrameWidth = mProfile.videoFrameWidth;
+ int videoFrameHeight = mProfile.videoFrameHeight;
+ if (mCameraDisplayOrientation == 90 || mCameraDisplayOrientation == 270) {
+ int tmp = videoFrameWidth;
+ videoFrameWidth = videoFrameHeight;
+ videoFrameHeight = tmp;
+ }
+
+ invoke(mGraphEnv, sGraphEnvironmentAddReferences,
+ new Object[] {new Object[] {
+ "textureSourceCallback", mSourceReadyCallback,
+ "recordingWidth", videoFrameWidth,
+ "recordingHeight", videoFrameHeight,
+ "recordingProfile", mProfile,
+ "learningDoneListener", mLearningDoneListener,
+ "recordingDoneListener", mRecordingDoneListener}});
+ mRunner = null;
+ mGraphId = -1;
+ mCurrentEffect = EFFECT_NONE;
+ }
+
+ private synchronized void initializeEffect(boolean forceReset) {
+ if (forceReset ||
+ mCurrentEffect != mEffect ||
+ mCurrentEffect == EFFECT_BACKDROPPER) {
+
+ invoke(mGraphEnv, sGraphEnvironmentAddReferences,
+ new Object[] {new Object[] {
+ "previewSurfaceTexture", mPreviewSurfaceTexture,
+ "previewWidth", mPreviewWidth,
+ "previewHeight", mPreviewHeight,
+ "orientation", mOrientationHint}});
+ if (mState == STATE_PREVIEW ||
+ mState == STATE_STARTING_PREVIEW) {
+ // Switching effects while running. Inform video camera.
+ sendMessage(mCurrentEffect, EFFECT_MSG_SWITCHING_EFFECT);
+ }
+
+ switch (mEffect) {
+ case EFFECT_GOOFY_FACE:
+ mGraphId = (Integer) invoke(mGraphEnv,
+ sGraphEnvironmentLoadGraph,
+ new Object[] {mContext, R.raw.goofy_face});
+ break;
+ case EFFECT_BACKDROPPER:
+ sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING);
+ mGraphId = (Integer) invoke(mGraphEnv,
+ sGraphEnvironmentLoadGraph,
+ new Object[] {mContext, R.raw.backdropper});
+ break;
+ default:
+ throw new RuntimeException("Unknown effect ID" + mEffect + "!");
+ }
+ mCurrentEffect = mEffect;
+
+ mOldRunner = mRunner;
+ mRunner = invoke(mGraphEnv, sGraphEnvironmentGetRunner,
+ new Object[] {mGraphId,
+ getConstant(sClsGraphEnvironment, "MODE_ASYNCHRONOUS")});
+ invoke(mRunner, sGraphRunnerSetDoneCallback, new Object[] {mRunnerDoneCallback});
+ if (mLogVerbose) {
+ Log.v(TAG, "New runner: " + mRunner
+ + ". Old runner: " + mOldRunner);
+ }
+ if (mState == STATE_PREVIEW ||
+ mState == STATE_STARTING_PREVIEW) {
+ // Switching effects while running. Stop existing runner.
+ // The stop callback will take care of starting new runner.
+ mCameraDevice.stopPreview();
+ mCameraDevice.setPreviewTexture(null);
+ invoke(mOldRunner, sGraphRunnerStop);
+ }
+ }
+
+ switch (mCurrentEffect) {
+ case EFFECT_GOOFY_FACE:
+ tryEnableVideoStabilization(true);
+ Object goofyFilter = getGraphFilter(mRunner, "goofyrenderer");
+ setInputValue(goofyFilter, "currentEffect",
+ ((Integer) mEffectParameter).intValue());
+ break;
+ case EFFECT_BACKDROPPER:
+ tryEnableVideoStabilization(false);
+ Object backgroundSrc = getGraphFilter(mRunner, "background");
+ if (ApiHelper.HAS_EFFECTS_RECORDING_CONTEXT_INPUT) {
+ // Set the context first before setting sourceUrl to
+ // guarantee the content URI get resolved properly.
+ setInputValue(backgroundSrc, "context", mContext);
+ }
+ setInputValue(backgroundSrc, "sourceUrl", mEffectParameter);
+ // For front camera, the background video needs to be mirrored in the
+ // backdropper filter
+ if (mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ Object replacer = getGraphFilter(mRunner, "replacer");
+ setInputValue(replacer, "mirrorBg", true);
+ if (mLogVerbose) Log.v(TAG, "Setting the background to be mirrored");
+ }
+ break;
+ default:
+ break;
+ }
+ setFaceDetectOrientation();
+ setRecordingOrientation();
+ }
+
+ public synchronized void startPreview() {
+ if (mLogVerbose) Log.v(TAG, "Starting preview (" + this + ")");
+
+ switch (mState) {
+ case STATE_STARTING_PREVIEW:
+ case STATE_PREVIEW:
+ // Already running preview
+ Log.w(TAG, "startPreview called when already running preview");
+ return;
+ case STATE_RECORD:
+ throw new RuntimeException("Cannot start preview when already recording!");
+ case STATE_RELEASED:
+ throw new RuntimeException("setEffect called on an already released recorder!");
+ default:
+ break;
+ }
+
+ if (mEffect == EFFECT_NONE) {
+ throw new RuntimeException("No effect selected!");
+ }
+ if (mEffectParameter == null) {
+ throw new RuntimeException("No effect parameter provided!");
+ }
+ if (mProfile == null) {
+ throw new RuntimeException("No recording profile provided!");
+ }
+ if (mPreviewSurfaceTexture == null) {
+ if (mLogVerbose) Log.v(TAG, "Passed a null surface; waiting for valid one");
+ mState = STATE_WAITING_FOR_SURFACE;
+ return;
+ }
+ if (mCameraDevice == null) {
+ throw new RuntimeException("No camera to record from!");
+ }
+
+ if (mLogVerbose) Log.v(TAG, "Initializing filter framework and running the graph.");
+ initializeFilterFramework();
+
+ initializeEffect(true);
+
+ mState = STATE_STARTING_PREVIEW;
+ invoke(mRunner, sGraphRunnerRun);
+ // Rest of preview startup handled in mSourceReadyCallback
+ }
+
+ private Object invokeObjectEquals(Object proxy, Object[] args) {
+ return Boolean.valueOf(proxy == args[0]);
+ }
+
+ private Object invokeObjectToString() {
+ return "Proxy-" + toString();
+ }
+
+ private void invokeOnLearningDone() {
+ if (mLogVerbose) Log.v(TAG, "Learning done callback triggered");
+ // Called in a processing thread, so have to post message back to UI
+ // thread
+ sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_DONE_LEARNING);
+ enable3ALocks(true);
+ }
+
+ private void invokeOnRunnerDone(Object[] args) {
+ int runnerDoneResult = (Integer) args[0];
+ synchronized (EffectsRecorder.this) {
+ if (mLogVerbose) {
+ Log.v(TAG,
+ "Graph runner done (" + EffectsRecorder.this
+ + ", mRunner " + mRunner
+ + ", mOldRunner " + mOldRunner + ")");
+ }
+ if (runnerDoneResult ==
+ (Integer) getConstant(sClsGraphRunner, "RESULT_ERROR")) {
+ // Handle error case
+ Log.e(TAG, "Error running filter graph!");
+ Exception e = null;
+ if (mRunner != null) {
+ e = (Exception) invoke(mRunner, sGraphRunnerGetError);
+ } else if (mOldRunner != null) {
+ e = (Exception) invoke(mOldRunner, sGraphRunnerGetError);
+ }
+ raiseError(e);
+ }
+ if (mOldRunner != null) {
+ // Tear down old graph if available
+ if (mLogVerbose) Log.v(TAG, "Tearing down old graph.");
+ Object glEnv = getContextGLEnvironment(mGraphEnv);
+ if (glEnv != null && !(Boolean) invoke(glEnv, sGLEnvironmentIsActive)) {
+ invoke(glEnv, sGLEnvironmentActivate);
+ }
+ getGraphTearDown(mOldRunner,
+ invoke(mGraphEnv, sGraphEnvironmentGetContext));
+ if (glEnv != null && (Boolean) invoke(glEnv, sGLEnvironmentIsActive)) {
+ invoke(glEnv, sGLEnvironmentDeactivate);
+ }
+ mOldRunner = null;
+ }
+ if (mState == STATE_PREVIEW ||
+ mState == STATE_STARTING_PREVIEW) {
+ // Switching effects, start up the new runner
+ if (mLogVerbose) {
+ Log.v(TAG, "Previous effect halted. Running graph again. state: "
+ + mState);
+ }
+ tryEnable3ALocks(false);
+ // In case of an error, the graph restarts from beginning and in case
+ // of the BACKDROPPER effect, the learner re-learns the background.
+ // Hence, we need to show the learning dialogue to the user
+ // to avoid recording before the learning is done. Else, the user
+ // could start recording before the learning is done and the new
+ // background comes up later leading to an end result video
+ // with a heterogeneous background.
+ // For BACKDROPPER effect, this path is also executed sometimes at
+ // the end of a normal recording session. In such a case, the graph
+ // does not restart and hence the learner does not re-learn. So we
+ // do not want to show the learning dialogue then.
+ if (runnerDoneResult == (Integer) getConstant(
+ sClsGraphRunner, "RESULT_ERROR")
+ && mCurrentEffect == EFFECT_BACKDROPPER) {
+ sendMessage(EFFECT_BACKDROPPER, EFFECT_MSG_STARTED_LEARNING);
+ }
+ invoke(mRunner, sGraphRunnerRun);
+ } else if (mState != STATE_RELEASED) {
+ // Shutting down effects
+ if (mLogVerbose) Log.v(TAG, "Runner halted, restoring direct preview");
+ tryEnable3ALocks(false);
+ sendMessage(EFFECT_NONE, EFFECT_MSG_EFFECTS_STOPPED);
+ } else {
+ // STATE_RELEASED - camera will be/has been released as well, do nothing.
+ }
+ }
+ }
+
+ private void invokeOnSurfaceTextureSourceReady(Object[] args) {
+ SurfaceTexture source = (SurfaceTexture) args[0];
+ if (mLogVerbose) Log.v(TAG, "SurfaceTexture ready callback received");
+ synchronized (EffectsRecorder.this) {
+ mTextureSource = source;
+
+ if (mState == STATE_CONFIGURE) {
+ // Stop preview happened while the runner was doing startup tasks
+ // Since we haven't started anything up, don't do anything
+ // Rest of cleanup will happen in onRunnerDone
+ if (mLogVerbose) Log.v(TAG, "Ready callback: Already stopped, skipping.");
+ return;
+ }
+ if (mState == STATE_RELEASED) {
+ // EffectsRecorder has been released, so don't touch the camera device
+ // or anything else
+ if (mLogVerbose) Log.v(TAG, "Ready callback: Already released, skipping.");
+ return;
+ }
+ if (source == null) {
+ if (mLogVerbose) {
+ Log.v(TAG, "Ready callback: source null! Looks like graph was closed!");
+ }
+ if (mState == STATE_PREVIEW ||
+ mState == STATE_STARTING_PREVIEW ||
+ mState == STATE_RECORD) {
+ // A null source here means the graph is shutting down
+ // unexpectedly, so we need to turn off preview before
+ // the surface texture goes away.
+ if (mLogVerbose) {
+ Log.v(TAG, "Ready callback: State: " + mState
+ + ". stopCameraPreview");
+ }
+
+ stopCameraPreview();
+ }
+ return;
+ }
+
+ // Lock AE/AWB to reduce transition flicker
+ tryEnable3ALocks(true);
+
+ mCameraDevice.stopPreview();
+ if (mLogVerbose) Log.v(TAG, "Runner active, connecting effects preview");
+ mCameraDevice.setPreviewTexture(mTextureSource);
+
+ mCameraDevice.startPreview();
+
+ // Unlock AE/AWB after preview started
+ tryEnable3ALocks(false);
+
+ mState = STATE_PREVIEW;
+
+ if (mLogVerbose) Log.v(TAG, "Start preview/effect switch complete");
+
+ // Sending a message to listener that preview is complete
+ sendMessage(mCurrentEffect, EFFECT_MSG_PREVIEW_RUNNING);
+ }
+ }
+
+ private void invokeOnRecordingDone() {
+ // Forward the callback to the VideoModule object (as an asynchronous event).
+ if (mLogVerbose) Log.v(TAG, "Recording done callback triggered");
+ sendMessage(EFFECT_NONE, EFFECT_MSG_RECORDING_DONE);
+ }
+
+ public synchronized void startRecording() {
+ if (mLogVerbose) Log.v(TAG, "Starting recording (" + this + ")");
+
+ switch (mState) {
+ case STATE_RECORD:
+ throw new RuntimeException("Already recording, cannot begin anew!");
+ case STATE_RELEASED:
+ throw new RuntimeException(
+ "startRecording called on an already released recorder!");
+ default:
+ break;
+ }
+
+ if ((mOutputFile == null) && (mFd == null)) {
+ throw new RuntimeException("No output file name or descriptor provided!");
+ }
+
+ if (mState == STATE_CONFIGURE) {
+ startPreview();
+ }
+
+ Object recorder = getGraphFilter(mRunner, "recorder");
+ if (mFd != null) {
+ setInputValue(recorder, "outputFileDescriptor", mFd);
+ } else {
+ setInputValue(recorder, "outputFile", mOutputFile);
+ }
+ // It is ok to set the audiosource without checking for timelapse here
+ // since that check will be done in the MediaEncoderFilter itself
+ setInputValue(recorder, "audioSource", MediaRecorder.AudioSource.CAMCORDER);
+ setInputValue(recorder, "recordingProfile", mProfile);
+ setInputValue(recorder, "orientationHint", mOrientationHint);
+ // Important to set the timelapseinterval to 0 if the capture rate is not >0
+ // since the recorder does not get created every time the recording starts.
+ // The recorder infers whether the capture is timelapsed based on the value of
+ // this interval
+ boolean captureTimeLapse = mCaptureRate > 0;
+ if (captureTimeLapse) {
+ double timeBetweenFrameCapture = 1 / mCaptureRate;
+ setInputValue(recorder, "timelapseRecordingIntervalUs",
+ (long) (1000000 * timeBetweenFrameCapture));
+
+ } else {
+ setInputValue(recorder, "timelapseRecordingIntervalUs", 0L);
+ }
+
+ if (mInfoListener != null) {
+ setInputValue(recorder, "infoListener", mInfoListener);
+ }
+ if (mErrorListener != null) {
+ setInputValue(recorder, "errorListener", mErrorListener);
+ }
+ setInputValue(recorder, "maxFileSize", mMaxFileSize);
+ setInputValue(recorder, "maxDurationMs", mMaxDurationMs);
+ setInputValue(recorder, "recording", true);
+ mSoundPlayer.play(SoundClips.START_VIDEO_RECORDING);
+ mState = STATE_RECORD;
+ }
+
+ public synchronized void stopRecording() {
+ if (mLogVerbose) Log.v(TAG, "Stop recording (" + this + ")");
+
+ switch (mState) {
+ case STATE_CONFIGURE:
+ case STATE_STARTING_PREVIEW:
+ case STATE_PREVIEW:
+ Log.w(TAG, "StopRecording called when recording not active!");
+ return;
+ case STATE_RELEASED:
+ throw new RuntimeException("stopRecording called on released EffectsRecorder!");
+ default:
+ break;
+ }
+ Object recorder = getGraphFilter(mRunner, "recorder");
+ setInputValue(recorder, "recording", false);
+ mSoundPlayer.play(SoundClips.STOP_VIDEO_RECORDING);
+ mState = STATE_PREVIEW;
+ }
+
+ // Called to tell the filter graph that the display surfacetexture is not valid anymore.
+ // So the filter graph should not hold any reference to the surface created with that.
+ public synchronized void disconnectDisplay() {
+ if (mLogVerbose) Log.v(TAG, "Disconnecting the graph from the " +
+ "SurfaceTexture");
+ Object display = getGraphFilter(mRunner, "display");
+ invoke(display, sSurfaceTextureTargetDisconnect, new Object[] {
+ invoke(mGraphEnv, sGraphEnvironmentGetContext)});
+ }
+
+ // The VideoModule will call this to notify that the camera is being
+ // released to the outside world. This call should happen after the
+ // stopRecording call. Else, the effects may throw an exception.
+ // With the recording stopped, the stopPreview call will not try to
+ // release the camera again.
+ // This must be called in onPause() if the effects are ON.
+ public synchronized void disconnectCamera() {
+ if (mLogVerbose) Log.v(TAG, "Disconnecting the effects from Camera");
+ stopCameraPreview();
+ mCameraDevice = null;
+ }
+
+ // In a normal case, when the disconnect is not called, we should not
+ // set the camera device to null, since on return callback, we try to
+ // enable 3A locks, which need the cameradevice.
+ public synchronized void stopCameraPreview() {
+ if (mLogVerbose) Log.v(TAG, "Stopping camera preview.");
+ if (mCameraDevice == null) {
+ Log.d(TAG, "Camera already null. Nothing to disconnect");
+ return;
+ }
+ mCameraDevice.stopPreview();
+ mCameraDevice.setPreviewTexture(null);
+ }
+
+ // Stop and release effect resources
+ public synchronized void stopPreview() {
+ if (mLogVerbose) Log.v(TAG, "Stopping preview (" + this + ")");
+ switch (mState) {
+ case STATE_CONFIGURE:
+ Log.w(TAG, "StopPreview called when preview not active!");
+ return;
+ case STATE_RELEASED:
+ throw new RuntimeException("stopPreview called on released EffectsRecorder!");
+ default:
+ break;
+ }
+
+ if (mState == STATE_RECORD) {
+ stopRecording();
+ }
+
+ mCurrentEffect = EFFECT_NONE;
+
+ // This will not do anything if the camera has already been disconnected.
+ stopCameraPreview();
+
+ mState = STATE_CONFIGURE;
+ mOldRunner = mRunner;
+ invoke(mRunner, sGraphRunnerStop);
+ mRunner = null;
+ // Rest of stop and release handled in mRunnerDoneCallback
+ }
+
+ // Try to enable/disable video stabilization if supported; otherwise return false
+ // It is called from a synchronized block.
+ boolean tryEnableVideoStabilization(boolean toggle) {
+ if (mLogVerbose) Log.v(TAG, "tryEnableVideoStabilization.");
+ if (mCameraDevice == null) {
+ Log.d(TAG, "Camera already null. Not enabling video stabilization.");
+ return false;
+ }
+ Camera.Parameters params = mCameraDevice.getParameters();
+
+ String vstabSupported = params.get("video-stabilization-supported");
+ if ("true".equals(vstabSupported)) {
+ if (mLogVerbose) Log.v(TAG, "Setting video stabilization to " + toggle);
+ params.set("video-stabilization", toggle ? "true" : "false");
+ mCameraDevice.setParameters(params);
+ return true;
+ }
+ if (mLogVerbose) Log.v(TAG, "Video stabilization not supported");
+ return false;
+ }
+
+ // Try to enable/disable 3A locks if supported; otherwise return false
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ synchronized boolean tryEnable3ALocks(boolean toggle) {
+ if (mLogVerbose) Log.v(TAG, "tryEnable3ALocks");
+ if (mCameraDevice == null) {
+ Log.d(TAG, "Camera already null. Not tryenabling 3A locks.");
+ return false;
+ }
+ Camera.Parameters params = mCameraDevice.getParameters();
+ if (Util.isAutoExposureLockSupported(params) &&
+ Util.isAutoWhiteBalanceLockSupported(params)) {
+ params.setAutoExposureLock(toggle);
+ params.setAutoWhiteBalanceLock(toggle);
+ mCameraDevice.setParameters(params);
+ return true;
+ }
+ return false;
+ }
+
+ // Try to enable/disable 3A locks if supported; otherwise, throw error
+ // Use this when locks are essential to success
+ synchronized void enable3ALocks(boolean toggle) {
+ if (mLogVerbose) Log.v(TAG, "Enable3ALocks");
+ if (mCameraDevice == null) {
+ Log.d(TAG, "Camera already null. Not enabling 3A locks.");
+ return;
+ }
+ Camera.Parameters params = mCameraDevice.getParameters();
+ if (!tryEnable3ALocks(toggle)) {
+ throw new RuntimeException("Attempt to lock 3A on camera with no locking support!");
+ }
+ }
+
+ static class SerializableInvocationHandler
+ implements InvocationHandler, Serializable {
+ private final int mEffectsRecorderIndex;
+ public SerializableInvocationHandler(int index) {
+ mEffectsRecorderIndex = index;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+ if (sEffectsRecorder == null) return null;
+ if (mEffectsRecorderIndex != sEffectsRecorderIndex) {
+ Log.v(TAG, "Ignore old callback " + mEffectsRecorderIndex);
+ return null;
+ }
+ if (method.equals(sObjectEquals)) {
+ return sEffectsRecorder.invokeObjectEquals(proxy, args);
+ } else if (method.equals(sObjectToString)) {
+ return sEffectsRecorder.invokeObjectToString();
+ } else if (method.equals(sLearningDoneListenerOnLearningDone)) {
+ sEffectsRecorder.invokeOnLearningDone();
+ } else if (method.equals(sOnRunnerDoneListenerOnRunnerDone)) {
+ sEffectsRecorder.invokeOnRunnerDone(args);
+ } else if (method.equals(
+ sSurfaceTextureSourceListenerOnSurfaceTextureSourceReady)) {
+ sEffectsRecorder.invokeOnSurfaceTextureSourceReady(args);
+ } else if (method.equals(sOnRecordingDoneListenerOnRecordingDone)) {
+ sEffectsRecorder.invokeOnRecordingDone();
+ }
+ return null;
+ }
+ }
+
+ // Indicates that all camera/recording activity needs to halt
+ public synchronized void release() {
+ if (mLogVerbose) Log.v(TAG, "Releasing (" + this + ")");
+
+ switch (mState) {
+ case STATE_RECORD:
+ case STATE_STARTING_PREVIEW:
+ case STATE_PREVIEW:
+ stopPreview();
+ // Fall-through
+ default:
+ if (mSoundPlayer != null) {
+ mSoundPlayer.release();
+ mSoundPlayer = null;
+ }
+ mState = STATE_RELEASED;
+ break;
+ }
+ sEffectsRecorder = null;
+ }
+
+ private void sendMessage(final int effect, final int msg) {
+ if (mEffectsListener != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mEffectsListener.onEffectsUpdate(effect, msg);
+ }
+ });
+ }
+ }
+
+ private void raiseError(final Exception exception) {
+ if (mEffectsListener != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mFd != null) {
+ mEffectsListener.onEffectsError(exception, null);
+ } else {
+ mEffectsListener.onEffectsError(exception, mOutputFile);
+ }
+ }
+ });
+ }
+ }
+
+ // invoke method on receiver with no arguments
+ private Object invoke(Object receiver, Method method) {
+ try {
+ return method.invoke(receiver);
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ // invoke method on receiver with arguments
+ private Object invoke(Object receiver, Method method, Object[] args) {
+ try {
+ return method.invoke(receiver, args);
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private void setInputValue(Object receiver, String key, Object value) {
+ try {
+ sFilterSetInputValue.invoke(receiver, new Object[] {key, value});
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private Object newInstance(Constructor<?> ct, Object[] initArgs) {
+ try {
+ return ct.newInstance(initArgs);
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private Object newInstance(Constructor<?> ct) {
+ try {
+ return ct.newInstance();
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private Object getGraphFilter(Object receiver, String name) {
+ try {
+ return sFilterGraphGetFilter.invoke(sGraphRunnerGetGraph
+ .invoke(receiver), new Object[] {name});
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private Object getContextGLEnvironment(Object receiver) {
+ try {
+ return sFilterContextGetGLEnvironment
+ .invoke(sGraphEnvironmentGetContext.invoke(receiver));
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private void getGraphTearDown(Object receiver, Object filterContext) {
+ try {
+ sFilterGraphTearDown.invoke(sGraphRunnerGetGraph.invoke(receiver),
+ new Object[]{filterContext});
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private Object getConstant(Class<?> cls, String name) {
+ try {
+ return cls.getDeclaredField(name).get(null);
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+}
diff --git a/src/com/android/camera/Exif.java b/src/com/android/camera/Exif.java
new file mode 100644
index 000000000..c6ec6af50
--- /dev/null
+++ b/src/com/android/camera/Exif.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.camera;
+
+import android.util.Log;
+
+import com.android.gallery3d.exif.ExifInterface;
+
+import java.io.IOException;
+
+public class Exif {
+ private static final String TAG = "CameraExif";
+
+ public static ExifInterface getExif(byte[] jpegData) {
+ ExifInterface exif = new ExifInterface();
+ try {
+ exif.readExif(jpegData);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to read EXIF data", e);
+ }
+ return exif;
+ }
+
+ // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
+ public static int getOrientation(ExifInterface exif) {
+ Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+ if (val == null) {
+ return 0;
+ } else {
+ return ExifInterface.getRotationForOrientationValue(val.shortValue());
+ }
+ }
+
+ public static int getOrientation(byte[] jpegData) {
+ if (jpegData == null) return 0;
+
+ ExifInterface exif = getExif(jpegData);
+ return getOrientation(exif);
+ }
+}
diff --git a/src/com/android/camera/FocusOverlayManager.java b/src/com/android/camera/FocusOverlayManager.java
new file mode 100644
index 000000000..8bcb52fe5
--- /dev/null
+++ b/src/com/android/camera/FocusOverlayManager.java
@@ -0,0 +1,558 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.annotation.TargetApi;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.hardware.Camera.Area;
+import android.hardware.Camera.Parameters;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/* A class that handles everything about focus in still picture mode.
+ * This also handles the metering area because it is the same as focus area.
+ *
+ * The test cases:
+ * (1) The camera has continuous autofocus. Move the camera. Take a picture when
+ * CAF is not in progress.
+ * (2) The camera has continuous autofocus. Move the camera. Take a picture when
+ * CAF is in progress.
+ * (3) The camera has face detection. Point the camera at some faces. Hold the
+ * shutter. Release to take a picture.
+ * (4) The camera has face detection. Point the camera at some faces. Single tap
+ * the shutter to take a picture.
+ * (5) The camera has autofocus. Single tap the shutter to take a picture.
+ * (6) The camera has autofocus. Hold the shutter. Release to take a picture.
+ * (7) The camera has no autofocus. Single tap the shutter and take a picture.
+ * (8) The camera has autofocus and supports focus area. Touch the screen to
+ * trigger autofocus. Take a picture.
+ * (9) The camera has autofocus and supports focus area. Touch the screen to
+ * trigger autofocus. Wait until it times out.
+ * (10) The camera has no autofocus and supports metering area. Touch the screen
+ * to change metering area.
+ */
+public class FocusOverlayManager {
+ private static final String TAG = "CAM_FocusManager";
+
+ private static final int RESET_TOUCH_FOCUS = 0;
+ private static final int RESET_TOUCH_FOCUS_DELAY = 3000;
+
+ private int mState = STATE_IDLE;
+ private static final int STATE_IDLE = 0; // Focus is not active.
+ private static final int STATE_FOCUSING = 1; // Focus is in progress.
+ // Focus is in progress and the camera should take a picture after focus finishes.
+ private static final int STATE_FOCUSING_SNAP_ON_FINISH = 2;
+ private static final int STATE_SUCCESS = 3; // Focus finishes and succeeds.
+ private static final int STATE_FAIL = 4; // Focus finishes and fails.
+
+ private boolean mInitialized;
+ private boolean mFocusAreaSupported;
+ private boolean mMeteringAreaSupported;
+ private boolean mLockAeAwbNeeded;
+ private boolean mAeAwbLock;
+ private Matrix mMatrix;
+
+ private int mPreviewWidth; // The width of the preview frame layout.
+ private int mPreviewHeight; // The height of the preview frame layout.
+ private boolean mMirror; // true if the camera is front-facing.
+ private int mDisplayOrientation;
+ private List<Object> mFocusArea; // focus area in driver format
+ private List<Object> mMeteringArea; // metering area in driver format
+ private String mFocusMode;
+ private String[] mDefaultFocusModes;
+ private String mOverrideFocusMode;
+ private Parameters mParameters;
+ private ComboPreferences mPreferences;
+ private Handler mHandler;
+ Listener mListener;
+ private boolean mPreviousMoving;
+ private boolean mFocusDefault;
+
+ private FocusUI mUI;
+
+ public interface FocusUI {
+ public boolean hasFaces();
+ public void clearFocus();
+ public void setFocusPosition(int x, int y);
+ public void onFocusStarted();
+ public void onFocusSucceeded(boolean timeOut);
+ public void onFocusFailed(boolean timeOut);
+ public void pauseFaceDetection();
+ public void resumeFaceDetection();
+ }
+
+ public interface Listener {
+ public void autoFocus();
+ public void cancelAutoFocus();
+ public boolean capture();
+ public void startFaceDetection();
+ public void stopFaceDetection();
+ public void setFocusParameters();
+ }
+
+ private class MainHandler extends Handler {
+ public MainHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case RESET_TOUCH_FOCUS: {
+ cancelAutoFocus();
+ mListener.startFaceDetection();
+ break;
+ }
+ }
+ }
+ }
+
+ public FocusOverlayManager(ComboPreferences preferences, String[] defaultFocusModes,
+ Parameters parameters, Listener listener,
+ boolean mirror, Looper looper, FocusUI ui) {
+ mHandler = new MainHandler(looper);
+ mMatrix = new Matrix();
+ mPreferences = preferences;
+ mDefaultFocusModes = defaultFocusModes;
+ setParameters(parameters);
+ mListener = listener;
+ setMirror(mirror);
+ mFocusDefault = true;
+ mUI = ui;
+ }
+
+ public void setParameters(Parameters parameters) {
+ // parameters can only be null when onConfigurationChanged is called
+ // before camera is open. We will just return in this case, because
+ // parameters will be set again later with the right parameters after
+ // camera is open.
+ if (parameters == null) return;
+ mParameters = parameters;
+ mFocusAreaSupported = Util.isFocusAreaSupported(parameters);
+ mMeteringAreaSupported = Util.isMeteringAreaSupported(parameters);
+ mLockAeAwbNeeded = (Util.isAutoExposureLockSupported(mParameters) ||
+ Util.isAutoWhiteBalanceLockSupported(mParameters));
+ }
+
+ public void setPreviewSize(int previewWidth, int previewHeight) {
+ if (mPreviewWidth != previewWidth || mPreviewHeight != previewHeight) {
+ mPreviewWidth = previewWidth;
+ mPreviewHeight = previewHeight;
+ setMatrix();
+ }
+ }
+
+ public void setMirror(boolean mirror) {
+ mMirror = mirror;
+ setMatrix();
+ }
+
+ public void setDisplayOrientation(int displayOrientation) {
+ mDisplayOrientation = displayOrientation;
+ setMatrix();
+ }
+
+ private void setMatrix() {
+ if (mPreviewWidth != 0 && mPreviewHeight != 0) {
+ Matrix matrix = new Matrix();
+ Util.prepareMatrix(matrix, mMirror, mDisplayOrientation,
+ mPreviewWidth, mPreviewHeight);
+ // In face detection, the matrix converts the driver coordinates to UI
+ // coordinates. In tap focus, the inverted matrix converts the UI
+ // coordinates to driver coordinates.
+ matrix.invert(mMatrix);
+ mInitialized = true;
+ }
+ }
+
+ private void lockAeAwbIfNeeded() {
+ if (mLockAeAwbNeeded && !mAeAwbLock) {
+ mAeAwbLock = true;
+ mListener.setFocusParameters();
+ }
+ }
+
+ private void unlockAeAwbIfNeeded() {
+ if (mLockAeAwbNeeded && mAeAwbLock && (mState != STATE_FOCUSING_SNAP_ON_FINISH)) {
+ mAeAwbLock = false;
+ mListener.setFocusParameters();
+ }
+ }
+
+ public void onShutterDown() {
+ if (!mInitialized) return;
+
+ boolean autoFocusCalled = false;
+ if (needAutoFocusCall()) {
+ // Do not focus if touch focus has been triggered.
+ if (mState != STATE_SUCCESS && mState != STATE_FAIL) {
+ autoFocus();
+ autoFocusCalled = true;
+ }
+ }
+
+ if (!autoFocusCalled) lockAeAwbIfNeeded();
+ }
+
+ public void onShutterUp() {
+ if (!mInitialized) return;
+
+ if (needAutoFocusCall()) {
+ // User releases half-pressed focus key.
+ if (mState == STATE_FOCUSING || mState == STATE_SUCCESS
+ || mState == STATE_FAIL) {
+ cancelAutoFocus();
+ }
+ }
+
+ // Unlock AE and AWB after cancelAutoFocus. Camera API does not
+ // guarantee setParameters can be called during autofocus.
+ unlockAeAwbIfNeeded();
+ }
+
+ public void doSnap() {
+ if (!mInitialized) return;
+
+ // If the user has half-pressed the shutter and focus is completed, we
+ // can take the photo right away. If the focus mode is infinity, we can
+ // also take the photo.
+ if (!needAutoFocusCall() || (mState == STATE_SUCCESS || mState == STATE_FAIL)) {
+ capture();
+ } else if (mState == STATE_FOCUSING) {
+ // Half pressing the shutter (i.e. the focus button event) will
+ // already have requested AF for us, so just request capture on
+ // focus here.
+ mState = STATE_FOCUSING_SNAP_ON_FINISH;
+ } else if (mState == STATE_IDLE) {
+ // We didn't do focus. This can happen if the user press focus key
+ // while the snapshot is still in progress. The user probably wants
+ // the next snapshot as soon as possible, so we just do a snapshot
+ // without focusing again.
+ capture();
+ }
+ }
+
+ public void onAutoFocus(boolean focused, boolean shutterButtonPressed) {
+ if (mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+ // Take the picture no matter focus succeeds or fails. No need
+ // to play the AF sound if we're about to play the shutter
+ // sound.
+ if (focused) {
+ mState = STATE_SUCCESS;
+ } else {
+ mState = STATE_FAIL;
+ }
+ updateFocusUI();
+ capture();
+ } else if (mState == STATE_FOCUSING) {
+ // This happens when (1) user is half-pressing the focus key or
+ // (2) touch focus is triggered. Play the focus tone. Do not
+ // take the picture now.
+ if (focused) {
+ mState = STATE_SUCCESS;
+ } else {
+ mState = STATE_FAIL;
+ }
+ updateFocusUI();
+ // If this is triggered by touch focus, cancel focus after a
+ // while.
+ if (!mFocusDefault) {
+ mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
+ }
+ if (shutterButtonPressed) {
+ // Lock AE & AWB so users can half-press shutter and recompose.
+ lockAeAwbIfNeeded();
+ }
+ } else if (mState == STATE_IDLE) {
+ // User has released the focus key before focus completes.
+ // Do nothing.
+ }
+ }
+
+ public void onAutoFocusMoving(boolean moving) {
+ if (!mInitialized) return;
+
+
+ // Ignore if the camera has detected some faces.
+ if (mUI.hasFaces()) {
+ mUI.clearFocus();
+ return;
+ }
+
+ // Ignore if we have requested autofocus. This method only handles
+ // continuous autofocus.
+ if (mState != STATE_IDLE) return;
+
+ // animate on false->true trasition only b/8219520
+ if (moving && !mPreviousMoving) {
+ mUI.onFocusStarted();
+ } else if (!moving) {
+ mUI.onFocusSucceeded(true);
+ }
+ mPreviousMoving = moving;
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void initializeFocusAreas(int x, int y) {
+ if (mFocusArea == null) {
+ mFocusArea = new ArrayList<Object>();
+ mFocusArea.add(new Area(new Rect(), 1));
+ }
+
+ // Convert the coordinates to driver format.
+ calculateTapArea(x, y, 1f, ((Area) mFocusArea.get(0)).rect);
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void initializeMeteringAreas(int x, int y) {
+ if (mMeteringArea == null) {
+ mMeteringArea = new ArrayList<Object>();
+ mMeteringArea.add(new Area(new Rect(), 1));
+ }
+
+ // Convert the coordinates to driver format.
+ // AE area is bigger because exposure is sensitive and
+ // easy to over- or underexposure if area is too small.
+ calculateTapArea(x, y, 1.5f, ((Area) mMeteringArea.get(0)).rect);
+ }
+
+ public void onSingleTapUp(int x, int y) {
+ if (!mInitialized || mState == STATE_FOCUSING_SNAP_ON_FINISH) return;
+
+ // Let users be able to cancel previous touch focus.
+ if ((!mFocusDefault) && (mState == STATE_FOCUSING ||
+ mState == STATE_SUCCESS || mState == STATE_FAIL)) {
+ cancelAutoFocus();
+ }
+ if (mPreviewWidth == 0 || mPreviewHeight == 0) return;
+ mFocusDefault = false;
+ // Initialize mFocusArea.
+ if (mFocusAreaSupported) {
+ initializeFocusAreas(x, y);
+ }
+ // Initialize mMeteringArea.
+ if (mMeteringAreaSupported) {
+ initializeMeteringAreas(x, y);
+ }
+
+ // Use margin to set the focus indicator to the touched area.
+ mUI.setFocusPosition(x, y);
+
+ // Stop face detection because we want to specify focus and metering area.
+ mListener.stopFaceDetection();
+
+ // Set the focus area and metering area.
+ mListener.setFocusParameters();
+ if (mFocusAreaSupported) {
+ autoFocus();
+ } else { // Just show the indicator in all other cases.
+ updateFocusUI();
+ // Reset the metering area in 3 seconds.
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
+ }
+ }
+
+ public void onPreviewStarted() {
+ mState = STATE_IDLE;
+ }
+
+ public void onPreviewStopped() {
+ // If auto focus was in progress, it would have been stopped.
+ mState = STATE_IDLE;
+ resetTouchFocus();
+ updateFocusUI();
+ }
+
+ public void onCameraReleased() {
+ onPreviewStopped();
+ }
+
+ private void autoFocus() {
+ Log.v(TAG, "Start autofocus.");
+ mListener.autoFocus();
+ mState = STATE_FOCUSING;
+ // Pause the face view because the driver will keep sending face
+ // callbacks after the focus completes.
+ mUI.pauseFaceDetection();
+ updateFocusUI();
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+
+ private void cancelAutoFocus() {
+ Log.v(TAG, "Cancel autofocus.");
+
+ // Reset the tap area before calling mListener.cancelAutofocus.
+ // Otherwise, focus mode stays at auto and the tap area passed to the
+ // driver is not reset.
+ resetTouchFocus();
+ mListener.cancelAutoFocus();
+ mUI.resumeFaceDetection();
+ mState = STATE_IDLE;
+ updateFocusUI();
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+
+ private void capture() {
+ if (mListener.capture()) {
+ mState = STATE_IDLE;
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+ }
+
+ public String getFocusMode() {
+ if (mOverrideFocusMode != null) return mOverrideFocusMode;
+ if (mParameters == null) return Parameters.FOCUS_MODE_AUTO;
+ List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
+
+ if (mFocusAreaSupported && !mFocusDefault) {
+ // Always use autofocus in tap-to-focus.
+ mFocusMode = Parameters.FOCUS_MODE_AUTO;
+ } else {
+ // The default is continuous autofocus.
+ mFocusMode = mPreferences.getString(
+ CameraSettings.KEY_FOCUS_MODE, null);
+
+ // Try to find a supported focus mode from the default list.
+ if (mFocusMode == null) {
+ for (int i = 0; i < mDefaultFocusModes.length; i++) {
+ String mode = mDefaultFocusModes[i];
+ if (Util.isSupported(mode, supportedFocusModes)) {
+ mFocusMode = mode;
+ break;
+ }
+ }
+ }
+ }
+ if (!Util.isSupported(mFocusMode, supportedFocusModes)) {
+ // For some reasons, the driver does not support the current
+ // focus mode. Fall back to auto.
+ if (Util.isSupported(Parameters.FOCUS_MODE_AUTO,
+ mParameters.getSupportedFocusModes())) {
+ mFocusMode = Parameters.FOCUS_MODE_AUTO;
+ } else {
+ mFocusMode = mParameters.getFocusMode();
+ }
+ }
+ return mFocusMode;
+ }
+
+ public List getFocusAreas() {
+ return mFocusArea;
+ }
+
+ public List getMeteringAreas() {
+ return mMeteringArea;
+ }
+
+ public void updateFocusUI() {
+ if (!mInitialized) return;
+ // Show only focus indicator or face indicator.
+
+ if (mState == STATE_IDLE) {
+ if (mFocusDefault) {
+ mUI.clearFocus();
+ } else {
+ // Users touch on the preview and the indicator represents the
+ // metering area. Either focus area is not supported or
+ // autoFocus call is not required.
+ mUI.onFocusStarted();
+ }
+ } else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+ mUI.onFocusStarted();
+ } else {
+ if (Util.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) {
+ // TODO: check HAL behavior and decide if this can be removed.
+ mUI.onFocusSucceeded(false);
+ } else if (mState == STATE_SUCCESS) {
+ mUI.onFocusSucceeded(false);
+ } else if (mState == STATE_FAIL) {
+ mUI.onFocusFailed(false);
+ }
+ }
+ }
+
+ public void resetTouchFocus() {
+ if (!mInitialized) return;
+
+ // Put focus indicator to the center. clear reset position
+ mUI.clearFocus();
+ // Initialize mFocusArea.
+ if (mFocusAreaSupported) {
+ initializeFocusAreas(mPreviewWidth / 2, mPreviewHeight / 2);
+ }
+ // Initialize mMeteringArea.
+ if (mMeteringAreaSupported) {
+ initializeMeteringAreas(mPreviewWidth / 2, mPreviewHeight / 2);
+ }
+ mFocusDefault = true;
+ }
+
+ private void calculateTapArea(int x, int y, float areaMultiple, Rect rect) {
+ int areaSize = (int) (Math.min(mPreviewWidth, mPreviewHeight) * areaMultiple / 20);
+ int left = Util.clamp(x - areaSize, 0, mPreviewWidth - 2 * areaSize);
+ int top = Util.clamp(y - areaSize, 0, mPreviewHeight - 2 * areaSize);
+
+ RectF rectF = new RectF(left, top, left + 2 * areaSize, top + 2 * areaSize);
+ mMatrix.mapRect(rectF);
+ Util.rectFToRect(rectF, rect);
+ }
+
+ /* package */ int getFocusState() {
+ return mState;
+ }
+
+ public boolean isFocusCompleted() {
+ return mState == STATE_SUCCESS || mState == STATE_FAIL;
+ }
+
+ public boolean isFocusingSnapOnFinish() {
+ return mState == STATE_FOCUSING_SNAP_ON_FINISH;
+ }
+
+ public void removeMessages() {
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+
+ public void overrideFocusMode(String focusMode) {
+ mOverrideFocusMode = focusMode;
+ }
+
+ public void setAeAwbLock(boolean lock) {
+ mAeAwbLock = lock;
+ }
+
+ public boolean getAeAwbLock() {
+ return mAeAwbLock;
+ }
+
+ private boolean needAutoFocusCall() {
+ String focusMode = getFocusMode();
+ return !(focusMode.equals(Parameters.FOCUS_MODE_INFINITY)
+ || focusMode.equals(Parameters.FOCUS_MODE_FIXED)
+ || focusMode.equals(Parameters.FOCUS_MODE_EDOF));
+ }
+}
diff --git a/src/com/android/camera/IconListPreference.java b/src/com/android/camera/IconListPreference.java
new file mode 100644
index 000000000..e5f75d3a5
--- /dev/null
+++ b/src/com/android/camera/IconListPreference.java
@@ -0,0 +1,117 @@
+/*
+ * 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.camera;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.R;
+
+import java.util.List;
+
+/** A {@code ListPreference} where each entry has a corresponding icon. */
+public class IconListPreference extends ListPreference {
+ private int mSingleIconId;
+ private int mIconIds[];
+ private int mLargeIconIds[];
+ private int mImageIds[];
+ private boolean mUseSingleIcon;
+
+ public IconListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.IconListPreference, 0, 0);
+ Resources res = context.getResources();
+ mSingleIconId = a.getResourceId(
+ R.styleable.IconListPreference_singleIcon, 0);
+ mIconIds = getIds(res, a.getResourceId(
+ R.styleable.IconListPreference_icons, 0));
+ mLargeIconIds = getIds(res, a.getResourceId(
+ R.styleable.IconListPreference_largeIcons, 0));
+ mImageIds = getIds(res, a.getResourceId(
+ R.styleable.IconListPreference_images, 0));
+ a.recycle();
+ }
+
+ public int getSingleIcon() {
+ return mSingleIconId;
+ }
+
+ public int[] getIconIds() {
+ return mIconIds;
+ }
+
+ public int[] getLargeIconIds() {
+ return mLargeIconIds;
+ }
+
+ public int[] getImageIds() {
+ return mImageIds;
+ }
+
+ public boolean getUseSingleIcon() {
+ return mUseSingleIcon;
+ }
+
+ public void setIconIds(int[] iconIds) {
+ mIconIds = iconIds;
+ }
+
+ public void setLargeIconIds(int[] largeIconIds) {
+ mLargeIconIds = largeIconIds;
+ }
+
+ public void setUseSingleIcon(boolean useSingle) {
+ mUseSingleIcon = useSingle;
+ }
+
+ private int[] getIds(Resources res, int iconsRes) {
+ if (iconsRes == 0) return null;
+ TypedArray array = res.obtainTypedArray(iconsRes);
+ int n = array.length();
+ int ids[] = new int[n];
+ for (int i = 0; i < n; ++i) {
+ ids[i] = array.getResourceId(i, 0);
+ }
+ array.recycle();
+ return ids;
+ }
+
+ @Override
+ public void filterUnsupported(List<String> supported) {
+ CharSequence entryValues[] = getEntryValues();
+ IntArray iconIds = new IntArray();
+ IntArray largeIconIds = new IntArray();
+ IntArray imageIds = new IntArray();
+
+ for (int i = 0, len = entryValues.length; i < len; i++) {
+ if (supported.indexOf(entryValues[i].toString()) >= 0) {
+ if (mIconIds != null) iconIds.add(mIconIds[i]);
+ if (mLargeIconIds != null) largeIconIds.add(mLargeIconIds[i]);
+ if (mImageIds != null) imageIds.add(mImageIds[i]);
+ }
+ }
+ if (mIconIds != null) mIconIds = iconIds.toArray(new int[iconIds.size()]);
+ if (mLargeIconIds != null) {
+ mLargeIconIds = largeIconIds.toArray(new int[largeIconIds.size()]);
+ }
+ if (mImageIds != null) mImageIds = imageIds.toArray(new int[imageIds.size()]);
+ super.filterUnsupported(supported);
+ }
+}
diff --git a/src/com/android/camera/ImageTaskManager.java b/src/com/android/camera/ImageTaskManager.java
new file mode 100644
index 000000000..1324942fd
--- /dev/null
+++ b/src/com/android/camera/ImageTaskManager.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 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.camera;
+
+import android.net.Uri;
+
+/**
+ * The interface for background image processing task manager.
+ */
+interface ImageTaskManager {
+
+ /**
+ * Callback interface for task events.
+ */
+ public interface TaskListener {
+ public void onTaskQueued(String filePath, Uri imageUri);
+ public void onTaskDone(String filePath, Uri imageUri);
+ public void onTaskProgress(
+ String filePath, Uri imageUri, int progress);
+ }
+
+ public void addTaskListener(TaskListener l);
+
+ public void removeTaskListener(TaskListener l);
+
+ /**
+ * Get task progress by Uri.
+ *
+ * @param uri The Uri of the final image file to identify the task.
+ * @return Integer from 0 to 100, or -1. The percentage of the task done
+ * so far. -1 means not found.
+ */
+ public int getTaskProgress(Uri uri);
+}
diff --git a/src/com/android/camera/IntArray.java b/src/com/android/camera/IntArray.java
new file mode 100644
index 000000000..a2550dbd8
--- /dev/null
+++ b/src/com/android/camera/IntArray.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.camera;
+
+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;
+ }
+}
diff --git a/src/com/android/camera/ListPreference.java b/src/com/android/camera/ListPreference.java
new file mode 100644
index 000000000..38866de9d
--- /dev/null
+++ b/src/com/android/camera/ListPreference.java
@@ -0,0 +1,202 @@
+/*
+ * 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.camera;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A type of <code>CameraPreference</code> whose number of possible values
+ * is limited.
+ */
+public class ListPreference extends CameraPreference {
+ private static final String TAG = "ListPreference";
+ private final String mKey;
+ private String mValue;
+ private final CharSequence[] mDefaultValues;
+
+ private CharSequence[] mEntries;
+ private CharSequence[] mEntryValues;
+ private CharSequence[] mLabels;
+ private boolean mLoaded = false;
+
+ public ListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.ListPreference, 0, 0);
+
+ mKey = Util.checkNotNull(
+ a.getString(R.styleable.ListPreference_key));
+
+ // We allow the defaultValue attribute to be a string or an array of
+ // strings. The reason we need multiple default values is that some
+ // of them may be unsupported on a specific platform (for example,
+ // continuous auto-focus). In that case the first supported value
+ // in the array will be used.
+ int attrDefaultValue = R.styleable.ListPreference_defaultValue;
+ TypedValue tv = a.peekValue(attrDefaultValue);
+ if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) {
+ mDefaultValues = a.getTextArray(attrDefaultValue);
+ } else {
+ mDefaultValues = new CharSequence[1];
+ mDefaultValues[0] = a.getString(attrDefaultValue);
+ }
+
+ setEntries(a.getTextArray(R.styleable.ListPreference_entries));
+ setEntryValues(a.getTextArray(
+ R.styleable.ListPreference_entryValues));
+ setLabels(a.getTextArray(
+ R.styleable.ListPreference_labelList));
+ a.recycle();
+ }
+
+ public String getKey() {
+ return mKey;
+ }
+
+ public CharSequence[] getEntries() {
+ return mEntries;
+ }
+
+ public CharSequence[] getEntryValues() {
+ return mEntryValues;
+ }
+
+ public CharSequence[] getLabels() {
+ return mLabels;
+ }
+
+ public void setEntries(CharSequence entries[]) {
+ mEntries = entries == null ? new CharSequence[0] : entries;
+ }
+
+ public void setEntryValues(CharSequence values[]) {
+ mEntryValues = values == null ? new CharSequence[0] : values;
+ }
+
+ public void setLabels(CharSequence labels[]) {
+ mLabels = labels == null ? new CharSequence[0] : labels;
+ }
+
+ public String getValue() {
+ if (!mLoaded) {
+ mValue = getSharedPreferences().getString(mKey,
+ findSupportedDefaultValue());
+ mLoaded = true;
+ }
+ return mValue;
+ }
+
+ // Find the first value in mDefaultValues which is supported.
+ private String findSupportedDefaultValue() {
+ for (int i = 0; i < mDefaultValues.length; i++) {
+ for (int j = 0; j < mEntryValues.length; j++) {
+ // Note that mDefaultValues[i] may be null (if unspecified
+ // in the xml file).
+ if (mEntryValues[j].equals(mDefaultValues[i])) {
+ return mDefaultValues[i].toString();
+ }
+ }
+ }
+ return null;
+ }
+
+ public void setValue(String value) {
+ if (findIndexOfValue(value) < 0) throw new IllegalArgumentException();
+ mValue = value;
+ persistStringValue(value);
+ }
+
+ public void setValueIndex(int index) {
+ setValue(mEntryValues[index].toString());
+ }
+
+ public int findIndexOfValue(String value) {
+ for (int i = 0, n = mEntryValues.length; i < n; ++i) {
+ if (Util.equals(mEntryValues[i], value)) return i;
+ }
+ return -1;
+ }
+
+ public int getCurrentIndex() {
+ return findIndexOfValue(getValue());
+ }
+
+ public String getEntry() {
+ return mEntries[findIndexOfValue(getValue())].toString();
+ }
+
+ public String getLabel() {
+ return mLabels[findIndexOfValue(getValue())].toString();
+ }
+
+ protected void persistStringValue(String value) {
+ SharedPreferences.Editor editor = getSharedPreferences().edit();
+ editor.putString(mKey, value);
+ editor.apply();
+ }
+
+ @Override
+ public void reloadValue() {
+ this.mLoaded = false;
+ }
+
+ public void filterUnsupported(List<String> supported) {
+ ArrayList<CharSequence> entries = new ArrayList<CharSequence>();
+ ArrayList<CharSequence> entryValues = new ArrayList<CharSequence>();
+ for (int i = 0, len = mEntryValues.length; i < len; i++) {
+ if (supported.indexOf(mEntryValues[i].toString()) >= 0) {
+ entries.add(mEntries[i]);
+ entryValues.add(mEntryValues[i]);
+ }
+ }
+ int size = entries.size();
+ mEntries = entries.toArray(new CharSequence[size]);
+ mEntryValues = entryValues.toArray(new CharSequence[size]);
+ }
+
+ public void filterDuplicated() {
+ ArrayList<CharSequence> entries = new ArrayList<CharSequence>();
+ ArrayList<CharSequence> entryValues = new ArrayList<CharSequence>();
+ for (int i = 0, len = mEntryValues.length; i < len; i++) {
+ if (!entries.contains(mEntries[i])) {
+ entries.add(mEntries[i]);
+ entryValues.add(mEntryValues[i]);
+ }
+ }
+ int size = entries.size();
+ mEntries = entries.toArray(new CharSequence[size]);
+ mEntryValues = entryValues.toArray(new CharSequence[size]);
+ }
+
+ public void print() {
+ Log.v(TAG, "Preference key=" + getKey() + ". value=" + getValue());
+ for (int i = 0; i < mEntryValues.length; i++) {
+ Log.v(TAG, "entryValues[" + i + "]=" + mEntryValues[i]);
+ }
+ }
+}
diff --git a/src/com/android/camera/LocationManager.java b/src/com/android/camera/LocationManager.java
new file mode 100644
index 000000000..fcf21b60f
--- /dev/null
+++ b/src/com/android/camera/LocationManager.java
@@ -0,0 +1,181 @@
+/*
+ * 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.camera;
+
+import android.content.Context;
+import android.location.Location;
+import android.location.LocationProvider;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * A class that handles everything about location.
+ */
+public class LocationManager {
+ private static final String TAG = "LocationManager";
+
+ private Context mContext;
+ private Listener mListener;
+ private android.location.LocationManager mLocationManager;
+ private boolean mRecordLocation;
+
+ LocationListener [] mLocationListeners = new LocationListener[] {
+ new LocationListener(android.location.LocationManager.GPS_PROVIDER),
+ new LocationListener(android.location.LocationManager.NETWORK_PROVIDER)
+ };
+
+ public interface Listener {
+ public void showGpsOnScreenIndicator(boolean hasSignal);
+ public void hideGpsOnScreenIndicator();
+ }
+
+ public LocationManager(Context context, Listener listener) {
+ mContext = context;
+ mListener = listener;
+ }
+
+ public Location getCurrentLocation() {
+ if (!mRecordLocation) return null;
+
+ // go in best to worst order
+ for (int i = 0; i < mLocationListeners.length; i++) {
+ Location l = mLocationListeners[i].current();
+ if (l != null) return l;
+ }
+ Log.d(TAG, "No location received yet.");
+ return null;
+ }
+
+ public void recordLocation(boolean recordLocation) {
+ if (mRecordLocation != recordLocation) {
+ mRecordLocation = recordLocation;
+ if (recordLocation) {
+ startReceivingLocationUpdates();
+ } else {
+ stopReceivingLocationUpdates();
+ }
+ }
+ }
+
+ private void startReceivingLocationUpdates() {
+ if (mLocationManager == null) {
+ mLocationManager = (android.location.LocationManager)
+ mContext.getSystemService(Context.LOCATION_SERVICE);
+ }
+ if (mLocationManager != null) {
+ try {
+ mLocationManager.requestLocationUpdates(
+ android.location.LocationManager.NETWORK_PROVIDER,
+ 1000,
+ 0F,
+ mLocationListeners[1]);
+ } catch (SecurityException ex) {
+ Log.i(TAG, "fail to request location update, ignore", ex);
+ } catch (IllegalArgumentException ex) {
+ Log.d(TAG, "provider does not exist " + ex.getMessage());
+ }
+ try {
+ mLocationManager.requestLocationUpdates(
+ android.location.LocationManager.GPS_PROVIDER,
+ 1000,
+ 0F,
+ mLocationListeners[0]);
+ if (mListener != null) mListener.showGpsOnScreenIndicator(false);
+ } catch (SecurityException ex) {
+ Log.i(TAG, "fail to request location update, ignore", ex);
+ } catch (IllegalArgumentException ex) {
+ Log.d(TAG, "provider does not exist " + ex.getMessage());
+ }
+ Log.d(TAG, "startReceivingLocationUpdates");
+ }
+ }
+
+ private void stopReceivingLocationUpdates() {
+ if (mLocationManager != null) {
+ for (int i = 0; i < mLocationListeners.length; i++) {
+ try {
+ mLocationManager.removeUpdates(mLocationListeners[i]);
+ } catch (Exception ex) {
+ Log.i(TAG, "fail to remove location listners, ignore", ex);
+ }
+ }
+ Log.d(TAG, "stopReceivingLocationUpdates");
+ }
+ if (mListener != null) mListener.hideGpsOnScreenIndicator();
+ }
+
+ private class LocationListener
+ implements android.location.LocationListener {
+ Location mLastLocation;
+ boolean mValid = false;
+ String mProvider;
+
+ public LocationListener(String provider) {
+ mProvider = provider;
+ mLastLocation = new Location(mProvider);
+ }
+
+ @Override
+ public void onLocationChanged(Location newLocation) {
+ if (newLocation.getLatitude() == 0.0
+ && newLocation.getLongitude() == 0.0) {
+ // Hack to filter out 0.0,0.0 locations
+ return;
+ }
+ // If GPS is available before start camera, we won't get status
+ // update so update GPS indicator when we receive data.
+ if (mListener != null && mRecordLocation &&
+ android.location.LocationManager.GPS_PROVIDER.equals(mProvider)) {
+ mListener.showGpsOnScreenIndicator(true);
+ }
+ if (!mValid) {
+ Log.d(TAG, "Got first location.");
+ }
+ mLastLocation.set(newLocation);
+ mValid = true;
+ }
+
+ @Override
+ public void onProviderEnabled(String provider) {
+ }
+
+ @Override
+ public void onProviderDisabled(String provider) {
+ mValid = false;
+ }
+
+ @Override
+ public void onStatusChanged(
+ String provider, int status, Bundle extras) {
+ switch(status) {
+ case LocationProvider.OUT_OF_SERVICE:
+ case LocationProvider.TEMPORARILY_UNAVAILABLE: {
+ mValid = false;
+ if (mListener != null && mRecordLocation &&
+ android.location.LocationManager.GPS_PROVIDER.equals(provider)) {
+ mListener.showGpsOnScreenIndicator(false);
+ }
+ break;
+ }
+ }
+ }
+
+ public Location current() {
+ return mValid ? mLastLocation : null;
+ }
+ }
+}
diff --git a/src/com/android/camera/MediaSaveService.java b/src/com/android/camera/MediaSaveService.java
new file mode 100644
index 000000000..40675b8c0
--- /dev/null
+++ b/src/com/android/camera/MediaSaveService.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2013 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.camera;
+
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.location.Location;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.IBinder;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+
+import com.android.gallery3d.exif.ExifInterface;
+
+import java.io.File;
+
+/*
+ * Service for saving images in the background thread.
+ */
+public class MediaSaveService extends Service {
+ // The memory limit for unsaved image is 20MB.
+ private static final int SAVE_TASK_MEMORY_LIMIT = 20 * 1024 * 1024;
+ private static final String TAG = "CAM_" + MediaSaveService.class.getSimpleName();
+
+ private final IBinder mBinder = new LocalBinder();
+ private Listener mListener;
+ // Memory used by the total queued save request, in bytes.
+ private long mMemoryUse;
+
+ interface Listener {
+ public void onQueueStatus(boolean full);
+ }
+
+ interface OnMediaSavedListener {
+ public void onMediaSaved(Uri uri);
+ }
+
+ class LocalBinder extends Binder {
+ public MediaSaveService getService() {
+ return MediaSaveService.this;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flag, int startId) {
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ }
+
+ @Override
+ public void onCreate() {
+ mMemoryUse = 0;
+ }
+
+ public boolean isQueueFull() {
+ return (mMemoryUse >= SAVE_TASK_MEMORY_LIMIT);
+ }
+
+ public void addImage(final byte[] data, String title, long date, Location loc,
+ int width, int height, int orientation, ExifInterface exif,
+ OnMediaSavedListener l, ContentResolver resolver) {
+ if (isQueueFull()) {
+ Log.e(TAG, "Cannot add image when the queue is full");
+ return;
+ }
+ ImageSaveTask t = new ImageSaveTask(data, title, date,
+ (loc == null) ? null : new Location(loc),
+ width, height, orientation, exif, resolver, l);
+
+ mMemoryUse += data.length;
+ if (isQueueFull()) {
+ onQueueFull();
+ }
+ t.execute();
+ }
+
+ public void addImage(final byte[] data, String title, Location loc,
+ int width, int height, int orientation, ExifInterface exif,
+ OnMediaSavedListener l, ContentResolver resolver) {
+ addImage(data, title, System.currentTimeMillis(), loc, width, height,
+ orientation, exif, l, resolver);
+ }
+
+ public void addVideo(String path, long duration, ContentValues values,
+ OnMediaSavedListener l, ContentResolver resolver) {
+ // We don't set a queue limit for video saving because the file
+ // is already in the storage. Only updating the database.
+ new VideoSaveTask(path, duration, values, l, resolver).execute();
+ }
+
+ public void setListener(Listener l) {
+ mListener = l;
+ if (l == null) return;
+ l.onQueueStatus(isQueueFull());
+ }
+
+ private void onQueueFull() {
+ if (mListener != null) mListener.onQueueStatus(true);
+ }
+
+ private void onQueueAvailable() {
+ if (mListener != null) mListener.onQueueStatus(false);
+ }
+
+ private class ImageSaveTask extends AsyncTask <Void, Void, Uri> {
+ private byte[] data;
+ private String title;
+ private long date;
+ private Location loc;
+ private int width, height;
+ private int orientation;
+ private ExifInterface exif;
+ private ContentResolver resolver;
+ private OnMediaSavedListener listener;
+
+ public ImageSaveTask(byte[] data, String title, long date, Location loc,
+ int width, int height, int orientation, ExifInterface exif,
+ ContentResolver resolver, OnMediaSavedListener listener) {
+ this.data = data;
+ this.title = title;
+ this.date = date;
+ this.loc = loc;
+ this.width = width;
+ this.height = height;
+ this.orientation = orientation;
+ this.exif = exif;
+ this.resolver = resolver;
+ this.listener = listener;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ // do nothing.
+ }
+
+ @Override
+ protected Uri doInBackground(Void... v) {
+ return Storage.addImage(
+ resolver, title, date, loc, orientation, exif, data, width, height);
+ }
+
+ @Override
+ protected void onPostExecute(Uri uri) {
+ if (listener != null) listener.onMediaSaved(uri);
+ boolean previouslyFull = isQueueFull();
+ mMemoryUse -= data.length;
+ if (isQueueFull() != previouslyFull) onQueueAvailable();
+ }
+ }
+
+ private class VideoSaveTask extends AsyncTask <Void, Void, Uri> {
+ private String path;
+ private long duration;
+ private ContentValues values;
+ private OnMediaSavedListener listener;
+ private ContentResolver resolver;
+
+ public VideoSaveTask(String path, long duration, ContentValues values,
+ OnMediaSavedListener l, ContentResolver r) {
+ this.path = path;
+ this.duration = duration;
+ this.values = new ContentValues(values);
+ this.listener = l;
+ this.resolver = r;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ // do nothing.
+ }
+
+ @Override
+ protected Uri doInBackground(Void... v) {
+ values.put(Video.Media.SIZE, new File(path).length());
+ values.put(Video.Media.DURATION, duration);
+ Uri uri = null;
+ try {
+ Uri videoTable = Uri.parse("content://media/external/video/media");
+ uri = resolver.insert(videoTable, values);
+
+ // Rename the video file to the final name. This avoids other
+ // apps reading incomplete data. We need to do it after we are
+ // certain that the previous insert to MediaProvider is completed.
+ String finalName = values.getAsString(
+ Video.Media.DATA);
+ if (new File(path).renameTo(new File(finalName))) {
+ path = finalName;
+ }
+
+ resolver.update(uri, values, null, null);
+ } catch (Exception e) {
+ // We failed to insert into the database. This can happen if
+ // the SD card is unmounted.
+ Log.e(TAG, "failed to add video to media store", e);
+ uri = null;
+ } finally {
+ Log.v(TAG, "Current video URI: " + uri);
+ }
+ return uri;
+ }
+
+ @Override
+ protected void onPostExecute(Uri uri) {
+ if (listener != null) listener.onMediaSaved(uri);
+ }
+ }
+}
diff --git a/src/com/android/camera/OnClickAttr.java b/src/com/android/camera/OnClickAttr.java
new file mode 100644
index 000000000..07a10635b
--- /dev/null
+++ b/src/com/android/camera/OnClickAttr.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.camera;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+
+/**
+ * Interface for OnClickAttr annotation.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface OnClickAttr {
+}
diff --git a/src/com/android/camera/OnScreenHint.java b/src/com/android/camera/OnScreenHint.java
new file mode 100644
index 000000000..4d7fa7088
--- /dev/null
+++ b/src/com/android/camera/OnScreenHint.java
@@ -0,0 +1,190 @@
+/*
+ * 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.camera;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+/**
+ * A on-screen hint is a view containing a little message for the user and will
+ * be shown on the screen continuously. This class helps you create and show
+ * those.
+ *
+ * <p>
+ * When the view is shown to the user, appears as a floating view over the
+ * application.
+ * <p>
+ * The easiest way to use this class is to call one of the static methods that
+ * constructs everything you need and returns a new {@code OnScreenHint} object.
+ */
+public class OnScreenHint {
+ static final String TAG = "OnScreenHint";
+
+ int mGravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
+ int mX, mY;
+ float mHorizontalMargin;
+ float mVerticalMargin;
+ View mView;
+ View mNextView;
+
+ private final WindowManager.LayoutParams mParams =
+ new WindowManager.LayoutParams();
+ private final WindowManager mWM;
+ private final Handler mHandler = new Handler();
+
+ /**
+ * Construct an empty OnScreenHint object.
+ *
+ * @param context The context to use. Usually your
+ * {@link android.app.Application} or
+ * {@link android.app.Activity} object.
+ */
+ private OnScreenHint(Context context) {
+ mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ mY = context.getResources().getDimensionPixelSize(
+ R.dimen.hint_y_offset);
+
+ mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+ mParams.format = PixelFormat.TRANSLUCENT;
+ mParams.windowAnimations = R.style.Animation_OnScreenHint;
+ mParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+ mParams.setTitle("OnScreenHint");
+ }
+
+ /**
+ * Show the view on the screen.
+ */
+ public void show() {
+ if (mNextView == null) {
+ throw new RuntimeException("View is not initialized");
+ }
+ mHandler.post(mShow);
+ }
+
+ /**
+ * Close the view if it's showing.
+ */
+ public void cancel() {
+ mHandler.post(mHide);
+ }
+
+ /**
+ * Make a standard hint that just contains a text view.
+ *
+ * @param context The context to use. Usually your
+ * {@link android.app.Application} or
+ * {@link android.app.Activity} object.
+ * @param text The text to show. Can be formatted text.
+ *
+ */
+ public static OnScreenHint makeText(Context context, CharSequence text) {
+ OnScreenHint result = new OnScreenHint(context);
+
+ LayoutInflater inflate =
+ (LayoutInflater) context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ View v = inflate.inflate(R.layout.on_screen_hint, null);
+ TextView tv = (TextView) v.findViewById(R.id.message);
+ tv.setText(text);
+
+ result.mNextView = v;
+
+ return result;
+ }
+
+ /**
+ * Update the text in a OnScreenHint that was previously created using one
+ * of the makeText() methods.
+ * @param s The new text for the OnScreenHint.
+ */
+ public void setText(CharSequence s) {
+ if (mNextView == null) {
+ throw new RuntimeException("This OnScreenHint was not "
+ + "created with OnScreenHint.makeText()");
+ }
+ TextView tv = (TextView) mNextView.findViewById(R.id.message);
+ if (tv == null) {
+ throw new RuntimeException("This OnScreenHint was not "
+ + "created with OnScreenHint.makeText()");
+ }
+ tv.setText(s);
+ }
+
+ private synchronized void handleShow() {
+ if (mView != mNextView) {
+ // remove the old view if necessary
+ handleHide();
+ mView = mNextView;
+ final int gravity = mGravity;
+ mParams.gravity = gravity;
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK)
+ == Gravity.FILL_HORIZONTAL) {
+ mParams.horizontalWeight = 1.0f;
+ }
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK)
+ == Gravity.FILL_VERTICAL) {
+ mParams.verticalWeight = 1.0f;
+ }
+ mParams.x = mX;
+ mParams.y = mY;
+ mParams.verticalMargin = mVerticalMargin;
+ mParams.horizontalMargin = mHorizontalMargin;
+ if (mView.getParent() != null) {
+ mWM.removeView(mView);
+ }
+ mWM.addView(mView, mParams);
+ }
+ }
+
+ private synchronized void handleHide() {
+ if (mView != null) {
+ // note: checking parent() just to make sure the view has
+ // been added... i have seen cases where we get here when
+ // the view isn't yet added, so let's try not to crash.
+ if (mView.getParent() != null) {
+ mWM.removeView(mView);
+ }
+ mView = null;
+ }
+ }
+
+ private final Runnable mShow = new Runnable() {
+ @Override
+ public void run() {
+ handleShow();
+ }
+ };
+
+ private final Runnable mHide = new Runnable() {
+ @Override
+ public void run() {
+ handleHide();
+ }
+ };
+}
+
diff --git a/src/com/android/camera/OnScreenIndicators.java b/src/com/android/camera/OnScreenIndicators.java
new file mode 100644
index 000000000..77c8fafc0
--- /dev/null
+++ b/src/com/android/camera/OnScreenIndicators.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2013 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.camera;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.hardware.Camera;
+import android.hardware.Camera.Parameters;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.gallery3d.R;
+
+/**
+ * The on-screen indicators of the pie menu button. They show the camera
+ * settings in the viewfinder.
+ */
+public class OnScreenIndicators {
+ private final int[] mWBArray;
+ private final View mOnScreenIndicators;
+ private final ImageView mExposureIndicator;
+ private final ImageView mFlashIndicator;
+ private final ImageView mSceneIndicator;
+ private final ImageView mLocationIndicator;
+ private final ImageView mTimerIndicator;
+ private final ImageView mWBIndicator;
+
+ public OnScreenIndicators(Context ctx, View onScreenIndicatorsView) {
+ TypedArray iconIds = ctx.getResources().obtainTypedArray(
+ R.array.camera_wb_indicators);
+ final int n = iconIds.length();
+ mWBArray = new int[n];
+ for (int i = 0; i < n; i++) {
+ mWBArray[i] = iconIds.getResourceId(i, R.drawable.ic_indicator_wb_off);
+ }
+ mOnScreenIndicators = onScreenIndicatorsView;
+ mExposureIndicator = (ImageView) onScreenIndicatorsView.findViewById(
+ R.id.menu_exposure_indicator);
+ mFlashIndicator = (ImageView) onScreenIndicatorsView.findViewById(
+ R.id.menu_flash_indicator);
+ mSceneIndicator = (ImageView) onScreenIndicatorsView.findViewById(
+ R.id.menu_scenemode_indicator);
+ mLocationIndicator = (ImageView) onScreenIndicatorsView.findViewById(
+ R.id.menu_location_indicator);
+ mTimerIndicator = (ImageView) onScreenIndicatorsView.findViewById(
+ R.id.menu_timer_indicator);
+ mWBIndicator = (ImageView) onScreenIndicatorsView.findViewById(
+ R.id.menu_wb_indicator);
+ }
+
+ /**
+ * Resets all indicators to show the default values.
+ */
+ public void resetToDefault() {
+ updateExposureOnScreenIndicator(0);
+ updateFlashOnScreenIndicator(Parameters.FLASH_MODE_OFF);
+ updateSceneOnScreenIndicator(Parameters.SCENE_MODE_AUTO);
+ updateWBIndicator(2);
+ updateTimerIndicator(false);
+ updateLocationIndicator(false);
+ }
+
+ /**
+ * Sets the exposure indicator using exposure compensations step rounding.
+ */
+ public void updateExposureOnScreenIndicator(Camera.Parameters params, int value) {
+ if (mExposureIndicator == null) {
+ return;
+ }
+ float step = params.getExposureCompensationStep();
+ value = Math.round(value * step);
+ updateExposureOnScreenIndicator(value);
+ }
+
+ /**
+ * Set the exposure indicator to the given value.
+ *
+ * @param value Value between -3 and 3. If outside this range, 0 is used by
+ * default.
+ */
+ public void updateExposureOnScreenIndicator(int value) {
+ int id = 0;
+ switch(value) {
+ case -3:
+ id = R.drawable.ic_indicator_ev_n3;
+ break;
+ case -2:
+ id = R.drawable.ic_indicator_ev_n2;
+ break;
+ case -1:
+ id = R.drawable.ic_indicator_ev_n1;
+ break;
+ case 0:
+ id = R.drawable.ic_indicator_ev_0;
+ break;
+ case 1:
+ id = R.drawable.ic_indicator_ev_p1;
+ break;
+ case 2:
+ id = R.drawable.ic_indicator_ev_p2;
+ break;
+ case 3:
+ id = R.drawable.ic_indicator_ev_p3;
+ break;
+ }
+ mExposureIndicator.setImageResource(id);
+ }
+
+ public void updateWBIndicator(int wbIndex) {
+ if (mWBIndicator == null) return;
+ mWBIndicator.setImageResource(mWBArray[wbIndex]);
+ }
+
+ public void updateTimerIndicator(boolean on) {
+ if (mTimerIndicator == null) return;
+ mTimerIndicator.setImageResource(on ? R.drawable.ic_indicator_timer_on
+ : R.drawable.ic_indicator_timer_off);
+ }
+
+ public void updateLocationIndicator(boolean on) {
+ if (mLocationIndicator == null) return;
+ mLocationIndicator.setImageResource(on ? R.drawable.ic_indicator_loc_on
+ : R.drawable.ic_indicator_loc_off);
+ }
+
+ /**
+ * Set the flash indicator to the given value.
+ *
+ * @param value One of Parameters.FLASH_MODE_OFF,
+ * Parameters.FLASH_MODE_AUTO, Parameters.FLASH_MODE_ON.
+ */
+ public void updateFlashOnScreenIndicator(String value) {
+ if (mFlashIndicator == null) {
+ return;
+ }
+ if (value == null || Parameters.FLASH_MODE_OFF.equals(value)) {
+ mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off);
+ } else {
+ if (Parameters.FLASH_MODE_AUTO.equals(value)) {
+ mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_auto);
+ } else if (Parameters.FLASH_MODE_ON.equals(value)
+ || Parameters.FLASH_MODE_TORCH.equals(value)) {
+ mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_on);
+ } else {
+ mFlashIndicator.setImageResource(R.drawable.ic_indicator_flash_off);
+ }
+ }
+ }
+
+ /**
+ * Set the scene indicator depending on the given scene mode.
+ *
+ * @param value the current Parameters.SCENE_MODE_* value.
+ */
+ public void updateSceneOnScreenIndicator(String value) {
+ if (mSceneIndicator == null) {
+ return;
+ }
+ if ((value == null) || Parameters.SCENE_MODE_AUTO.equals(value)) {
+ mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_off);
+ } else if (Parameters.SCENE_MODE_HDR.equals(value)) {
+ mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_hdr);
+ } else {
+ mSceneIndicator.setImageResource(R.drawable.ic_indicator_sce_on);
+ }
+ }
+
+ /**
+ * Sets the visibility of all indicators.
+ *
+ * @param visibility View.VISIBLE, View.GONE etc.
+ */
+ public void setVisibility(int visibility) {
+ mOnScreenIndicators.setVisibility(visibility);
+ }
+}
diff --git a/src/com/android/camera/PhotoController.java b/src/com/android/camera/PhotoController.java
new file mode 100644
index 000000000..bc824d917
--- /dev/null
+++ b/src/com/android/camera/PhotoController.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2013 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.camera;
+
+import android.view.SurfaceHolder;
+import android.view.View;
+
+import com.android.camera.ShutterButton.OnShutterButtonListener;
+
+
+public interface PhotoController extends OnShutterButtonListener {
+
+ public static final int PREVIEW_STOPPED = 0;
+ public static final int IDLE = 1; // preview is active
+ // Focus is in progress. The exact focus state is in Focus.java.
+ public static final int FOCUSING = 2;
+ public static final int SNAPSHOT_IN_PROGRESS = 3;
+ // Switching between cameras.
+ public static final int SWITCHING_CAMERA = 4;
+
+ // returns the actual set zoom value
+ public int onZoomChanged(int requestedZoom);
+
+ public boolean isImageCaptureIntent();
+
+ public boolean isCameraIdle();
+
+ public void onCaptureDone();
+
+ public void onCaptureCancelled();
+
+ public void onCaptureRetake();
+
+ public void cancelAutoFocus();
+
+ public void stopPreview();
+
+ public int getCameraState();
+
+ public void onSingleTapUp(View view, int x, int y);
+
+ public void onSurfaceCreated(SurfaceHolder holder);
+
+ public void onCountDownFinished();
+
+ public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight);
+
+ public void updateCameraOrientation();
+
+ public void enableRecordingLocation(boolean enable);
+}
diff --git a/src/com/android/camera/PhotoMenu.java b/src/com/android/camera/PhotoMenu.java
new file mode 100644
index 000000000..6c1e2d085
--- /dev/null
+++ b/src/com/android/camera/PhotoMenu.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.content.res.Resources;
+import android.hardware.Camera.Parameters;
+
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.CountdownTimerPopup;
+import com.android.camera.ui.ListPrefSettingPopup;
+import com.android.camera.ui.PieItem;
+import com.android.camera.ui.PieItem.OnClickListener;
+import com.android.camera.ui.PieRenderer;
+import com.android.gallery3d.R;
+
+import java.util.Locale;
+
+public class PhotoMenu extends PieController
+ implements CountdownTimerPopup.Listener,
+ ListPrefSettingPopup.Listener {
+ private static String TAG = "CAM_photomenu";
+
+ private final String mSettingOff;
+
+ private PhotoUI mUI;
+ private AbstractSettingPopup mPopup;
+ private CameraActivity mActivity;
+
+ public PhotoMenu(CameraActivity activity, PhotoUI ui, PieRenderer pie) {
+ super(activity, pie);
+ mUI = ui;
+ mSettingOff = activity.getString(R.string.setting_off_value);
+ mActivity = activity;
+ }
+
+ public void initialize(PreferenceGroup group) {
+ super.initialize(group);
+ mPopup = null;
+ PieItem item = null;
+ final Resources res = mActivity.getResources();
+ Locale locale = res.getConfiguration().locale;
+ // the order is from left to right in the menu
+
+ // hdr
+ if (group.findPreference(CameraSettings.KEY_CAMERA_HDR) != null) {
+ item = makeSwitchItem(CameraSettings.KEY_CAMERA_HDR, true);
+ mRenderer.addItem(item);
+ }
+ // exposure compensation
+ if (group.findPreference(CameraSettings.KEY_EXPOSURE) != null) {
+ item = makeItem(CameraSettings.KEY_EXPOSURE);
+ item.setLabel(res.getString(R.string.pref_exposure_label));
+ mRenderer.addItem(item);
+ }
+ // more settings
+ PieItem more = makeItem(R.drawable.ic_settings_holo_light);
+ more.setLabel(res.getString(R.string.camera_menu_more_label));
+ mRenderer.addItem(more);
+ // flash
+ if (group.findPreference(CameraSettings.KEY_FLASH_MODE) != null) {
+ item = makeItem(CameraSettings.KEY_FLASH_MODE);
+ item.setLabel(res.getString(R.string.pref_camera_flashmode_label));
+ mRenderer.addItem(item);
+ }
+ // camera switcher
+ if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) {
+ item = makeSwitchItem(CameraSettings.KEY_CAMERA_ID, false);
+ final PieItem fitem = item;
+ item.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(PieItem item) {
+ // Find the index of next camera.
+ ListPreference pref = mPreferenceGroup
+ .findPreference(CameraSettings.KEY_CAMERA_ID);
+ if (pref != null) {
+ int index = pref.findIndexOfValue(pref.getValue());
+ CharSequence[] values = pref.getEntryValues();
+ index = (index + 1) % values.length;
+ pref.setValueIndex(index);
+ mListener.onCameraPickerClicked(index);
+ }
+ updateItem(fitem, CameraSettings.KEY_CAMERA_ID);
+ }
+ });
+ mRenderer.addItem(item);
+ }
+ // location
+ if (group.findPreference(CameraSettings.KEY_RECORD_LOCATION) != null) {
+ item = makeSwitchItem(CameraSettings.KEY_RECORD_LOCATION, true);
+ more.addItem(item);
+ if (mActivity.isSecureCamera()) {
+ // Prevent location preference from getting changed in secure camera mode
+ item.setEnabled(false);
+ }
+ }
+ // countdown timer
+ final ListPreference ctpref = group.findPreference(CameraSettings.KEY_TIMER);
+ final ListPreference beeppref = group.findPreference(CameraSettings.KEY_TIMER_SOUND_EFFECTS);
+ item = makeItem(R.drawable.ic_timer);
+ item.setLabel(res.getString(R.string.pref_camera_timer_title).toUpperCase(locale));
+ item.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(PieItem item) {
+ CountdownTimerPopup timerPopup = (CountdownTimerPopup) mActivity.getLayoutInflater().inflate(
+ R.layout.countdown_setting_popup, null, false);
+ timerPopup.initialize(ctpref, beeppref);
+ timerPopup.setSettingChangedListener(PhotoMenu.this);
+ mUI.dismissPopup();
+ mPopup = timerPopup;
+ mUI.showPopup(mPopup);
+ }
+ });
+ more.addItem(item);
+ // image size
+ item = makeItem(R.drawable.ic_imagesize);
+ final ListPreference sizePref = group.findPreference(CameraSettings.KEY_PICTURE_SIZE);
+ item.setLabel(res.getString(R.string.pref_camera_picturesize_title).toUpperCase(locale));
+ item.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(PieItem item) {
+ ListPrefSettingPopup popup = (ListPrefSettingPopup) mActivity.getLayoutInflater().inflate(
+ R.layout.list_pref_setting_popup, null, false);
+ popup.initialize(sizePref);
+ popup.setSettingChangedListener(PhotoMenu.this);
+ mUI.dismissPopup();
+ mPopup = popup;
+ mUI.showPopup(mPopup);
+ }
+ });
+ more.addItem(item);
+ // white balance
+ if (group.findPreference(CameraSettings.KEY_WHITE_BALANCE) != null) {
+ item = makeItem(CameraSettings.KEY_WHITE_BALANCE);
+ item.setLabel(res.getString(R.string.pref_camera_whitebalance_label));
+ more.addItem(item);
+ }
+ // scene mode
+ if (group.findPreference(CameraSettings.KEY_SCENE_MODE) != null) {
+ IconListPreference pref = (IconListPreference) group.findPreference(
+ CameraSettings.KEY_SCENE_MODE);
+ pref.setUseSingleIcon(true);
+ item = makeItem(CameraSettings.KEY_SCENE_MODE);
+ more.addItem(item);
+ }
+ }
+
+ @Override
+ // Hit when an item in a popup gets selected
+ public void onListPrefChanged(ListPreference pref) {
+ if (mPopup != null) {
+ mUI.dismissPopup();
+ }
+ onSettingChanged(pref);
+ }
+
+ public void popupDismissed() {
+ if (mPopup != null) {
+ mPopup = null;
+ }
+ }
+
+ // Return true if the preference has the specified key but not the value.
+ private static boolean notSame(ListPreference pref, String key, String value) {
+ return (key.equals(pref.getKey()) && !value.equals(pref.getValue()));
+ }
+
+ private void setPreference(String key, String value) {
+ ListPreference pref = mPreferenceGroup.findPreference(key);
+ if (pref != null && !value.equals(pref.getValue())) {
+ pref.setValue(value);
+ reloadPreferences();
+ }
+ }
+
+ @Override
+ public void onSettingChanged(ListPreference pref) {
+ // Reset the scene mode if HDR is set to on. Reset HDR if scene mode is
+ // set to non-auto.
+ if (notSame(pref, CameraSettings.KEY_CAMERA_HDR, mSettingOff)) {
+ setPreference(CameraSettings.KEY_SCENE_MODE, Parameters.SCENE_MODE_AUTO);
+ } else if (notSame(pref, CameraSettings.KEY_SCENE_MODE, Parameters.SCENE_MODE_AUTO)) {
+ setPreference(CameraSettings.KEY_CAMERA_HDR, mSettingOff);
+ }
+ super.onSettingChanged(pref);
+ }
+}
diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java
new file mode 100644
index 000000000..c65a49ef9
--- /dev/null
+++ b/src/com/android/camera/PhotoModule.java
@@ -0,0 +1,2006 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences.Editor;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera.CameraInfo;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.location.Location;
+import android.media.CameraProfile;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.OrientationEventListener;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.camera.CameraManager.CameraAFCallback;
+import com.android.camera.CameraManager.CameraAFMoveCallback;
+import com.android.camera.CameraManager.CameraPictureCallback;
+import com.android.camera.CameraManager.CameraProxy;
+import com.android.camera.CameraManager.CameraShutterCallback;
+import com.android.camera.ui.CountDownView.OnCountDownFinishedListener;
+import com.android.camera.ui.PopupManager;
+import com.android.camera.ui.RotateTextToast;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.exif.Rational;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Formatter;
+import java.util.List;
+
+public class PhotoModule
+ implements CameraModule,
+ PhotoController,
+ FocusOverlayManager.Listener,
+ CameraPreference.OnPreferenceChangedListener,
+ ShutterButton.OnShutterButtonListener,
+ MediaSaveService.Listener,
+ OnCountDownFinishedListener,
+ SensorEventListener {
+
+ private static final String TAG = "CAM_PhotoModule";
+
+ // We number the request code from 1000 to avoid collision with Gallery.
+ private static final int REQUEST_CROP = 1000;
+
+ private static final int SETUP_PREVIEW = 1;
+ private static final int FIRST_TIME_INIT = 2;
+ private static final int CLEAR_SCREEN_DELAY = 3;
+ private static final int SET_CAMERA_PARAMETERS_WHEN_IDLE = 4;
+ private static final int CHECK_DISPLAY_ROTATION = 5;
+ private static final int SHOW_TAP_TO_FOCUS_TOAST = 6;
+ private static final int SWITCH_CAMERA = 7;
+ private static final int SWITCH_CAMERA_START_ANIMATION = 8;
+ private static final int CAMERA_OPEN_DONE = 9;
+ private static final int START_PREVIEW_DONE = 10;
+ private static final int OPEN_CAMERA_FAIL = 11;
+ private static final int CAMERA_DISABLED = 12;
+ private static final int CAPTURE_ANIMATION_DONE = 13;
+
+ // The subset of parameters we need to update in setCameraParameters().
+ private static final int UPDATE_PARAM_INITIALIZE = 1;
+ private static final int UPDATE_PARAM_ZOOM = 2;
+ private static final int UPDATE_PARAM_PREFERENCE = 4;
+ private static final int UPDATE_PARAM_ALL = -1;
+
+ // This is the timeout to keep the camera in onPause for the first time
+ // after screen on if the activity is started from secure lock screen.
+ private static final int KEEP_CAMERA_TIMEOUT = 1000; // ms
+
+ // copied from Camera hierarchy
+ private CameraActivity mActivity;
+ private CameraProxy mCameraDevice;
+ private int mCameraId;
+ private Parameters mParameters;
+ private boolean mPaused;
+
+ private PhotoUI mUI;
+
+ // The activity is going to switch to the specified camera id. This is
+ // needed because texture copy is done in GL thread. -1 means camera is not
+ // switching.
+ protected int mPendingSwitchCameraId = -1;
+ private boolean mOpenCameraFail;
+ private boolean mCameraDisabled;
+
+ // When setCameraParametersWhenIdle() is called, we accumulate the subsets
+ // needed to be updated in mUpdateSet.
+ private int mUpdateSet;
+
+ private static final int SCREEN_DELAY = 2 * 60 * 1000;
+
+ private int mZoomValue; // The current zoom value.
+
+ private Parameters mInitialParams;
+ private boolean mFocusAreaSupported;
+ private boolean mMeteringAreaSupported;
+ private boolean mAeLockSupported;
+ private boolean mAwbLockSupported;
+ private boolean mContinousFocusSupported;
+
+ // The degrees of the device rotated clockwise from its natural orientation.
+ private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
+ private ComboPreferences mPreferences;
+
+ private static final String sTempCropFilename = "crop-temp";
+
+ private ContentProviderClient mMediaProviderClient;
+ private boolean mFaceDetectionStarted = false;
+
+ // mCropValue and mSaveUri are used only if isImageCaptureIntent() is true.
+ private String mCropValue;
+ private Uri mSaveUri;
+
+ // We use a queue to generated names of the images to be used later
+ // when the image is ready to be saved.
+ private NamedImages mNamedImages;
+
+ private Runnable mDoSnapRunnable = new Runnable() {
+ @Override
+ public void run() {
+ onShutterButtonClick();
+ }
+ };
+
+ private Runnable mFlashRunnable = new Runnable() {
+ @Override
+ public void run() {
+ animateFlash();
+ }
+ };
+
+ private final StringBuilder mBuilder = new StringBuilder();
+ private final Formatter mFormatter = new Formatter(mBuilder);
+ private final Object[] mFormatterArgs = new Object[1];
+
+ /**
+ * An unpublished intent flag requesting to return as soon as capturing
+ * is completed.
+ *
+ * TODO: consider publishing by moving into MediaStore.
+ */
+ private static final String EXTRA_QUICK_CAPTURE =
+ "android.intent.extra.quickCapture";
+
+ // The display rotation in degrees. This is only valid when mCameraState is
+ // not PREVIEW_STOPPED.
+ private int mDisplayRotation;
+ // The value for android.hardware.Camera.setDisplayOrientation.
+ private int mCameraDisplayOrientation;
+ // The value for UI components like indicators.
+ private int mDisplayOrientation;
+ // The value for android.hardware.Camera.Parameters.setRotation.
+ private int mJpegRotation;
+ private boolean mFirstTimeInitialized;
+ private boolean mIsImageCaptureIntent;
+
+ private int mCameraState = PREVIEW_STOPPED;
+ private boolean mSnapshotOnIdle = false;
+
+ private ContentResolver mContentResolver;
+
+ private LocationManager mLocationManager;
+
+ private final PostViewPictureCallback mPostViewPictureCallback =
+ new PostViewPictureCallback();
+ private final RawPictureCallback mRawPictureCallback =
+ new RawPictureCallback();
+ private final AutoFocusCallback mAutoFocusCallback =
+ new AutoFocusCallback();
+ private final Object mAutoFocusMoveCallback =
+ ApiHelper.HAS_AUTO_FOCUS_MOVE_CALLBACK
+ ? new AutoFocusMoveCallback()
+ : null;
+
+ private final CameraErrorCallback mErrorCallback = new CameraErrorCallback();
+
+ private long mFocusStartTime;
+ private long mShutterCallbackTime;
+ private long mPostViewPictureCallbackTime;
+ private long mRawPictureCallbackTime;
+ private long mJpegPictureCallbackTime;
+ private long mOnResumeTime;
+ private byte[] mJpegImageData;
+
+ // These latency time are for the CameraLatency test.
+ public long mAutoFocusTime;
+ public long mShutterLag;
+ public long mShutterToPictureDisplayedTime;
+ public long mPictureDisplayedToJpegCallbackTime;
+ public long mJpegCallbackFinishTime;
+ public long mCaptureStartTime;
+
+ // This handles everything about focus.
+ private FocusOverlayManager mFocusManager;
+
+ private String mSceneMode;
+
+ private final Handler mHandler = new MainHandler();
+ private PreferenceGroup mPreferenceGroup;
+
+ private boolean mQuickCapture;
+ private SensorManager mSensorManager;
+ private float[] mGData = new float[3];
+ private float[] mMData = new float[3];
+ private float[] mR = new float[16];
+ private int mHeading = -1;
+
+ CameraStartUpThread mCameraStartUpThread;
+ ConditionVariable mStartPreviewPrerequisiteReady = new ConditionVariable();
+
+ private MediaSaveService.OnMediaSavedListener mOnMediaSavedListener =
+ new MediaSaveService.OnMediaSavedListener() {
+ @Override
+ public void onMediaSaved(Uri uri) {
+ if (uri != null) {
+ mActivity.notifyNewMedia(uri);
+ }
+ }
+ };
+
+ // The purpose is not to block the main thread in onCreate and onResume.
+ private class CameraStartUpThread extends Thread {
+ private volatile boolean mCancelled;
+
+ public void cancel() {
+ mCancelled = true;
+ interrupt();
+ }
+
+ public boolean isCanceled() {
+ return mCancelled;
+ }
+
+ @Override
+ public void run() {
+ try {
+ // We need to check whether the activity is paused before long
+ // operations to ensure that onPause() can be done ASAP.
+ if (mCancelled) return;
+ mCameraDevice = Util.openCamera(mActivity, mCameraId);
+ mParameters = mCameraDevice.getParameters();
+ // Wait until all the initialization needed by startPreview are
+ // done.
+ mStartPreviewPrerequisiteReady.block();
+
+ initializeCapabilities();
+ if (mFocusManager == null) initializeFocusManager();
+ if (mCancelled) return;
+ setCameraParameters(UPDATE_PARAM_ALL);
+ mHandler.sendEmptyMessage(CAMERA_OPEN_DONE);
+ if (mCancelled) return;
+ startPreview();
+ mHandler.sendEmptyMessage(START_PREVIEW_DONE);
+ mOnResumeTime = SystemClock.uptimeMillis();
+ mHandler.sendEmptyMessage(CHECK_DISPLAY_ROTATION);
+ } catch (CameraHardwareException e) {
+ mHandler.sendEmptyMessage(OPEN_CAMERA_FAIL);
+ } catch (CameraDisabledException e) {
+ mHandler.sendEmptyMessage(CAMERA_DISABLED);
+ }
+ }
+ }
+
+ /**
+ * This Handler is used to post message back onto the main thread of the
+ * application
+ */
+ private class MainHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case SETUP_PREVIEW: {
+ setupPreview();
+ break;
+ }
+
+ case CLEAR_SCREEN_DELAY: {
+ mActivity.getWindow().clearFlags(
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ break;
+ }
+
+ case FIRST_TIME_INIT: {
+ initializeFirstTime();
+ break;
+ }
+
+ case SET_CAMERA_PARAMETERS_WHEN_IDLE: {
+ setCameraParametersWhenIdle(0);
+ break;
+ }
+
+ case CHECK_DISPLAY_ROTATION: {
+ // Set the display orientation if display rotation has changed.
+ // Sometimes this happens when the device is held upside
+ // down and camera app is opened. Rotation animation will
+ // take some time and the rotation value we have got may be
+ // wrong. Framework does not have a callback for this now.
+ if (Util.getDisplayRotation(mActivity) != mDisplayRotation) {
+ setDisplayOrientation();
+ }
+ if (SystemClock.uptimeMillis() - mOnResumeTime < 5000) {
+ mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100);
+ }
+ break;
+ }
+
+ case SHOW_TAP_TO_FOCUS_TOAST: {
+ showTapToFocusToast();
+ break;
+ }
+
+ case SWITCH_CAMERA: {
+ switchCamera();
+ break;
+ }
+
+ case SWITCH_CAMERA_START_ANIMATION: {
+ // TODO: Need to revisit
+ // ((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera();
+ break;
+ }
+
+ case CAMERA_OPEN_DONE: {
+ onCameraOpened();
+ break;
+ }
+
+ case START_PREVIEW_DONE: {
+ onPreviewStarted();
+ break;
+ }
+
+ case OPEN_CAMERA_FAIL: {
+ mCameraStartUpThread = null;
+ mOpenCameraFail = true;
+ Util.showErrorAndFinish(mActivity,
+ R.string.cannot_connect_camera);
+ break;
+ }
+
+ case CAMERA_DISABLED: {
+ mCameraStartUpThread = null;
+ mCameraDisabled = true;
+ Util.showErrorAndFinish(mActivity,
+ R.string.camera_disabled);
+ break;
+ }
+ case CAPTURE_ANIMATION_DONE: {
+ mUI.enablePreviewThumb(false);
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void init(CameraActivity activity, View parent) {
+ mActivity = activity;
+ mUI = new PhotoUI(activity, this, parent);
+ mPreferences = new ComboPreferences(mActivity);
+ CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
+ mCameraId = getPreferredCameraId(mPreferences);
+
+ mContentResolver = mActivity.getContentResolver();
+
+ // To reduce startup time, open the camera and start the preview in
+ // another thread.
+ mCameraStartUpThread = new CameraStartUpThread();
+ mCameraStartUpThread.start();
+
+ // Surface texture is from camera screen nail and startPreview needs it.
+ // This must be done before startPreview.
+ mIsImageCaptureIntent = isImageCaptureIntent();
+
+ mPreferences.setLocalId(mActivity, mCameraId);
+ CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+ // we need to reset exposure for the preview
+ resetExposureCompensation();
+ // Starting the preview needs preferences, camera screen nail, and
+ // focus area indicator.
+ mStartPreviewPrerequisiteReady.open();
+
+ initializeControlByIntent();
+ mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
+ mLocationManager = new LocationManager(mActivity, mUI);
+ mSensorManager = (SensorManager)(mActivity.getSystemService(Context.SENSOR_SERVICE));
+ }
+
+ private void initializeControlByIntent() {
+ mUI.initializeControlByIntent();
+ if (mIsImageCaptureIntent) {
+ setupCaptureParams();
+ }
+ }
+
+ private void onPreviewStarted() {
+ mCameraStartUpThread = null;
+ setCameraState(IDLE);
+ startFaceDetection();
+ locationFirstRun();
+ }
+
+ // Prompt the user to pick to record location for the very first run of
+ // camera only
+ private void locationFirstRun() {
+ if (RecordLocationPreference.isSet(mPreferences)) {
+ return;
+ }
+ if (mActivity.isSecureCamera()) return;
+ // Check if the back camera exists
+ int backCameraId = CameraHolder.instance().getBackCameraId();
+ if (backCameraId == -1) {
+ // If there is no back camera, do not show the prompt.
+ return;
+ }
+ mUI.showLocationDialog();
+ }
+
+ public void enableRecordingLocation(boolean enable) {
+ setLocationPreference(enable ? RecordLocationPreference.VALUE_ON
+ : RecordLocationPreference.VALUE_OFF);
+ }
+
+ private void setLocationPreference(String value) {
+ mPreferences.edit()
+ .putString(CameraSettings.KEY_RECORD_LOCATION, value)
+ .apply();
+ // TODO: Fix this to use the actual onSharedPreferencesChanged listener
+ // instead of invoking manually
+ onSharedPreferenceChanged();
+ }
+
+ private void onCameraOpened() {
+ View root = mUI.getRootView();
+ // These depend on camera parameters.
+
+ int width = root.getWidth();
+ int height = root.getHeight();
+ mFocusManager.setPreviewSize(width, height);
+ openCameraCommon();
+ }
+
+ private void switchCamera() {
+ if (mPaused) return;
+
+ Log.v(TAG, "Start to switch camera. id=" + mPendingSwitchCameraId);
+ mCameraId = mPendingSwitchCameraId;
+ mPendingSwitchCameraId = -1;
+ setCameraId(mCameraId);
+
+ // from onPause
+ closeCamera();
+ mUI.collapseCameraControls();
+ mUI.clearFaces();
+ if (mFocusManager != null) mFocusManager.removeMessages();
+
+ // Restart the camera and initialize the UI. From onCreate.
+ mPreferences.setLocalId(mActivity, mCameraId);
+ CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+ try {
+ mCameraDevice = Util.openCamera(mActivity, mCameraId);
+ mParameters = mCameraDevice.getParameters();
+ } catch (CameraHardwareException e) {
+ Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+ return;
+ } catch (CameraDisabledException e) {
+ Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+ return;
+ }
+ initializeCapabilities();
+ CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+ boolean mirror = (info.facing == CameraInfo.CAMERA_FACING_FRONT);
+ mFocusManager.setMirror(mirror);
+ mFocusManager.setParameters(mInitialParams);
+ setupPreview();
+
+ // reset zoom value index
+ mZoomValue = 0;
+ openCameraCommon();
+
+ if (ApiHelper.HAS_SURFACE_TEXTURE) {
+ // Start switch camera animation. Post a message because
+ // onFrameAvailable from the old camera may already exist.
+ mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION);
+ }
+ }
+
+ protected void setCameraId(int cameraId) {
+ ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+ pref.setValue("" + cameraId);
+ }
+
+ // either open a new camera or switch cameras
+ private void openCameraCommon() {
+ loadCameraPreferences();
+
+ mUI.onCameraOpened(mPreferenceGroup, mPreferences, mParameters, this);
+ updateSceneMode();
+ showTapToFocusToastIfNeeded();
+
+
+ }
+
+ public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) {
+ if (mFocusManager != null) mFocusManager.setPreviewSize(width, height);
+ }
+
+ private void resetExposureCompensation() {
+ String value = mPreferences.getString(CameraSettings.KEY_EXPOSURE,
+ CameraSettings.EXPOSURE_DEFAULT_VALUE);
+ if (!CameraSettings.EXPOSURE_DEFAULT_VALUE.equals(value)) {
+ Editor editor = mPreferences.edit();
+ editor.putString(CameraSettings.KEY_EXPOSURE, "0");
+ editor.apply();
+ }
+ }
+
+ private void keepMediaProviderInstance() {
+ // We want to keep a reference to MediaProvider in camera's lifecycle.
+ // TODO: Utilize mMediaProviderClient instance to replace
+ // ContentResolver calls.
+ if (mMediaProviderClient == null) {
+ mMediaProviderClient = mContentResolver
+ .acquireContentProviderClient(MediaStore.AUTHORITY);
+ }
+ }
+
+ // Snapshots can only be taken after this is called. It should be called
+ // once only. We could have done these things in onCreate() but we want to
+ // make preview screen appear as soon as possible.
+ private void initializeFirstTime() {
+ if (mFirstTimeInitialized) return;
+
+ // Initialize location service.
+ boolean recordLocation = RecordLocationPreference.get(
+ mPreferences, mContentResolver);
+ mLocationManager.recordLocation(recordLocation);
+
+ keepMediaProviderInstance();
+
+ mUI.initializeFirstTime();
+ MediaSaveService s = mActivity.getMediaSaveService();
+ // We set the listener only when both service and shutterbutton
+ // are initialized.
+ if (s != null) {
+ s.setListener(this);
+ }
+
+ mNamedImages = new NamedImages();
+
+ mFirstTimeInitialized = true;
+ addIdleHandler();
+
+ mActivity.updateStorageSpaceAndHint();
+ }
+
+ // If the activity is paused and resumed, this method will be called in
+ // onResume.
+ private void initializeSecondTime() {
+ // Start location update if needed.
+ boolean recordLocation = RecordLocationPreference.get(
+ mPreferences, mContentResolver);
+ mLocationManager.recordLocation(recordLocation);
+ MediaSaveService s = mActivity.getMediaSaveService();
+ if (s != null) {
+ s.setListener(this);
+ }
+ mNamedImages = new NamedImages();
+ mUI.initializeSecondTime(mParameters);
+ keepMediaProviderInstance();
+ }
+
+ @Override
+ public void onSurfaceCreated(SurfaceHolder holder) {
+ // Do not access the camera if camera start up thread is not finished.
+ if (mCameraDevice == null || mCameraStartUpThread != null)
+ return;
+
+ mCameraDevice.setPreviewDisplay(holder);
+ // This happens when onConfigurationChanged arrives, surface has been
+ // destroyed, and there is no onFullScreenChanged.
+ if (mCameraState == PREVIEW_STOPPED) {
+ setupPreview();
+ }
+ }
+
+ private void showTapToFocusToastIfNeeded() {
+ // Show the tap to focus toast if this is the first start.
+ if (mFocusAreaSupported &&
+ mPreferences.getBoolean(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, true)) {
+ // Delay the toast for one second to wait for orientation.
+ mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_FOCUS_TOAST, 1000);
+ }
+ }
+
+ private void addIdleHandler() {
+ MessageQueue queue = Looper.myQueue();
+ queue.addIdleHandler(new MessageQueue.IdleHandler() {
+ @Override
+ public boolean queueIdle() {
+ Storage.ensureOSXCompatible();
+ return false;
+ }
+ });
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ @Override
+ public void startFaceDetection() {
+ if (!ApiHelper.HAS_FACE_DETECTION) return;
+ if (mFaceDetectionStarted) return;
+ if (mParameters.getMaxNumDetectedFaces() > 0) {
+ mFaceDetectionStarted = true;
+ CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+ mUI.onStartFaceDetection(mDisplayOrientation,
+ (info.facing == CameraInfo.CAMERA_FACING_FRONT));
+ mCameraDevice.setFaceDetectionCallback(mHandler, mUI);
+ mCameraDevice.startFaceDetection();
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ @Override
+ public void stopFaceDetection() {
+ if (!ApiHelper.HAS_FACE_DETECTION) return;
+ if (!mFaceDetectionStarted) return;
+ if (mParameters.getMaxNumDetectedFaces() > 0) {
+ mFaceDetectionStarted = false;
+ mCameraDevice.setFaceDetectionCallback(null, null);
+ mCameraDevice.stopFaceDetection();
+ mUI.clearFaces();
+ }
+ }
+
+ private final class ShutterCallback
+ implements CameraShutterCallback {
+
+ private boolean mAnimateFlash;
+
+ public ShutterCallback(boolean animateFlash) {
+ mAnimateFlash = animateFlash;
+ }
+
+ @Override
+ public void onShutter(CameraProxy camera) {
+ mShutterCallbackTime = System.currentTimeMillis();
+ mShutterLag = mShutterCallbackTime - mCaptureStartTime;
+ Log.v(TAG, "mShutterLag = " + mShutterLag + "ms");
+ if (mAnimateFlash) {
+ mActivity.runOnUiThread(mFlashRunnable);
+ }
+ }
+ }
+
+ private final class PostViewPictureCallback
+ implements CameraPictureCallback {
+ @Override
+ public void onPictureTaken(byte [] data, CameraProxy camera) {
+ mPostViewPictureCallbackTime = System.currentTimeMillis();
+ Log.v(TAG, "mShutterToPostViewCallbackTime = "
+ + (mPostViewPictureCallbackTime - mShutterCallbackTime)
+ + "ms");
+ }
+ }
+
+ private final class RawPictureCallback
+ implements CameraPictureCallback {
+ @Override
+ public void onPictureTaken(byte [] rawData, CameraProxy camera) {
+ mRawPictureCallbackTime = System.currentTimeMillis();
+ Log.v(TAG, "mShutterToRawCallbackTime = "
+ + (mRawPictureCallbackTime - mShutterCallbackTime) + "ms");
+ }
+ }
+
+ private final class JpegPictureCallback
+ implements CameraPictureCallback {
+ Location mLocation;
+
+ public JpegPictureCallback(Location loc) {
+ mLocation = loc;
+ }
+
+ @Override
+ public void onPictureTaken(final byte [] jpegData, CameraProxy camera) {
+ if (mPaused) {
+ return;
+ }
+ //TODO: We should show the picture taken rather than frozen preview here
+ if (mIsImageCaptureIntent) {
+ stopPreview();
+ }
+ if (mSceneMode == Util.SCENE_MODE_HDR) {
+ mUI.showSwitcher();
+ mUI.setSwipingEnabled(true);
+ }
+
+ mJpegPictureCallbackTime = System.currentTimeMillis();
+ // If postview callback has arrived, the captured image is displayed
+ // in postview callback. If not, the captured image is displayed in
+ // raw picture callback.
+ if (mPostViewPictureCallbackTime != 0) {
+ mShutterToPictureDisplayedTime =
+ mPostViewPictureCallbackTime - mShutterCallbackTime;
+ mPictureDisplayedToJpegCallbackTime =
+ mJpegPictureCallbackTime - mPostViewPictureCallbackTime;
+ } else {
+ mShutterToPictureDisplayedTime =
+ mRawPictureCallbackTime - mShutterCallbackTime;
+ mPictureDisplayedToJpegCallbackTime =
+ mJpegPictureCallbackTime - mRawPictureCallbackTime;
+ }
+ Log.v(TAG, "mPictureDisplayedToJpegCallbackTime = "
+ + mPictureDisplayedToJpegCallbackTime + "ms");
+
+ /*TODO:
+ // Only animate when in full screen capture mode
+ // i.e. If monkey/a user swipes to the gallery during picture taking,
+ // don't show animation
+ if (ApiHelper.HAS_SURFACE_TEXTURE && !mIsImageCaptureIntent
+ && mActivity.mShowCameraAppView) {
+ // Finish capture animation
+ mHandler.removeMessages(CAPTURE_ANIMATION_DONE);
+ ((CameraScreenNail) mActivity.mCameraScreenNail).animateSlide();
+ mHandler.sendEmptyMessageDelayed(CAPTURE_ANIMATION_DONE,
+ CaptureAnimManager.getAnimationDuration());
+ } */
+ mFocusManager.updateFocusUI(); // Ensure focus indicator is hidden.
+ if (!mIsImageCaptureIntent) {
+ if (ApiHelper.CAN_START_PREVIEW_IN_JPEG_CALLBACK) {
+ setupPreview();
+ } else {
+ // Camera HAL of some devices have a bug. Starting preview
+ // immediately after taking a picture will fail. Wait some
+ // time before starting the preview.
+ mHandler.sendEmptyMessageDelayed(SETUP_PREVIEW, 300);
+ }
+ }
+
+ if (!mIsImageCaptureIntent) {
+ // Calculate the width and the height of the jpeg.
+ Size s = mParameters.getPictureSize();
+ ExifInterface exif = Exif.getExif(jpegData);
+ int orientation = Exif.getOrientation(exif);
+ int width, height;
+ if ((mJpegRotation + orientation) % 180 == 0) {
+ width = s.width;
+ height = s.height;
+ } else {
+ width = s.height;
+ height = s.width;
+ }
+ String title = mNamedImages.getTitle();
+ long date = mNamedImages.getDate();
+ if (title == null) {
+ Log.e(TAG, "Unbalanced name/data pair");
+ } else {
+ if (date == -1) date = mCaptureStartTime;
+ if (mHeading >= 0) {
+ // heading direction has been updated by the sensor.
+ ExifTag directionRefTag = exif.buildTag(
+ ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
+ ExifInterface.GpsTrackRef.MAGNETIC_DIRECTION);
+ ExifTag directionTag = exif.buildTag(
+ ExifInterface.TAG_GPS_IMG_DIRECTION,
+ new Rational(mHeading, 1));
+ exif.setTag(directionRefTag);
+ exif.setTag(directionTag);
+ }
+ mActivity.getMediaSaveService().addImage(
+ jpegData, title, date, mLocation, width, height,
+ orientation, exif, mOnMediaSavedListener, mContentResolver);
+ }
+ } else {
+ mJpegImageData = jpegData;
+ if (!mQuickCapture) {
+ mUI.showPostCaptureAlert();
+ } else {
+ onCaptureDone();
+ }
+ }
+
+ // Check this in advance of each shot so we don't add to shutter
+ // latency. It's true that someone else could write to the SD card in
+ // the mean time and fill it, but that could have happened between the
+ // shutter press and saving the JPEG too.
+ mActivity.updateStorageSpaceAndHint();
+
+ long now = System.currentTimeMillis();
+ mJpegCallbackFinishTime = now - mJpegPictureCallbackTime;
+ Log.v(TAG, "mJpegCallbackFinishTime = "
+ + mJpegCallbackFinishTime + "ms");
+ mJpegPictureCallbackTime = 0;
+ }
+ }
+
+ private final class AutoFocusCallback implements CameraAFCallback {
+ @Override
+ public void onAutoFocus(
+ boolean focused, CameraProxy camera) {
+ if (mPaused) return;
+
+ mAutoFocusTime = System.currentTimeMillis() - mFocusStartTime;
+ Log.v(TAG, "mAutoFocusTime = " + mAutoFocusTime + "ms");
+ setCameraState(IDLE);
+ mFocusManager.onAutoFocus(focused, mUI.isShutterPressed());
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ private final class AutoFocusMoveCallback
+ implements CameraAFMoveCallback {
+ @Override
+ public void onAutoFocusMoving(
+ boolean moving, CameraProxy camera) {
+ mFocusManager.onAutoFocusMoving(moving);
+ }
+ }
+
+ private static class NamedImages {
+ private ArrayList<NamedEntity> mQueue;
+ private boolean mStop;
+ private NamedEntity mNamedEntity;
+
+ public NamedImages() {
+ mQueue = new ArrayList<NamedEntity>();
+ }
+
+ public void nameNewImage(ContentResolver resolver, long date) {
+ NamedEntity r = new NamedEntity();
+ r.title = Util.createJpegName(date);
+ r.date = date;
+ mQueue.add(r);
+ }
+
+ public String getTitle() {
+ if (mQueue.isEmpty()) {
+ mNamedEntity = null;
+ return null;
+ }
+ mNamedEntity = mQueue.get(0);
+ mQueue.remove(0);
+
+ return mNamedEntity.title;
+ }
+
+ // Must be called after getTitle().
+ public long getDate() {
+ if (mNamedEntity == null) return -1;
+ return mNamedEntity.date;
+ }
+
+ private static class NamedEntity {
+ String title;
+ long date;
+ }
+ }
+
+ private void setCameraState(int state) {
+ mCameraState = state;
+ switch (state) {
+ case PhotoController.PREVIEW_STOPPED:
+ case PhotoController.SNAPSHOT_IN_PROGRESS:
+ case PhotoController.SWITCHING_CAMERA:
+ mUI.enableGestures(false);
+ break;
+ case PhotoController.IDLE:
+ mUI.enableGestures(true);
+ break;
+ }
+ }
+
+ private void animateFlash() {
+ // Only animate when in full screen capture mode
+ // i.e. If monkey/a user swipes to the gallery during picture taking,
+ // don't show animation
+ if (!mIsImageCaptureIntent) {
+ mUI.animateFlash();
+
+ // TODO: mUI.enablePreviewThumb(true);
+ // mHandler.sendEmptyMessageDelayed(CAPTURE_ANIMATION_DONE,
+ // CaptureAnimManager.getAnimationDuration());
+ }
+ }
+
+ @Override
+ public boolean capture() {
+ // If we are already in the middle of taking a snapshot or the image save request
+ // is full then ignore.
+ if (mCameraDevice == null || mCameraState == SNAPSHOT_IN_PROGRESS
+ || mCameraState == SWITCHING_CAMERA
+ || mActivity.getMediaSaveService().isQueueFull()) {
+ return false;
+ }
+ mCaptureStartTime = System.currentTimeMillis();
+ mPostViewPictureCallbackTime = 0;
+ mJpegImageData = null;
+
+ final boolean animateBefore = (mSceneMode == Util.SCENE_MODE_HDR);
+
+ if (animateBefore) {
+ animateFlash();
+ }
+
+ // Set rotation and gps data.
+ int orientation;
+ // We need to be consistent with the framework orientation (i.e. the
+ // orientation of the UI.) when the auto-rotate screen setting is on.
+ if (mActivity.isAutoRotateScreen()) {
+ orientation = (360 - mDisplayRotation) % 360;
+ } else {
+ orientation = mOrientation;
+ }
+ mJpegRotation = Util.getJpegRotation(mCameraId, orientation);
+ mParameters.setRotation(mJpegRotation);
+ Location loc = mLocationManager.getCurrentLocation();
+ Util.setGpsParameters(mParameters, loc);
+ mCameraDevice.setParameters(mParameters);
+
+ mCameraDevice.takePicture(mHandler,
+ new ShutterCallback(!animateBefore),
+ mRawPictureCallback, mPostViewPictureCallback,
+ new JpegPictureCallback(loc));
+
+ mNamedImages.nameNewImage(mContentResolver, mCaptureStartTime);
+
+ mFaceDetectionStarted = false;
+ setCameraState(SNAPSHOT_IN_PROGRESS);
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
+ UsageStatistics.ACTION_CAPTURE_DONE, "Photo");
+ return true;
+ }
+
+ @Override
+ public void setFocusParameters() {
+ setCameraParameters(UPDATE_PARAM_PREFERENCE);
+ }
+
+ private int getPreferredCameraId(ComboPreferences preferences) {
+ int intentCameraId = Util.getCameraFacingIntentExtras(mActivity);
+ if (intentCameraId != -1) {
+ // Testing purpose. Launch a specific camera through the intent
+ // extras.
+ return intentCameraId;
+ } else {
+ return CameraSettings.readPreferredCameraId(preferences);
+ }
+ }
+
+ private void updateSceneMode() {
+ // If scene mode is set, we cannot set flash mode, white balance, and
+ // focus mode, instead, we read it from driver
+ if (!Parameters.SCENE_MODE_AUTO.equals(mSceneMode)) {
+ overrideCameraSettings(mParameters.getFlashMode(),
+ mParameters.getWhiteBalance(), mParameters.getFocusMode());
+ } else {
+ overrideCameraSettings(null, null, null);
+ }
+ }
+
+ private void overrideCameraSettings(final String flashMode,
+ final String whiteBalance, final String focusMode) {
+ mUI.overrideSettings(
+ CameraSettings.KEY_FLASH_MODE, flashMode,
+ CameraSettings.KEY_WHITE_BALANCE, whiteBalance,
+ CameraSettings.KEY_FOCUS_MODE, focusMode);
+ }
+
+ private void loadCameraPreferences() {
+ CameraSettings settings = new CameraSettings(mActivity, mInitialParams,
+ mCameraId, CameraHolder.instance().getCameraInfo());
+ mPreferenceGroup = settings.getPreferenceGroup(R.xml.camera_preferences);
+ }
+
+ @Override
+ public void onOrientationChanged(int orientation) {
+ // We keep the last known orientation. So if the user first orient
+ // the camera then point the camera to floor or sky, we still have
+ // the correct orientation.
+ if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) return;
+ mOrientation = Util.roundOrientation(orientation, mOrientation);
+
+ // Show the toast after getting the first orientation changed.
+ if (mHandler.hasMessages(SHOW_TAP_TO_FOCUS_TOAST)) {
+ mHandler.removeMessages(SHOW_TAP_TO_FOCUS_TOAST);
+ showTapToFocusToast();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (mMediaProviderClient != null) {
+ mMediaProviderClient.release();
+ mMediaProviderClient = null;
+ }
+ }
+
+ @Override
+ public void onCaptureCancelled() {
+ mActivity.setResultEx(Activity.RESULT_CANCELED, new Intent());
+ mActivity.finish();
+ }
+
+ @Override
+ public void onCaptureRetake() {
+ if (mPaused)
+ return;
+ mUI.hidePostCaptureAlert();
+ setupPreview();
+ }
+
+ @Override
+ public void onCaptureDone() {
+ if (mPaused) {
+ return;
+ }
+
+ byte[] data = mJpegImageData;
+
+ if (mCropValue == null) {
+ // First handle the no crop case -- just return the value. If the
+ // caller specifies a "save uri" then write the data to its
+ // stream. Otherwise, pass back a scaled down version of the bitmap
+ // directly in the extras.
+ if (mSaveUri != null) {
+ OutputStream outputStream = null;
+ try {
+ outputStream = mContentResolver.openOutputStream(mSaveUri);
+ outputStream.write(data);
+ outputStream.close();
+
+ mActivity.setResultEx(Activity.RESULT_OK);
+ mActivity.finish();
+ } catch (IOException ex) {
+ // ignore exception
+ } finally {
+ Util.closeSilently(outputStream);
+ }
+ } else {
+ ExifInterface exif = Exif.getExif(data);
+ int orientation = Exif.getOrientation(exif);
+ Bitmap bitmap = Util.makeBitmap(data, 50 * 1024);
+ bitmap = Util.rotate(bitmap, orientation);
+ mActivity.setResultEx(Activity.RESULT_OK,
+ new Intent("inline-data").putExtra("data", bitmap));
+ mActivity.finish();
+ }
+ } else {
+ // Save the image to a temp file and invoke the cropper
+ Uri tempUri = null;
+ FileOutputStream tempStream = null;
+ try {
+ File path = mActivity.getFileStreamPath(sTempCropFilename);
+ path.delete();
+ tempStream = mActivity.openFileOutput(sTempCropFilename, 0);
+ tempStream.write(data);
+ tempStream.close();
+ tempUri = Uri.fromFile(path);
+ } catch (FileNotFoundException ex) {
+ mActivity.setResultEx(Activity.RESULT_CANCELED);
+ mActivity.finish();
+ return;
+ } catch (IOException ex) {
+ mActivity.setResultEx(Activity.RESULT_CANCELED);
+ mActivity.finish();
+ return;
+ } finally {
+ Util.closeSilently(tempStream);
+ }
+
+ Bundle newExtras = new Bundle();
+ if (mCropValue.equals("circle")) {
+ newExtras.putString("circleCrop", "true");
+ }
+ if (mSaveUri != null) {
+ newExtras.putParcelable(MediaStore.EXTRA_OUTPUT, mSaveUri);
+ } else {
+ newExtras.putBoolean(CropExtras.KEY_RETURN_DATA, true);
+ }
+ if (mActivity.isSecureCamera()) {
+ newExtras.putBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, true);
+ }
+
+ Intent cropIntent = new Intent(CropActivity.CROP_ACTION);
+
+ cropIntent.setData(tempUri);
+ cropIntent.putExtras(newExtras);
+
+ mActivity.startActivityForResult(cropIntent, REQUEST_CROP);
+ }
+ }
+
+ @Override
+ public void onShutterButtonFocus(boolean pressed) {
+ if (mPaused || mUI.collapseCameraControls()
+ || (mCameraState == SNAPSHOT_IN_PROGRESS)
+ || (mCameraState == PREVIEW_STOPPED)) return;
+
+ // Do not do focus if there is not enough storage.
+ if (pressed && !canTakePicture()) return;
+
+ if (pressed) {
+ mFocusManager.onShutterDown();
+ } else {
+ // for countdown mode, we need to postpone the shutter release
+ // i.e. lock the focus during countdown.
+ if (!mUI.isCountingDown()) {
+ mFocusManager.onShutterUp();
+ }
+ }
+ }
+
+ @Override
+ public void onShutterButtonClick() {
+ if (mPaused || mUI.collapseCameraControls()
+ || (mCameraState == SWITCHING_CAMERA)
+ || (mCameraState == PREVIEW_STOPPED)) return;
+
+ // Do not take the picture if there is not enough storage.
+ if (mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) {
+ Log.i(TAG, "Not enough space or storage not ready. remaining="
+ + mActivity.getStorageSpace());
+ return;
+ }
+ Log.v(TAG, "onShutterButtonClick: mCameraState=" + mCameraState);
+
+ if (mSceneMode == Util.SCENE_MODE_HDR) {
+ mUI.hideSwitcher();
+ mUI.setSwipingEnabled(false);
+ }
+ // If the user wants to do a snapshot while the previous one is still
+ // in progress, remember the fact and do it after we finish the previous
+ // one and re-start the preview. Snapshot in progress also includes the
+ // state that autofocus is focusing and a picture will be taken when
+ // focus callback arrives.
+ if ((mFocusManager.isFocusingSnapOnFinish() || mCameraState == SNAPSHOT_IN_PROGRESS)
+ && !mIsImageCaptureIntent) {
+ mSnapshotOnIdle = true;
+ return;
+ }
+
+ String timer = mPreferences.getString(
+ CameraSettings.KEY_TIMER,
+ mActivity.getString(R.string.pref_camera_timer_default));
+ boolean playSound = mPreferences.getString(CameraSettings.KEY_TIMER_SOUND_EFFECTS,
+ mActivity.getString(R.string.pref_camera_timer_sound_default))
+ .equals(mActivity.getString(R.string.setting_on_value));
+
+ int seconds = Integer.parseInt(timer);
+ // When shutter button is pressed, check whether the previous countdown is
+ // finished. If not, cancel the previous countdown and start a new one.
+ if (mUI.isCountingDown()) {
+ mUI.cancelCountDown();
+ }
+ if (seconds > 0) {
+ mUI.startCountDown(seconds, playSound);
+ } else {
+ mSnapshotOnIdle = false;
+ mFocusManager.doSnap();
+ }
+ }
+
+ @Override
+ public void installIntentFilter() {
+ }
+
+ @Override
+ public boolean updateStorageHintOnResume() {
+ return mFirstTimeInitialized;
+ }
+
+ @Override
+ public void updateCameraAppView() {
+ }
+
+ @Override
+ public void onResumeBeforeSuper() {
+ mPaused = false;
+ }
+
+ @Override
+ public void onResumeAfterSuper() {
+ if (mOpenCameraFail || mCameraDisabled) return;
+
+ mJpegPictureCallbackTime = 0;
+ mZoomValue = 0;
+ // Start the preview if it is not started.
+ if (mCameraState == PREVIEW_STOPPED && mCameraStartUpThread == null) {
+ resetExposureCompensation();
+ mCameraStartUpThread = new CameraStartUpThread();
+ mCameraStartUpThread.start();
+ }
+
+ // If first time initialization is not finished, put it in the
+ // message queue.
+ if (!mFirstTimeInitialized) {
+ mHandler.sendEmptyMessage(FIRST_TIME_INIT);
+ } else {
+ initializeSecondTime();
+ }
+ keepScreenOnAwhile();
+
+ // Dismiss open menu if exists.
+ PopupManager.getInstance(mActivity).notifyShowPopup(null);
+ UsageStatistics.onContentViewChanged(
+ UsageStatistics.COMPONENT_CAMERA, "PhotoModule");
+
+ Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ if (gsensor != null) {
+ mSensorManager.registerListener(this, gsensor, SensorManager.SENSOR_DELAY_NORMAL);
+ }
+
+ Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+ if (msensor != null) {
+ mSensorManager.registerListener(this, msensor, SensorManager.SENSOR_DELAY_NORMAL);
+ }
+ }
+
+ void waitCameraStartUpThread() {
+ try {
+ if (mCameraStartUpThread != null) {
+ mCameraStartUpThread.cancel();
+ mCameraStartUpThread.join();
+ mCameraStartUpThread = null;
+ setCameraState(IDLE);
+ }
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+
+ @Override
+ public void onPauseBeforeSuper() {
+ mPaused = true;
+ Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ if (gsensor != null) {
+ mSensorManager.unregisterListener(this, gsensor);
+ }
+
+ Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
+ if (msensor != null) {
+ mSensorManager.unregisterListener(this, msensor);
+ }
+ }
+
+ @Override
+ public void onPauseAfterSuper() {
+ // Wait the camera start up thread to finish.
+ waitCameraStartUpThread();
+
+ // When camera is started from secure lock screen for the first time
+ // after screen on, the activity gets onCreate->onResume->onPause->onResume.
+ // To reduce the latency, keep the camera for a short time so it does
+ // not need to be opened again.
+ if (mCameraDevice != null && mActivity.isSecureCamera()
+ && CameraActivity.isFirstStartAfterScreenOn()) {
+ CameraActivity.resetFirstStartAfterScreenOn();
+ CameraHolder.instance().keep(KEEP_CAMERA_TIMEOUT);
+ }
+ // Reset the focus first. Camera CTS does not guarantee that
+ // cancelAutoFocus is allowed after preview stops.
+ if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
+ mCameraDevice.cancelAutoFocus();
+ }
+ stopPreview();
+
+ mNamedImages = null;
+
+ if (mLocationManager != null) mLocationManager.recordLocation(false);
+
+ // If we are in an image capture intent and has taken
+ // a picture, we just clear it in onPause.
+ mJpegImageData = null;
+
+ // Remove the messages in the event queue.
+ mHandler.removeMessages(SETUP_PREVIEW);
+ mHandler.removeMessages(FIRST_TIME_INIT);
+ mHandler.removeMessages(CHECK_DISPLAY_ROTATION);
+ mHandler.removeMessages(SWITCH_CAMERA);
+ mHandler.removeMessages(SWITCH_CAMERA_START_ANIMATION);
+ mHandler.removeMessages(CAMERA_OPEN_DONE);
+ mHandler.removeMessages(START_PREVIEW_DONE);
+ mHandler.removeMessages(OPEN_CAMERA_FAIL);
+ mHandler.removeMessages(CAMERA_DISABLED);
+
+ closeCamera();
+
+ resetScreenOn();
+ mUI.onPause();
+
+ mPendingSwitchCameraId = -1;
+ if (mFocusManager != null) mFocusManager.removeMessages();
+ MediaSaveService s = mActivity.getMediaSaveService();
+ if (s != null) {
+ s.setListener(null);
+ }
+ }
+
+ /**
+ * The focus manager is the first UI related element to get initialized,
+ * and it requires the RenderOverlay, so initialize it here
+ */
+ private void initializeFocusManager() {
+ // Create FocusManager object. startPreview needs it.
+ // if mFocusManager not null, reuse it
+ // otherwise create a new instance
+ if (mFocusManager != null) {
+ mFocusManager.removeMessages();
+ } else {
+ CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+ boolean mirror = (info.facing == CameraInfo.CAMERA_FACING_FRONT);
+ String[] defaultFocusModes = mActivity.getResources().getStringArray(
+ R.array.pref_camera_focusmode_default_array);
+ mFocusManager = new FocusOverlayManager(mPreferences, defaultFocusModes,
+ mInitialParams, this, mirror,
+ mActivity.getMainLooper(), mUI);
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ Log.v(TAG, "onConfigurationChanged");
+ setDisplayOrientation();
+ }
+
+ @Override
+ public void updateCameraOrientation() {
+ if (mDisplayRotation != Util.getDisplayRotation(mActivity)) {
+ setDisplayOrientation();
+ }
+ }
+
+ @Override
+ public void onActivityResult(
+ int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_CROP: {
+ Intent intent = new Intent();
+ if (data != null) {
+ Bundle extras = data.getExtras();
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ }
+ mActivity.setResultEx(resultCode, intent);
+ mActivity.finish();
+
+ File path = mActivity.getFileStreamPath(sTempCropFilename);
+ path.delete();
+
+ break;
+ }
+ }
+ }
+
+ private boolean canTakePicture() {
+ return isCameraIdle() && (mActivity.getStorageSpace() > Storage.LOW_STORAGE_THRESHOLD);
+ }
+
+ @Override
+ public void autoFocus() {
+ mFocusStartTime = System.currentTimeMillis();
+ mCameraDevice.autoFocus(mHandler, mAutoFocusCallback);
+ setCameraState(FOCUSING);
+ }
+
+ @Override
+ public void cancelAutoFocus() {
+ mCameraDevice.cancelAutoFocus();
+ setCameraState(IDLE);
+ setCameraParameters(UPDATE_PARAM_PREFERENCE);
+ }
+
+ // Preview area is touched. Handle touch focus.
+ @Override
+ public void onSingleTapUp(View view, int x, int y) {
+ if (mPaused || mCameraDevice == null || !mFirstTimeInitialized
+ || mCameraState == SNAPSHOT_IN_PROGRESS
+ || mCameraState == SWITCHING_CAMERA
+ || mCameraState == PREVIEW_STOPPED) {
+ return;
+ }
+
+ // Do not trigger touch focus if popup window is opened.
+ if (mUI.removeTopLevelPopup()) return;
+
+ // Check if metering area or focus area is supported.
+ if (!mFocusAreaSupported && !mMeteringAreaSupported) return;
+ mFocusManager.onSingleTapUp(x, y);
+ }
+
+ @Override
+ public boolean onBackPressed() {
+ return mUI.onBackPressed();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_FOCUS:
+ if (/*TODO: mActivity.isInCameraApp() &&*/ mFirstTimeInitialized) {
+ if (event.getRepeatCount() == 0) {
+ onShutterButtonFocus(true);
+ }
+ return true;
+ }
+ return false;
+ case KeyEvent.KEYCODE_CAMERA:
+ if (mFirstTimeInitialized && event.getRepeatCount() == 0) {
+ onShutterButtonClick();
+ }
+ return true;
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ // If we get a dpad center event without any focused view, move
+ // the focus to the shutter button and press it.
+ if (mFirstTimeInitialized && event.getRepeatCount() == 0) {
+ // Start auto-focus immediately to reduce shutter lag. After
+ // the shutter button gets the focus, onShutterButtonFocus()
+ // will be called again but it is fine.
+ if (mUI.removeTopLevelPopup()) return true;
+ onShutterButtonFocus(true);
+ mUI.pressShutterButton();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ if (/*mActivity.isInCameraApp() && */ mFirstTimeInitialized) {
+ onShutterButtonClick();
+ return true;
+ }
+ return false;
+ case KeyEvent.KEYCODE_FOCUS:
+ if (mFirstTimeInitialized) {
+ onShutterButtonFocus(false);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void closeCamera() {
+ if (mCameraDevice != null) {
+ mCameraDevice.setZoomChangeListener(null);
+ if(ApiHelper.HAS_FACE_DETECTION) {
+ mCameraDevice.setFaceDetectionCallback(null, null);
+ }
+ mCameraDevice.setErrorCallback(null);
+ CameraHolder.instance().release();
+ mFaceDetectionStarted = false;
+ mCameraDevice = null;
+ setCameraState(PREVIEW_STOPPED);
+ mFocusManager.onCameraReleased();
+ }
+ }
+
+ private void setDisplayOrientation() {
+ mDisplayRotation = Util.getDisplayRotation(mActivity);
+ mDisplayOrientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId);
+ mCameraDisplayOrientation = mDisplayOrientation;
+ mUI.setDisplayOrientation(mDisplayOrientation);
+ if (mFocusManager != null) {
+ mFocusManager.setDisplayOrientation(mDisplayOrientation);
+ }
+ // Change the camera display orientation
+ if (mCameraDevice != null) {
+ mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
+ }
+ }
+
+ // Only called by UI thread.
+ private void setupPreview() {
+ mFocusManager.resetTouchFocus();
+ startPreview();
+ setCameraState(IDLE);
+ startFaceDetection();
+ }
+
+ // This can be called by UI Thread or CameraStartUpThread. So this should
+ // not modify the views.
+ private void startPreview() {
+ mCameraDevice.setErrorCallback(mErrorCallback);
+
+ // ICS camera frameworks has a bug. Face detection state is not cleared
+ // after taking a picture. Stop the preview to work around it. The bug
+ // was fixed in JB.
+ if (mCameraState != PREVIEW_STOPPED) stopPreview();
+
+ setDisplayOrientation();
+
+ if (!mSnapshotOnIdle) {
+ // If the focus mode is continuous autofocus, call cancelAutoFocus to
+ // resume it because it may have been paused by autoFocus call.
+ if (Util.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusManager.getFocusMode())) {
+ mCameraDevice.cancelAutoFocus();
+ }
+ mFocusManager.setAeAwbLock(false); // Unlock AE and AWB.
+ }
+ setCameraParameters(UPDATE_PARAM_ALL);
+ // Let UI set its expected aspect ratio
+ mUI.setPreviewSize(mParameters.getPreviewSize());
+ Object st = mUI.getSurfaceTexture();
+ if (st != null) {
+ mCameraDevice.setPreviewTexture((SurfaceTexture) st);
+ }
+
+ Log.v(TAG, "startPreview");
+ mCameraDevice.startPreview();
+ mFocusManager.onPreviewStarted();
+
+ if (mSnapshotOnIdle) {
+ mHandler.post(mDoSnapRunnable);
+ }
+ }
+
+ @Override
+ public void stopPreview() {
+ if (mCameraDevice != null && mCameraState != PREVIEW_STOPPED) {
+ Log.v(TAG, "stopPreview");
+ mCameraDevice.stopPreview();
+ mFaceDetectionStarted = false;
+ }
+ setCameraState(PREVIEW_STOPPED);
+ if (mFocusManager != null) mFocusManager.onPreviewStopped();
+ }
+
+ @SuppressWarnings("deprecation")
+ private void updateCameraParametersInitialize() {
+ // Reset preview frame rate to the maximum because it may be lowered by
+ // video camera application.
+ int[] fpsRange = Util.getMaxPreviewFpsRange(mParameters);
+ if (fpsRange.length > 0) {
+ mParameters.setPreviewFpsRange(
+ fpsRange[Parameters.PREVIEW_FPS_MIN_INDEX],
+ fpsRange[Parameters.PREVIEW_FPS_MAX_INDEX]);
+ }
+
+ mParameters.set(Util.RECORDING_HINT, Util.FALSE);
+
+ // Disable video stabilization. Convenience methods not available in API
+ // level <= 14
+ String vstabSupported = mParameters.get("video-stabilization-supported");
+ if ("true".equals(vstabSupported)) {
+ mParameters.set("video-stabilization", "false");
+ }
+ }
+
+ private void updateCameraParametersZoom() {
+ // Set zoom.
+ if (mParameters.isZoomSupported()) {
+ mParameters.setZoom(mZoomValue);
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ private void setAutoExposureLockIfSupported() {
+ if (mAeLockSupported) {
+ mParameters.setAutoExposureLock(mFocusManager.getAeAwbLock());
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ private void setAutoWhiteBalanceLockIfSupported() {
+ if (mAwbLockSupported) {
+ mParameters.setAutoWhiteBalanceLock(mFocusManager.getAeAwbLock());
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void setFocusAreasIfSupported() {
+ if (mFocusAreaSupported) {
+ mParameters.setFocusAreas(mFocusManager.getFocusAreas());
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void setMeteringAreasIfSupported() {
+ if (mMeteringAreaSupported) {
+ // Use the same area for focus and metering.
+ mParameters.setMeteringAreas(mFocusManager.getMeteringAreas());
+ }
+ }
+
+ private void updateCameraParametersPreference() {
+ setAutoExposureLockIfSupported();
+ setAutoWhiteBalanceLockIfSupported();
+ setFocusAreasIfSupported();
+ setMeteringAreasIfSupported();
+
+ // Set picture size.
+ String pictureSize = mPreferences.getString(
+ CameraSettings.KEY_PICTURE_SIZE, null);
+ if (pictureSize == null) {
+ CameraSettings.initialCameraPictureSize(mActivity, mParameters);
+ } else {
+ List<Size> supported = mParameters.getSupportedPictureSizes();
+ CameraSettings.setCameraPictureSize(
+ pictureSize, supported, mParameters);
+ }
+ Size size = mParameters.getPictureSize();
+
+ // Set a preview size that is closest to the viewfinder height and has
+ // the right aspect ratio.
+ List<Size> sizes = mParameters.getSupportedPreviewSizes();
+ Size optimalSize = Util.getOptimalPreviewSize(mActivity, sizes,
+ (double) size.width / size.height);
+ Size original = mParameters.getPreviewSize();
+ if (!original.equals(optimalSize)) {
+ mParameters.setPreviewSize(optimalSize.width, optimalSize.height);
+
+ // Zoom related settings will be changed for different preview
+ // sizes, so set and read the parameters to get latest values
+ if (mHandler.getLooper() == Looper.myLooper()) {
+ // On UI thread only, not when camera starts up
+ setupPreview();
+ } else {
+ mCameraDevice.setParameters(mParameters);
+ }
+ mParameters = mCameraDevice.getParameters();
+ }
+ Log.v(TAG, "Preview size is " + optimalSize.width + "x" + optimalSize.height);
+
+ // Since changing scene mode may change supported values, set scene mode
+ // first. HDR is a scene mode. To promote it in UI, it is stored in a
+ // separate preference.
+ String hdr = mPreferences.getString(CameraSettings.KEY_CAMERA_HDR,
+ mActivity.getString(R.string.pref_camera_hdr_default));
+ if (mActivity.getString(R.string.setting_on_value).equals(hdr)) {
+ mSceneMode = Util.SCENE_MODE_HDR;
+ } else {
+ mSceneMode = mPreferences.getString(
+ CameraSettings.KEY_SCENE_MODE,
+ mActivity.getString(R.string.pref_camera_scenemode_default));
+ }
+ if (Util.isSupported(mSceneMode, mParameters.getSupportedSceneModes())) {
+ if (!mParameters.getSceneMode().equals(mSceneMode)) {
+ mParameters.setSceneMode(mSceneMode);
+
+ // Setting scene mode will change the settings of flash mode,
+ // white balance, and focus mode. Here we read back the
+ // parameters, so we can know those settings.
+ mCameraDevice.setParameters(mParameters);
+ mParameters = mCameraDevice.getParameters();
+ }
+ } else {
+ mSceneMode = mParameters.getSceneMode();
+ if (mSceneMode == null) {
+ mSceneMode = Parameters.SCENE_MODE_AUTO;
+ }
+ }
+
+ // Set JPEG quality.
+ int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId,
+ CameraProfile.QUALITY_HIGH);
+ mParameters.setJpegQuality(jpegQuality);
+
+ // For the following settings, we need to check if the settings are
+ // still supported by latest driver, if not, ignore the settings.
+
+ // Set exposure compensation
+ int value = CameraSettings.readExposure(mPreferences);
+ int max = mParameters.getMaxExposureCompensation();
+ int min = mParameters.getMinExposureCompensation();
+ if (value >= min && value <= max) {
+ mParameters.setExposureCompensation(value);
+ } else {
+ Log.w(TAG, "invalid exposure range: " + value);
+ }
+
+ if (Parameters.SCENE_MODE_AUTO.equals(mSceneMode)) {
+ // Set flash mode.
+ String flashMode = mPreferences.getString(
+ CameraSettings.KEY_FLASH_MODE,
+ mActivity.getString(R.string.pref_camera_flashmode_default));
+ List<String> supportedFlash = mParameters.getSupportedFlashModes();
+ if (Util.isSupported(flashMode, supportedFlash)) {
+ mParameters.setFlashMode(flashMode);
+ } else {
+ flashMode = mParameters.getFlashMode();
+ if (flashMode == null) {
+ flashMode = mActivity.getString(
+ R.string.pref_camera_flashmode_no_flash);
+ }
+ }
+
+ // Set white balance parameter.
+ String whiteBalance = mPreferences.getString(
+ CameraSettings.KEY_WHITE_BALANCE,
+ mActivity.getString(R.string.pref_camera_whitebalance_default));
+ if (Util.isSupported(whiteBalance,
+ mParameters.getSupportedWhiteBalance())) {
+ mParameters.setWhiteBalance(whiteBalance);
+ } else {
+ whiteBalance = mParameters.getWhiteBalance();
+ if (whiteBalance == null) {
+ whiteBalance = Parameters.WHITE_BALANCE_AUTO;
+ }
+ }
+
+ // Set focus mode.
+ mFocusManager.overrideFocusMode(null);
+ mParameters.setFocusMode(mFocusManager.getFocusMode());
+ } else {
+ mFocusManager.overrideFocusMode(mParameters.getFocusMode());
+ }
+
+ if (mContinousFocusSupported && ApiHelper.HAS_AUTO_FOCUS_MOVE_CALLBACK) {
+ updateAutoFocusMoveCallback();
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ private void updateAutoFocusMoveCallback() {
+ if (mParameters.getFocusMode().equals(Util.FOCUS_MODE_CONTINUOUS_PICTURE)) {
+ mCameraDevice.setAutoFocusMoveCallback(mHandler,
+ (CameraManager.CameraAFMoveCallback) mAutoFocusMoveCallback);
+ } else {
+ mCameraDevice.setAutoFocusMoveCallback(null, null);
+ }
+ }
+
+ // We separate the parameters into several subsets, so we can update only
+ // the subsets actually need updating. The PREFERENCE set needs extra
+ // locking because the preference can be changed from GLThread as well.
+ private void setCameraParameters(int updateSet) {
+ if ((updateSet & UPDATE_PARAM_INITIALIZE) != 0) {
+ updateCameraParametersInitialize();
+ }
+
+ if ((updateSet & UPDATE_PARAM_ZOOM) != 0) {
+ updateCameraParametersZoom();
+ }
+
+ if ((updateSet & UPDATE_PARAM_PREFERENCE) != 0) {
+ updateCameraParametersPreference();
+ }
+
+ mCameraDevice.setParameters(mParameters);
+ }
+
+ // If the Camera is idle, update the parameters immediately, otherwise
+ // accumulate them in mUpdateSet and update later.
+ private void setCameraParametersWhenIdle(int additionalUpdateSet) {
+ mUpdateSet |= additionalUpdateSet;
+ if (mCameraDevice == null) {
+ // We will update all the parameters when we open the device, so
+ // we don't need to do anything now.
+ mUpdateSet = 0;
+ return;
+ } else if (isCameraIdle()) {
+ setCameraParameters(mUpdateSet);
+ updateSceneMode();
+ mUpdateSet = 0;
+ } else {
+ if (!mHandler.hasMessages(SET_CAMERA_PARAMETERS_WHEN_IDLE)) {
+ mHandler.sendEmptyMessageDelayed(
+ SET_CAMERA_PARAMETERS_WHEN_IDLE, 1000);
+ }
+ }
+ }
+
+ public boolean isCameraIdle() {
+ return (mCameraState == IDLE) ||
+ (mCameraState == PREVIEW_STOPPED) ||
+ ((mFocusManager != null) && mFocusManager.isFocusCompleted()
+ && (mCameraState != SWITCHING_CAMERA));
+ }
+
+ public boolean isImageCaptureIntent() {
+ String action = mActivity.getIntent().getAction();
+ return (MediaStore.ACTION_IMAGE_CAPTURE.equals(action)
+ || CameraActivity.ACTION_IMAGE_CAPTURE_SECURE.equals(action));
+ }
+
+ private void setupCaptureParams() {
+ Bundle myExtras = mActivity.getIntent().getExtras();
+ if (myExtras != null) {
+ mSaveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
+ mCropValue = myExtras.getString("crop");
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged() {
+ // ignore the events after "onPause()"
+ if (mPaused) return;
+
+ boolean recordLocation = RecordLocationPreference.get(
+ mPreferences, mContentResolver);
+ mLocationManager.recordLocation(recordLocation);
+
+ setCameraParametersWhenIdle(UPDATE_PARAM_PREFERENCE);
+ mUI.updateOnScreenIndicators(mParameters, mPreferenceGroup, mPreferences);
+ }
+
+ @Override
+ public void onCameraPickerClicked(int cameraId) {
+ if (mPaused || mPendingSwitchCameraId != -1) return;
+
+ mPendingSwitchCameraId = cameraId;
+
+ Log.v(TAG, "Start to switch camera. cameraId=" + cameraId);
+ // We need to keep a preview frame for the animation before
+ // releasing the camera. This will trigger onPreviewTextureCopied.
+ //TODO: Need to animate the camera switch
+ switchCamera();
+ }
+
+ // Preview texture has been copied. Now camera can be released and the
+ // animation can be started.
+ @Override
+ public void onPreviewTextureCopied() {
+ mHandler.sendEmptyMessage(SWITCH_CAMERA);
+ }
+
+ @Override
+ public void onCaptureTextureCopied() {
+ }
+
+ @Override
+ public void onUserInteraction() {
+ if (!mActivity.isFinishing()) keepScreenOnAwhile();
+ }
+
+ private void resetScreenOn() {
+ mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+ mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ private void keepScreenOnAwhile() {
+ mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+ }
+
+ @Override
+ public void onOverriddenPreferencesClicked() {
+ if (mPaused) return;
+ mUI.showPreferencesToast();
+ }
+
+ private void showTapToFocusToast() {
+ // TODO: Use a toast?
+ new RotateTextToast(mActivity, R.string.tap_to_focus, 0).show();
+ // Clear the preference.
+ Editor editor = mPreferences.edit();
+ editor.putBoolean(CameraSettings.KEY_CAMERA_FIRST_USE_HINT_SHOWN, false);
+ editor.apply();
+ }
+
+ private void initializeCapabilities() {
+ mInitialParams = mCameraDevice.getParameters();
+ mFocusAreaSupported = Util.isFocusAreaSupported(mInitialParams);
+ mMeteringAreaSupported = Util.isMeteringAreaSupported(mInitialParams);
+ mAeLockSupported = Util.isAutoExposureLockSupported(mInitialParams);
+ mAwbLockSupported = Util.isAutoWhiteBalanceLockSupported(mInitialParams);
+ mContinousFocusSupported = mInitialParams.getSupportedFocusModes().contains(
+ Util.FOCUS_MODE_CONTINUOUS_PICTURE);
+ }
+
+ @Override
+ public void onCountDownFinished() {
+ mSnapshotOnIdle = false;
+ mFocusManager.doSnap();
+ mFocusManager.onShutterUp();
+ }
+
+ @Override
+ public void onShowSwitcherPopup() {
+ mUI.onShowSwitcherPopup();
+ }
+
+ @Override
+ public int onZoomChanged(int index) {
+ // Not useful to change zoom value when the activity is paused.
+ if (mPaused) return index;
+ mZoomValue = index;
+ if (mParameters == null || mCameraDevice == null) return index;
+ // Set zoom parameters asynchronously
+ mParameters.setZoom(mZoomValue);
+ mCameraDevice.setParameters(mParameters);
+ Parameters p = mCameraDevice.getParameters();
+ if (p != null) return p.getZoom();
+ return index;
+ }
+
+ @Override
+ public int getCameraState() {
+ return mCameraState;
+ }
+
+ @Override
+ public void onQueueStatus(boolean full) {
+ mUI.enableShutter(!full);
+ }
+
+ @Override
+ public void onMediaSaveServiceConnected(MediaSaveService s) {
+ // We set the listener only when both service and shutterbutton
+ // are initialized.
+ if (mFirstTimeInitialized) {
+ s.setListener(this);
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ int type = event.sensor.getType();
+ float[] data;
+ if (type == Sensor.TYPE_ACCELEROMETER) {
+ data = mGData;
+ } else if (type == Sensor.TYPE_MAGNETIC_FIELD) {
+ data = mMData;
+ } else {
+ // we should not be here.
+ return;
+ }
+ for (int i = 0; i < 3 ; i++) {
+ data[i] = event.values[i];
+ }
+ float[] orientation = new float[3];
+ SensorManager.getRotationMatrix(mR, null, mGData, mMData);
+ SensorManager.getOrientation(mR, orientation);
+ mHeading = (int) (orientation[0] * 180f / Math.PI) % 360;
+ if (mHeading < 0) {
+ mHeading += 360;
+ }
+ }
+
+ @Override
+ public void onSwitchMode(boolean toCamera) {
+ mUI.onSwitchMode(toCamera);
+ }
+
+/* Below is no longer needed, except to get rid of compile error
+ * TODO: Remove these
+ */
+
+ // TODO: Delete this function after old camera code is removed
+ @Override
+ public void onRestorePreferencesClicked() {}
+
+}
diff --git a/src/com/android/camera/PhotoUI.java b/src/com/android/camera/PhotoUI.java
new file mode 100644
index 000000000..d58ed7f13
--- /dev/null
+++ b/src/com/android/camera/PhotoUI.java
@@ -0,0 +1,864 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.graphics.Matrix;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.hardware.Camera.Face;
+import android.hardware.Camera.FaceDetectionListener;
+import android.hardware.Camera.Size;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.TextureView;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.Toast;
+
+import com.android.camera.CameraPreference.OnPreferenceChangedListener;
+import com.android.camera.FocusOverlayManager.FocusUI;
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.CameraControls;
+import com.android.camera.ui.CameraRootView;
+import com.android.camera.ui.CameraSwitcher;
+import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
+import com.android.camera.ui.CountDownView;
+import com.android.camera.ui.CountDownView.OnCountDownFinishedListener;
+import com.android.camera.ui.FaceView;
+import com.android.camera.ui.FocusIndicator;
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.PieRenderer.PieListener;
+import com.android.camera.ui.RenderOverlay;
+import com.android.camera.ui.ZoomRenderer;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.List;
+
+public class PhotoUI implements PieListener,
+ PreviewGestures.SingleTapListener,
+ FocusUI, TextureView.SurfaceTextureListener,
+ LocationManager.Listener, CameraRootView.MyDisplayListener,
+ CameraManager.CameraFaceDetectionCallback {
+
+ private static final String TAG = "CAM_UI";
+ private static final int UPDATE_TRANSFORM_MATRIX = 1;
+ private CameraActivity mActivity;
+ private PhotoController mController;
+ private PreviewGestures mGestures;
+
+ private View mRootView;
+ private Object mSurfaceTexture;
+
+ private AbstractSettingPopup mPopup;
+ private ShutterButton mShutterButton;
+ private CountDownView mCountDownView;
+
+ private FaceView mFaceView;
+ private RenderOverlay mRenderOverlay;
+ private View mReviewCancelButton;
+ private View mReviewDoneButton;
+ private View mReviewRetakeButton;
+
+ private View mMenuButton;
+ private View mBlocker;
+ private PhotoMenu mMenu;
+ private CameraSwitcher mSwitcher;
+ private CameraControls mCameraControls;
+ private AlertDialog mLocationDialog;
+
+ // Small indicators which show the camera settings in the viewfinder.
+ private OnScreenIndicators mOnScreenIndicators;
+
+ private PieRenderer mPieRenderer;
+ private ZoomRenderer mZoomRenderer;
+ private Toast mNotSelectableToast;
+
+ private int mZoomMax;
+ private List<Integer> mZoomRatios;
+
+ private int mPreviewWidth = 0;
+ private int mPreviewHeight = 0;
+ private float mSurfaceTextureUncroppedWidth;
+ private float mSurfaceTextureUncroppedHeight;
+
+ private View mPreviewThumb;
+ private ObjectAnimator mFlashAnim;
+ private View mFlashOverlay;
+
+ private SurfaceTextureSizeChangedListener mSurfaceTextureSizeListener;
+ private TextureView mTextureView;
+ private Matrix mMatrix = null;
+ private float mAspectRatio = 4f / 3f;
+ private final Object mLock = new Object();
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case UPDATE_TRANSFORM_MATRIX:
+ setTransformMatrix(mPreviewWidth, mPreviewHeight);
+ break;
+ default:
+ break;
+ }
+ }
+ };
+
+ public interface SurfaceTextureSizeChangedListener {
+ public void onSurfaceTextureSizeChanged(int uncroppedWidth, int uncroppedHeight);
+ }
+
+ private OnLayoutChangeListener mLayoutListener = new OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right,
+ int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ int width = right - left;
+ int height = bottom - top;
+ // Full-screen screennail
+ int w = width;
+ int h = height;
+ if (Util.getDisplayRotation(mActivity) % 180 != 0) {
+ w = height;
+ h = width;
+ }
+ if (mPreviewWidth != width || mPreviewHeight != height) {
+ mPreviewWidth = width;
+ mPreviewHeight = height;
+ onScreenSizeChanged(width, height, w, h);
+ mController.onScreenSizeChanged(width, height, w, h);
+ }
+ }
+ };
+
+ private ValueAnimator.AnimatorListener mAnimatorListener =
+ new ValueAnimator.AnimatorListener() {
+
+ @Override
+ public void onAnimationCancel(Animator arg0) {}
+
+ @Override
+ public void onAnimationEnd(Animator arg0) {
+ mFlashOverlay.setAlpha(0f);
+ mFlashOverlay.setVisibility(View.GONE);
+ mFlashAnim.removeListener(this);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator arg0) {}
+
+ @Override
+ public void onAnimationStart(Animator arg0) {}
+ };
+
+ public PhotoUI(CameraActivity activity, PhotoController controller, View parent) {
+ mActivity = activity;
+ mController = controller;
+ mRootView = parent;
+
+ mActivity.getLayoutInflater().inflate(R.layout.photo_module,
+ (ViewGroup) mRootView, true);
+ mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay);
+ mFlashOverlay = mRootView.findViewById(R.id.flash_overlay);
+ // display the view
+ mTextureView = (TextureView) mRootView.findViewById(R.id.preview_content);
+ mTextureView.setSurfaceTextureListener(this);
+ mTextureView.addOnLayoutChangeListener(mLayoutListener);
+ initIndicators();
+
+ mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button);
+ mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher);
+ mSwitcher.setCurrentIndex(CameraSwitcher.PHOTO_MODULE_INDEX);
+ mSwitcher.setSwitchListener((CameraSwitchListener) mActivity);
+ mMenuButton = mRootView.findViewById(R.id.menu);
+ if (ApiHelper.HAS_FACE_DETECTION) {
+ ViewStub faceViewStub = (ViewStub) mRootView
+ .findViewById(R.id.face_view_stub);
+ if (faceViewStub != null) {
+ faceViewStub.inflate();
+ mFaceView = (FaceView) mRootView.findViewById(R.id.face_view);
+ setSurfaceTextureSizeChangedListener(
+ (SurfaceTextureSizeChangedListener) mFaceView);
+ }
+ }
+ mCameraControls = (CameraControls) mRootView.findViewById(R.id.camera_controls);
+ ((CameraRootView) mRootView).setDisplayChangeListener(this);
+ }
+
+ public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) {
+ setTransformMatrix(width, height);
+ }
+
+ public void setSurfaceTextureSizeChangedListener(SurfaceTextureSizeChangedListener listener) {
+ mSurfaceTextureSizeListener = listener;
+ }
+
+ public void setPreviewSize(Size size) {
+ int width = size.width;
+ int height = size.height;
+ if (width == 0 || height == 0) {
+ Log.w(TAG, "Preview size should not be 0.");
+ return;
+ }
+ if (width > height) {
+ mAspectRatio = (float) width / height;
+ } else {
+ mAspectRatio = (float) height / width;
+ }
+ mHandler.sendEmptyMessage(UPDATE_TRANSFORM_MATRIX);
+ }
+
+ private void setTransformMatrix(int width, int height) {
+ mMatrix = mTextureView.getTransform(mMatrix);
+ int orientation = Util.getDisplayRotation(mActivity);
+ float scaleX = 1f, scaleY = 1f;
+ float scaledTextureWidth, scaledTextureHeight;
+ if (width > height) {
+ scaledTextureWidth = Math.max(width,
+ (int) (height * mAspectRatio));
+ scaledTextureHeight = Math.max(height,
+ (int)(width / mAspectRatio));
+ } else {
+ scaledTextureWidth = Math.max(width,
+ (int) (height / mAspectRatio));
+ scaledTextureHeight = Math.max(height,
+ (int) (width * mAspectRatio));
+ }
+
+ if (mSurfaceTextureUncroppedWidth != scaledTextureWidth ||
+ mSurfaceTextureUncroppedHeight != scaledTextureHeight) {
+ mSurfaceTextureUncroppedWidth = scaledTextureWidth;
+ mSurfaceTextureUncroppedHeight = scaledTextureHeight;
+ if (mSurfaceTextureSizeListener != null) {
+ mSurfaceTextureSizeListener.onSurfaceTextureSizeChanged(
+ (int) mSurfaceTextureUncroppedWidth, (int) mSurfaceTextureUncroppedHeight);
+ }
+ }
+ scaleX = scaledTextureWidth / width;
+ scaleY = scaledTextureHeight / height;
+ mMatrix.setScale(scaleX, scaleY, (float) width / 2, (float) height / 2);
+ mTextureView.setTransform(mMatrix);
+ }
+
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ synchronized (mLock) {
+ mSurfaceTexture = surface;
+ mLock.notifyAll();
+ }
+ }
+
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ // Ignored, Camera does all the work for us
+ }
+
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ mSurfaceTexture = null;
+ mController.stopPreview();
+ Log.w(TAG, "surfaceTexture is destroyed");
+ return true;
+ }
+
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ // Invoked every time there's a new Camera preview frame
+ }
+
+ public View getRootView() {
+ return mRootView;
+ }
+
+ private void initIndicators() {
+ mOnScreenIndicators = new OnScreenIndicators(mActivity,
+ mRootView.findViewById(R.id.on_screen_indicators));
+ }
+
+ public void onCameraOpened(PreferenceGroup prefGroup, ComboPreferences prefs,
+ Camera.Parameters params, OnPreferenceChangedListener listener) {
+ if (mPieRenderer == null) {
+ mPieRenderer = new PieRenderer(mActivity);
+ mPieRenderer.setPieListener(this);
+ mRenderOverlay.addRenderer(mPieRenderer);
+ }
+
+ if (mMenu == null) {
+ mMenu = new PhotoMenu(mActivity, this, mPieRenderer);
+ mMenu.setListener(listener);
+ }
+ mMenu.initialize(prefGroup);
+
+ if (mZoomRenderer == null) {
+ mZoomRenderer = new ZoomRenderer(mActivity);
+ mRenderOverlay.addRenderer(mZoomRenderer);
+ }
+
+ if (mGestures == null) {
+ // this will handle gesture disambiguation and dispatching
+ mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer);
+ mRenderOverlay.setGestures(mGestures);
+ }
+ mGestures.setZoomEnabled(params.isZoomSupported());
+ mGestures.setRenderOverlay(mRenderOverlay);
+ mRenderOverlay.requestLayout();
+
+ initializeZoom(params);
+ updateOnScreenIndicators(params, prefGroup, prefs);
+ }
+
+ private void openMenu() {
+ if (mPieRenderer != null) {
+ // If autofocus is not finished, cancel autofocus so that the
+ // subsequent touch can be handled by PreviewGestures
+ if (mController.getCameraState() == PhotoController.FOCUSING) {
+ mController.cancelAutoFocus();
+ }
+ mPieRenderer.showInCenter();
+ }
+ }
+
+ public void initializeControlByIntent() {
+ mBlocker = mRootView.findViewById(R.id.blocker);
+ mPreviewThumb = mRootView.findViewById(R.id.preview_thumb);
+ mPreviewThumb.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // TODO: go to filmstrip
+ // mActivity.gotoGallery();
+ }
+ });
+ mMenuButton = mRootView.findViewById(R.id.menu);
+ mMenuButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ openMenu();
+ }
+ });
+ if (mController.isImageCaptureIntent()) {
+ hideSwitcher();
+ ViewGroup cameraControls = (ViewGroup) mRootView.findViewById(R.id.camera_controls);
+ mActivity.getLayoutInflater().inflate(R.layout.review_module_control, cameraControls);
+
+ mReviewDoneButton = mRootView.findViewById(R.id.btn_done);
+ mReviewCancelButton = mRootView.findViewById(R.id.btn_cancel);
+ mReviewRetakeButton = mRootView.findViewById(R.id.btn_retake);
+ mReviewCancelButton.setVisibility(View.VISIBLE);
+
+ mReviewDoneButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mController.onCaptureDone();
+ }
+ });
+ mReviewCancelButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mController.onCaptureCancelled();
+ }
+ });
+
+ mReviewRetakeButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mController.onCaptureRetake();
+ }
+ });
+ }
+ }
+
+ public void hideUI() {
+ mCameraControls.setVisibility(View.INVISIBLE);
+ mSwitcher.closePopup();
+ }
+
+ public void showUI() {
+ mCameraControls.setVisibility(View.VISIBLE);
+ }
+
+ public void hideSwitcher() {
+ mSwitcher.closePopup();
+ mSwitcher.setVisibility(View.INVISIBLE);
+ }
+
+ public void showSwitcher() {
+ mSwitcher.setVisibility(View.VISIBLE);
+ }
+ // called from onResume but only the first time
+ public void initializeFirstTime() {
+ // Initialize shutter button.
+ mShutterButton.setImageResource(R.drawable.btn_new_shutter);
+ mShutterButton.setOnShutterButtonListener(mController);
+ mShutterButton.setVisibility(View.VISIBLE);
+ }
+
+ // called from onResume every other time
+ public void initializeSecondTime(Camera.Parameters params) {
+ initializeZoom(params);
+ if (mController.isImageCaptureIntent()) {
+ hidePostCaptureAlert();
+ }
+ if (mMenu != null) {
+ mMenu.reloadPreferences();
+ }
+ }
+
+ public void showLocationDialog() {
+ mLocationDialog = new AlertDialog.Builder(mActivity)
+ .setTitle(R.string.remember_location_title)
+ .setMessage(R.string.remember_location_prompt)
+ .setPositiveButton(R.string.remember_location_yes,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int arg1) {
+ mController.enableRecordingLocation(true);
+ mLocationDialog = null;
+ }
+ })
+ .setNegativeButton(R.string.remember_location_no,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int arg1) {
+ dialog.cancel();
+ }
+ })
+ .setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ mController.enableRecordingLocation(false);
+ mLocationDialog = null;
+ }
+ })
+ .show();
+ }
+
+ public void initializeZoom(Camera.Parameters params) {
+ if ((params == null) || !params.isZoomSupported()
+ || (mZoomRenderer == null)) return;
+ mZoomMax = params.getMaxZoom();
+ mZoomRatios = params.getZoomRatios();
+ // Currently we use immediate zoom for fast zooming to get better UX and
+ // there is no plan to take advantage of the smooth zoom.
+ if (mZoomRenderer != null) {
+ mZoomRenderer.setZoomMax(mZoomMax);
+ mZoomRenderer.setZoom(params.getZoom());
+ mZoomRenderer.setZoomValue(mZoomRatios.get(params.getZoom()));
+ mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener());
+ }
+ }
+
+ public void showGpsOnScreenIndicator(boolean hasSignal) { }
+
+ public void hideGpsOnScreenIndicator() { }
+
+ public void overrideSettings(final String ... keyvalues) {
+ mMenu.overrideSettings(keyvalues);
+ }
+
+ public void updateOnScreenIndicators(Camera.Parameters params,
+ PreferenceGroup group, ComboPreferences prefs) {
+ if (params == null) return;
+ mOnScreenIndicators.updateSceneOnScreenIndicator(params.getSceneMode());
+ mOnScreenIndicators.updateExposureOnScreenIndicator(params,
+ CameraSettings.readExposure(prefs));
+ mOnScreenIndicators.updateFlashOnScreenIndicator(params.getFlashMode());
+ int wbIndex = 2;
+ ListPreference pref = group.findPreference(CameraSettings.KEY_WHITE_BALANCE);
+ if (pref != null) {
+ wbIndex = pref.getCurrentIndex();
+ }
+ mOnScreenIndicators.updateWBIndicator(wbIndex);
+ boolean location = RecordLocationPreference.get(
+ prefs, mActivity.getContentResolver());
+ mOnScreenIndicators.updateLocationIndicator(location);
+ }
+
+ public void setCameraState(int state) {
+ }
+
+ public void animateFlash() {
+ // End the previous animation if the previous one is still running
+ if (mFlashAnim != null && mFlashAnim.isRunning()) {
+ mFlashAnim.end();
+ }
+ // Start new flash animation.
+ mFlashOverlay.setVisibility(View.VISIBLE);
+ mFlashAnim = ObjectAnimator.ofFloat((Object) mFlashOverlay, "alpha", 0.3f, 0f);
+ mFlashAnim.setDuration(300);
+ mFlashAnim.addListener(mAnimatorListener);
+ mFlashAnim.start();
+ }
+
+ public void enableGestures(boolean enable) {
+ if (mGestures != null) {
+ mGestures.setEnabled(enable);
+ }
+ }
+
+ // forward from preview gestures to controller
+ @Override
+ public void onSingleTapUp(View view, int x, int y) {
+ mController.onSingleTapUp(view, x, y);
+ }
+
+ public boolean onBackPressed() {
+ if (mPieRenderer != null && mPieRenderer.showsItems()) {
+ mPieRenderer.hide();
+ return true;
+ }
+ // In image capture mode, back button should:
+ // 1) if there is any popup, dismiss them, 2) otherwise, get out of
+ // image capture
+ if (mController.isImageCaptureIntent()) {
+ if (!removeTopLevelPopup()) {
+ // no popup to dismiss, cancel image capture
+ mController.onCaptureCancelled();
+ }
+ return true;
+ } else if (!mController.isCameraIdle()) {
+ // ignore backs while we're taking a picture
+ return true;
+ } else {
+ return removeTopLevelPopup();
+ }
+ }
+
+ public void onSwitchMode(boolean toCamera) {
+ if (toCamera) {
+ showUI();
+ } else {
+ hideUI();
+ }
+ if (mFaceView != null) {
+ mFaceView.setBlockDraw(!toCamera);
+ }
+ if (mPopup != null) {
+ dismissPopup(toCamera);
+ }
+ if (mGestures != null) {
+ mGestures.setEnabled(toCamera);
+ }
+ if (mRenderOverlay != null) {
+ // this can not happen in capture mode
+ mRenderOverlay.setVisibility(toCamera ? View.VISIBLE : View.GONE);
+ }
+ if (mPieRenderer != null) {
+ mPieRenderer.setBlockFocus(!toCamera);
+ }
+ setShowMenu(toCamera);
+ if (!toCamera && mCountDownView != null) mCountDownView.cancelCountDown();
+ }
+
+ public void enablePreviewThumb(boolean enabled) {
+ if (enabled) {
+ mPreviewThumb.setVisibility(View.VISIBLE);
+ } else {
+ mPreviewThumb.setVisibility(View.GONE);
+ }
+ }
+
+ public boolean removeTopLevelPopup() {
+ // Remove the top level popup or dialog box and return true if there's any
+ if (mPopup != null) {
+ dismissPopup();
+ return true;
+ }
+ return false;
+ }
+
+ public void showPopup(AbstractSettingPopup popup) {
+ hideUI();
+ mBlocker.setVisibility(View.INVISIBLE);
+ setShowMenu(false);
+ mPopup = popup;
+ mPopup.setVisibility(View.VISIBLE);
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ lp.gravity = Gravity.CENTER;
+ ((FrameLayout) mRootView).addView(mPopup, lp);
+ }
+
+ public void dismissPopup() {
+ dismissPopup(true);
+ }
+
+ private void dismissPopup(boolean fullScreen) {
+ if (fullScreen) {
+ showUI();
+ mBlocker.setVisibility(View.VISIBLE);
+ }
+ setShowMenu(fullScreen);
+ if (mPopup != null) {
+ ((FrameLayout) mRootView).removeView(mPopup);
+ mPopup = null;
+ }
+ mMenu.popupDismissed();
+ }
+
+ public void onShowSwitcherPopup() {
+ if (mPieRenderer != null && mPieRenderer.showsItems()) {
+ mPieRenderer.hide();
+ }
+ }
+
+ private void setShowMenu(boolean show) {
+ if (mOnScreenIndicators != null) {
+ mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+ if (mMenuButton != null) {
+ mMenuButton.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ public boolean collapseCameraControls() {
+ // Remove all the popups/dialog boxes
+ boolean ret = false;
+ if (mPopup != null) {
+ dismissPopup();
+ ret = true;
+ }
+ onShowSwitcherPopup();
+ return ret;
+ }
+
+ protected void showPostCaptureAlert() {
+ mOnScreenIndicators.setVisibility(View.GONE);
+ mMenuButton.setVisibility(View.GONE);
+ Util.fadeIn(mReviewDoneButton);
+ mShutterButton.setVisibility(View.INVISIBLE);
+ Util.fadeIn(mReviewRetakeButton);
+ pauseFaceDetection();
+ }
+
+ protected void hidePostCaptureAlert() {
+ mOnScreenIndicators.setVisibility(View.VISIBLE);
+ mMenuButton.setVisibility(View.VISIBLE);
+ Util.fadeOut(mReviewDoneButton);
+ mShutterButton.setVisibility(View.VISIBLE);
+ Util.fadeOut(mReviewRetakeButton);
+ resumeFaceDetection();
+ }
+
+ public void setDisplayOrientation(int orientation) {
+ if (mFaceView != null) {
+ mFaceView.setDisplayOrientation(orientation);
+ }
+ }
+
+ // shutter button handling
+
+ public boolean isShutterPressed() {
+ return mShutterButton.isPressed();
+ }
+
+ public void enableShutter(boolean enabled) {
+ if (mShutterButton != null) {
+ mShutterButton.setEnabled(enabled);
+ }
+ }
+
+ public void pressShutterButton() {
+ if (mShutterButton.isInTouchMode()) {
+ mShutterButton.requestFocusFromTouch();
+ } else {
+ mShutterButton.requestFocus();
+ }
+ mShutterButton.setPressed(true);
+ }
+
+ private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener {
+ @Override
+ public void onZoomValueChanged(int index) {
+ int newZoom = mController.onZoomChanged(index);
+ if (mZoomRenderer != null) {
+ mZoomRenderer.setZoomValue(mZoomRatios.get(newZoom));
+ }
+ }
+
+ @Override
+ public void onZoomStart() {
+ if (mPieRenderer != null) {
+ mPieRenderer.setBlockFocus(true);
+ }
+ }
+
+ @Override
+ public void onZoomEnd() {
+ if (mPieRenderer != null) {
+ mPieRenderer.setBlockFocus(false);
+ }
+ }
+ }
+
+ @Override
+ public void onPieOpened(int centerX, int centerY) {
+ setSwipingEnabled(false);
+ dismissPopup();
+ if (mFaceView != null) {
+ mFaceView.setBlockDraw(true);
+ }
+ }
+
+ @Override
+ public void onPieClosed() {
+ setSwipingEnabled(true);
+ if (mFaceView != null) {
+ mFaceView.setBlockDraw(false);
+ }
+ }
+
+ public void setSwipingEnabled(boolean enable) {
+ mActivity.setSwipingEnabled(enable);
+ }
+
+ public Object getSurfaceTexture() {
+ synchronized (mLock) {
+ if (mSurfaceTexture == null) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Unexpected interruption when waiting to get surface texture");
+ }
+ }
+ }
+ return mSurfaceTexture;
+ }
+
+ // Countdown timer
+
+ private void initializeCountDown() {
+ mActivity.getLayoutInflater().inflate(R.layout.count_down_to_capture,
+ (ViewGroup) mRootView, true);
+ mCountDownView = (CountDownView) (mRootView.findViewById(R.id.count_down_to_capture));
+ mCountDownView.setCountDownFinishedListener((OnCountDownFinishedListener) mController);
+ }
+
+ public boolean isCountingDown() {
+ return mCountDownView != null && mCountDownView.isCountingDown();
+ }
+
+ public void cancelCountDown() {
+ if (mCountDownView == null) return;
+ mCountDownView.cancelCountDown();
+ }
+
+ public void startCountDown(int sec, boolean playSound) {
+ if (mCountDownView == null) initializeCountDown();
+ mCountDownView.startCountDown(sec, playSound);
+ }
+
+ public void showPreferencesToast() {
+ if (mNotSelectableToast == null) {
+ String str = mActivity.getResources().getString(R.string.not_selectable_in_scene_mode);
+ mNotSelectableToast = Toast.makeText(mActivity, str, Toast.LENGTH_SHORT);
+ }
+ mNotSelectableToast.show();
+ }
+
+ public void onPause() {
+ cancelCountDown();
+
+ // Clear UI.
+ collapseCameraControls();
+ if (mFaceView != null) mFaceView.clear();
+
+ if (mLocationDialog != null && mLocationDialog.isShowing()) {
+ mLocationDialog.dismiss();
+ }
+ mLocationDialog = null;
+ mPreviewWidth = 0;
+ mPreviewHeight = 0;
+ }
+
+ // focus UI implementation
+
+ private FocusIndicator getFocusIndicator() {
+ return (mFaceView != null && mFaceView.faceExists()) ? mFaceView : mPieRenderer;
+ }
+
+ @Override
+ public boolean hasFaces() {
+ return (mFaceView != null && mFaceView.faceExists());
+ }
+
+ public void clearFaces() {
+ if (mFaceView != null) mFaceView.clear();
+ }
+
+ @Override
+ public void clearFocus() {
+ FocusIndicator indicator = getFocusIndicator();
+ if (indicator != null) indicator.clear();
+ }
+
+ @Override
+ public void setFocusPosition(int x, int y) {
+ mPieRenderer.setFocus(x, y);
+ }
+
+ @Override
+ public void onFocusStarted() {
+ getFocusIndicator().showStart();
+ }
+
+ @Override
+ public void onFocusSucceeded(boolean timeout) {
+ getFocusIndicator().showSuccess(timeout);
+ }
+
+ @Override
+ public void onFocusFailed(boolean timeout) {
+ getFocusIndicator().showFail(timeout);
+ }
+
+ @Override
+ public void pauseFaceDetection() {
+ if (mFaceView != null) mFaceView.pause();
+ }
+
+ @Override
+ public void resumeFaceDetection() {
+ if (mFaceView != null) mFaceView.resume();
+ }
+
+ public void onStartFaceDetection(int orientation, boolean mirror) {
+ mFaceView.clear();
+ mFaceView.setVisibility(View.VISIBLE);
+ mFaceView.setDisplayOrientation(orientation);
+ mFaceView.setMirror(mirror);
+ mFaceView.resume();
+ }
+
+ @Override
+ public void onFaceDetection(Face[] faces, CameraManager.CameraProxy camera) {
+ mFaceView.setFaces(faces);
+ }
+
+ public void onDisplayChanged() {
+ mCameraControls.checkLayoutFlip();
+ mController.updateCameraOrientation();
+ }
+
+}
diff --git a/src/com/android/camera/PieController.java b/src/com/android/camera/PieController.java
new file mode 100644
index 000000000..3cbcb4bf5
--- /dev/null
+++ b/src/com/android/camera/PieController.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import com.android.camera.CameraPreference.OnPreferenceChangedListener;
+import com.android.camera.drawable.TextDrawable;
+import com.android.camera.ui.PieItem;
+import com.android.camera.ui.PieItem.OnClickListener;
+import com.android.camera.ui.PieRenderer;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class PieController {
+
+ private static String TAG = "CAM_piecontrol";
+
+ protected static final int MODE_PHOTO = 0;
+ protected static final int MODE_VIDEO = 1;
+
+ protected static float CENTER = (float) Math.PI / 2;
+ protected static final float SWEEP = 0.06f;
+
+ protected Activity mActivity;
+ protected PreferenceGroup mPreferenceGroup;
+ protected OnPreferenceChangedListener mListener;
+ protected PieRenderer mRenderer;
+ private List<IconListPreference> mPreferences;
+ private Map<IconListPreference, PieItem> mPreferenceMap;
+ private Map<IconListPreference, String> mOverrides;
+
+ public void setListener(OnPreferenceChangedListener listener) {
+ mListener = listener;
+ }
+
+ public PieController(Activity activity, PieRenderer pie) {
+ mActivity = activity;
+ mRenderer = pie;
+ mPreferences = new ArrayList<IconListPreference>();
+ mPreferenceMap = new HashMap<IconListPreference, PieItem>();
+ mOverrides = new HashMap<IconListPreference, String>();
+ }
+
+ public void initialize(PreferenceGroup group) {
+ mRenderer.clearItems();
+ mPreferenceMap.clear();
+ setPreferenceGroup(group);
+ }
+
+ public void onSettingChanged(ListPreference pref) {
+ if (mListener != null) {
+ mListener.onSharedPreferenceChanged();
+ }
+ }
+
+ protected void setCameraId(int cameraId) {
+ ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+ pref.setValue("" + cameraId);
+ }
+
+ protected PieItem makeItem(int resId) {
+ // We need a mutable version as we change the alpha
+ Drawable d = mActivity.getResources().getDrawable(resId).mutate();
+ return new PieItem(d, 0);
+ }
+
+ protected PieItem makeItem(CharSequence value) {
+ TextDrawable drawable = new TextDrawable(mActivity.getResources(), value);
+ return new PieItem(drawable, 0);
+ }
+
+ public PieItem makeItem(String prefKey) {
+ final IconListPreference pref =
+ (IconListPreference) mPreferenceGroup.findPreference(prefKey);
+ if (pref == null) return null;
+ int[] iconIds = pref.getLargeIconIds();
+ int resid = -1;
+ if (!pref.getUseSingleIcon() && iconIds != null) {
+ // Each entry has a corresponding icon.
+ int index = pref.findIndexOfValue(pref.getValue());
+ resid = iconIds[index];
+ } else {
+ // The preference only has a single icon to represent it.
+ resid = pref.getSingleIcon();
+ }
+ PieItem item = makeItem(resid);
+ item.setLabel(pref.getTitle().toUpperCase());
+ mPreferences.add(pref);
+ mPreferenceMap.put(pref, item);
+ int nOfEntries = pref.getEntries().length;
+ if (nOfEntries > 1) {
+ for (int i = 0; i < nOfEntries; i++) {
+ PieItem inner = null;
+ if (iconIds != null) {
+ inner = makeItem(iconIds[i]);
+ } else {
+ inner = makeItem(pref.getEntries()[i]);
+ }
+ inner.setLabel(pref.getLabels()[i]);
+ item.addItem(inner);
+ final int index = i;
+ inner.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(PieItem item) {
+ pref.setValueIndex(index);
+ reloadPreference(pref);
+ onSettingChanged(pref);
+ }
+ });
+ }
+ }
+ return item;
+ }
+
+ public PieItem makeSwitchItem(final String prefKey, boolean addListener) {
+ final IconListPreference pref =
+ (IconListPreference) mPreferenceGroup.findPreference(prefKey);
+ if (pref == null) return null;
+ int[] iconIds = pref.getLargeIconIds();
+ int resid = -1;
+ int index = pref.findIndexOfValue(pref.getValue());
+ if (!pref.getUseSingleIcon() && iconIds != null) {
+ // Each entry has a corresponding icon.
+ resid = iconIds[index];
+ } else {
+ // The preference only has a single icon to represent it.
+ resid = pref.getSingleIcon();
+ }
+ PieItem item = makeItem(resid);
+ item.setLabel(pref.getLabels()[index]);
+ item.setImageResource(mActivity, resid);
+ mPreferences.add(pref);
+ mPreferenceMap.put(pref, item);
+ if (addListener) {
+ final PieItem fitem = item;
+ item.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(PieItem item) {
+ IconListPreference pref = (IconListPreference) mPreferenceGroup
+ .findPreference(prefKey);
+ int index = pref.findIndexOfValue(pref.getValue());
+ CharSequence[] values = pref.getEntryValues();
+ index = (index + 1) % values.length;
+ pref.setValueIndex(index);
+ fitem.setLabel(pref.getLabels()[index]);
+ fitem.setImageResource(mActivity,
+ ((IconListPreference) pref).getLargeIconIds()[index]);
+ reloadPreference(pref);
+ onSettingChanged(pref);
+ }
+ });
+ }
+ return item;
+ }
+
+
+ public PieItem makeDialItem(ListPreference pref, int iconId, float center, float sweep) {
+ PieItem item = makeItem(iconId);
+ return item;
+ }
+
+ public void addItem(String prefKey) {
+ PieItem item = makeItem(prefKey);
+ mRenderer.addItem(item);
+ }
+
+ public void updateItem(PieItem item, String prefKey) {
+ IconListPreference pref = (IconListPreference) mPreferenceGroup
+ .findPreference(prefKey);
+ if (pref != null) {
+ int index = pref.findIndexOfValue(pref.getValue());
+ item.setLabel(pref.getLabels()[index]);
+ item.setImageResource(mActivity,
+ ((IconListPreference) pref).getLargeIconIds()[index]);
+ }
+ }
+
+ public void setPreferenceGroup(PreferenceGroup group) {
+ mPreferenceGroup = group;
+ }
+
+ public void reloadPreferences() {
+ mPreferenceGroup.reloadValue();
+ for (IconListPreference pref : mPreferenceMap.keySet()) {
+ reloadPreference(pref);
+ }
+ }
+
+ private void reloadPreference(IconListPreference pref) {
+ if (pref.getUseSingleIcon()) return;
+ PieItem item = mPreferenceMap.get(pref);
+ String overrideValue = mOverrides.get(pref);
+ int[] iconIds = pref.getLargeIconIds();
+ if (iconIds != null) {
+ // Each entry has a corresponding icon.
+ int index;
+ if (overrideValue == null) {
+ index = pref.findIndexOfValue(pref.getValue());
+ } else {
+ index = pref.findIndexOfValue(overrideValue);
+ if (index == -1) {
+ // Avoid the crash if camera driver has bugs.
+ Log.e(TAG, "Fail to find override value=" + overrideValue);
+ pref.print();
+ return;
+ }
+ }
+ item.setImageResource(mActivity, iconIds[index]);
+ } else {
+ // The preference only has a single icon to represent it.
+ item.setImageResource(mActivity, pref.getSingleIcon());
+ }
+ }
+
+ // Scene mode may override other camera settings (ex: flash mode).
+ public void overrideSettings(final String ... keyvalues) {
+ if (keyvalues.length % 2 != 0) {
+ throw new IllegalArgumentException();
+ }
+ for (IconListPreference pref : mPreferenceMap.keySet()) {
+ override(pref, keyvalues);
+ }
+ }
+
+ private void override(IconListPreference pref, final String ... keyvalues) {
+ mOverrides.remove(pref);
+ for (int i = 0; i < keyvalues.length; i += 2) {
+ String key = keyvalues[i];
+ String value = keyvalues[i + 1];
+ if (key.equals(pref.getKey())) {
+ mOverrides.put(pref, value);
+ PieItem item = mPreferenceMap.get(pref);
+ item.setEnabled(value == null);
+ break;
+ }
+ }
+ reloadPreference(pref);
+ }
+}
diff --git a/src/com/android/camera/PreferenceGroup.java b/src/com/android/camera/PreferenceGroup.java
new file mode 100644
index 000000000..4d0519f4e
--- /dev/null
+++ b/src/com/android/camera/PreferenceGroup.java
@@ -0,0 +1,79 @@
+/*
+ * 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.camera;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import java.util.ArrayList;
+
+/**
+ * A collection of <code>CameraPreference</code>s. It may contain other
+ * <code>PreferenceGroup</code> and form a tree structure.
+ */
+public class PreferenceGroup extends CameraPreference {
+ private ArrayList<CameraPreference> list =
+ new ArrayList<CameraPreference>();
+
+ public PreferenceGroup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void addChild(CameraPreference child) {
+ list.add(child);
+ }
+
+ public void removePreference(int index) {
+ list.remove(index);
+ }
+
+ public CameraPreference get(int index) {
+ return list.get(index);
+ }
+
+ public int size() {
+ return list.size();
+ }
+
+ @Override
+ public void reloadValue() {
+ for (CameraPreference pref : list) {
+ pref.reloadValue();
+ }
+ }
+
+ /**
+ * Finds the preference with the given key recursively. Returns
+ * <code>null</code> if cannot find.
+ */
+ public ListPreference findPreference(String key) {
+ // Find a leaf preference with the given key. Currently, the base
+ // type of all "leaf" preference is "ListPreference". If we add some
+ // other types later, we need to change the code.
+ for (CameraPreference pref : list) {
+ if (pref instanceof ListPreference) {
+ ListPreference listPref = (ListPreference) pref;
+ if(listPref.getKey().equals(key)) return listPref;
+ } else if(pref instanceof PreferenceGroup) {
+ ListPreference listPref =
+ ((PreferenceGroup) pref).findPreference(key);
+ if (listPref != null) return listPref;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/camera/PreferenceInflater.java b/src/com/android/camera/PreferenceInflater.java
new file mode 100644
index 000000000..231c9833b
--- /dev/null
+++ b/src/com/android/camera/PreferenceInflater.java
@@ -0,0 +1,108 @@
+/*
+ * 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.camera;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Xml;
+import android.view.InflateException;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Inflate <code>CameraPreference</code> from XML resource.
+ */
+public class PreferenceInflater {
+ private static final String PACKAGE_NAME =
+ PreferenceInflater.class.getPackage().getName();
+
+ private static final Class<?>[] CTOR_SIGNATURE =
+ new Class[] {Context.class, AttributeSet.class};
+ private static final HashMap<String, Constructor<?>> sConstructorMap =
+ new HashMap<String, Constructor<?>>();
+
+ private Context mContext;
+
+ public PreferenceInflater(Context context) {
+ mContext = context;
+ }
+
+ public CameraPreference inflate(int resId) {
+ return inflate(mContext.getResources().getXml(resId));
+ }
+
+ private CameraPreference newPreference(String tagName, Object[] args) {
+ String name = PACKAGE_NAME + "." + tagName;
+ Constructor<?> constructor = sConstructorMap.get(name);
+ try {
+ if (constructor == null) {
+ // Class not found in the cache, see if it's real, and try to
+ // add it
+ Class<?> clazz = mContext.getClassLoader().loadClass(name);
+ constructor = clazz.getConstructor(CTOR_SIGNATURE);
+ sConstructorMap.put(name, constructor);
+ }
+ return (CameraPreference) constructor.newInstance(args);
+ } catch (NoSuchMethodException e) {
+ throw new InflateException("Error inflating class " + name, e);
+ } catch (ClassNotFoundException e) {
+ throw new InflateException("No such class: " + name, e);
+ } catch (Exception e) {
+ throw new InflateException("While create instance of" + name, e);
+ }
+ }
+
+ private CameraPreference inflate(XmlPullParser parser) {
+
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ ArrayList<CameraPreference> list = new ArrayList<CameraPreference>();
+ Object args[] = new Object[]{mContext, attrs};
+
+ try {
+ for (int type = parser.next();
+ type != XmlPullParser.END_DOCUMENT; type = parser.next()) {
+ if (type != XmlPullParser.START_TAG) continue;
+ CameraPreference pref = newPreference(parser.getName(), args);
+
+ int depth = parser.getDepth();
+ if (depth > list.size()) {
+ list.add(pref);
+ } else {
+ list.set(depth - 1, pref);
+ }
+ if (depth > 1) {
+ ((PreferenceGroup) list.get(depth - 2)).addChild(pref);
+ }
+ }
+
+ if (list.size() == 0) {
+ throw new InflateException("No root element found");
+ }
+ return list.get(0);
+ } catch (XmlPullParserException e) {
+ throw new InflateException(e);
+ } catch (IOException e) {
+ throw new InflateException(parser.getPositionDescription(), e);
+ }
+ }
+}
diff --git a/src/com/android/camera/PreviewFrameLayout.java b/src/com/android/camera/PreviewFrameLayout.java
new file mode 100644
index 000000000..03ef91c60
--- /dev/null
+++ b/src/com/android/camera/PreviewFrameLayout.java
@@ -0,0 +1,136 @@
+/*
+ * 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.camera;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewStub;
+import android.widget.RelativeLayout;
+
+import com.android.camera.ui.LayoutChangeHelper;
+import com.android.camera.ui.LayoutChangeNotifier;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+/**
+ * A layout which handles the preview aspect ratio.
+ */
+public class PreviewFrameLayout extends RelativeLayout implements LayoutChangeNotifier {
+
+ private static final String TAG = "CAM_preview";
+
+ /** A callback to be invoked when the preview frame's size changes. */
+ public interface OnSizeChangedListener {
+ public void onSizeChanged(int width, int height);
+ }
+
+ private double mAspectRatio;
+ private View mBorder;
+ private OnSizeChangedListener mListener;
+ private LayoutChangeHelper mLayoutChangeHelper;
+
+ public PreviewFrameLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setAspectRatio(4.0 / 3.0);
+ mLayoutChangeHelper = new LayoutChangeHelper(this);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ mBorder = findViewById(R.id.preview_border);
+ }
+
+ public void setAspectRatio(double ratio) {
+ if (ratio <= 0.0) throw new IllegalArgumentException();
+
+ if (mAspectRatio != ratio) {
+ mAspectRatio = ratio;
+ requestLayout();
+ }
+ }
+
+ public void showBorder(boolean enabled) {
+ mBorder.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ public void fadeOutBorder() {
+ Util.fadeOut(mBorder);
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ int previewWidth = MeasureSpec.getSize(widthSpec);
+ int previewHeight = MeasureSpec.getSize(heightSpec);
+
+ if (!ApiHelper.HAS_SURFACE_TEXTURE) {
+ // Get the padding of the border background.
+ int hPadding = getPaddingLeft() + getPaddingRight();
+ int vPadding = getPaddingTop() + getPaddingBottom();
+
+ // Resize the preview frame with correct aspect ratio.
+ previewWidth -= hPadding;
+ previewHeight -= vPadding;
+
+ boolean widthLonger = previewWidth > previewHeight;
+ int longSide = (widthLonger ? previewWidth : previewHeight);
+ int shortSide = (widthLonger ? previewHeight : previewWidth);
+ if (longSide > shortSide * mAspectRatio) {
+ longSide = (int) ((double) shortSide * mAspectRatio);
+ } else {
+ shortSide = (int) ((double) longSide / mAspectRatio);
+ }
+ if (widthLonger) {
+ previewWidth = longSide;
+ previewHeight = shortSide;
+ } else {
+ previewWidth = shortSide;
+ previewHeight = longSide;
+ }
+
+ // Add the padding of the border.
+ previewWidth += hPadding;
+ previewHeight += vPadding;
+ }
+
+ // Ask children to follow the new preview dimension.
+ super.onMeasure(MeasureSpec.makeMeasureSpec(previewWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(previewHeight, MeasureSpec.EXACTLY));
+ }
+
+ public void setOnSizeChangedListener(OnSizeChangedListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ if (mListener != null) mListener.onSizeChanged(w, h);
+ }
+
+ @Override
+ public void setOnLayoutChangeListener(
+ LayoutChangeNotifier.Listener listener) {
+ mLayoutChangeHelper.setOnLayoutChangeListener(listener);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ mLayoutChangeHelper.onLayout(changed, l, t, r, b);
+ }
+}
diff --git a/src/com/android/camera/PreviewGestures.java b/src/com/android/camera/PreviewGestures.java
new file mode 100644
index 000000000..466172b7c
--- /dev/null
+++ b/src/com/android/camera/PreviewGestures.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2013 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.camera;
+
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.RenderOverlay;
+import com.android.camera.ui.ZoomRenderer;
+
+/* PreviewGestures disambiguates touch events received on RenderOverlay
+ * and dispatch them to the proper recipient (i.e. zoom renderer or pie renderer).
+ * Touch events on CameraControls will be handled by framework.
+ * */
+public class PreviewGestures
+ implements ScaleGestureDetector.OnScaleGestureListener {
+
+ private static final String TAG = "CAM_gestures";
+
+ private static final int MODE_NONE = 0;
+ private static final int MODE_ZOOM = 2;
+
+ public static final int DIR_UP = 0;
+ public static final int DIR_DOWN = 1;
+ public static final int DIR_LEFT = 2;
+ public static final int DIR_RIGHT = 3;
+
+ private SingleTapListener mTapListener;
+ private RenderOverlay mOverlay;
+ private PieRenderer mPie;
+ private ZoomRenderer mZoom;
+ private MotionEvent mDown;
+ private MotionEvent mCurrent;
+ private ScaleGestureDetector mScale;
+ private int mMode;
+ private boolean mZoomEnabled;
+ private boolean mEnabled;
+ private boolean mZoomOnly;
+ private GestureDetector mGestureDetector;
+
+ private GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public void onLongPress (MotionEvent e) {
+ // Open pie
+ if (!mZoomOnly && mPie != null && !mPie.showsItems()) {
+ openPie();
+ }
+ }
+
+ @Override
+ public boolean onSingleTapUp (MotionEvent e) {
+ // Tap to focus when pie is not open
+ if (mPie == null || !mPie.showsItems()) {
+ mTapListener.onSingleTapUp(null, (int) e.getX(), (int) e.getY());
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onScroll (MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ if (mZoomOnly || mMode == MODE_ZOOM) return false;
+ int deltaX = (int) (e1.getX() - e2.getX());
+ int deltaY = (int) (e1.getY() - e2.getY());
+ if (deltaY > 2 * deltaX && deltaY > -2 * deltaX) {
+ // Open pie on swipe up
+ if (mPie != null && !mPie.showsItems()) {
+ openPie();
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+
+ public interface SingleTapListener {
+ public void onSingleTapUp(View v, int x, int y);
+ }
+
+ public PreviewGestures(CameraActivity ctx, SingleTapListener tapListener,
+ ZoomRenderer zoom, PieRenderer pie) {
+ mTapListener = tapListener;
+ mPie = pie;
+ mZoom = zoom;
+ mMode = MODE_NONE;
+ mScale = new ScaleGestureDetector(ctx, this);
+ mEnabled = true;
+ mGestureDetector = new GestureDetector(mGestureListener);
+ }
+
+ public void setRenderOverlay(RenderOverlay overlay) {
+ mOverlay = overlay;
+ }
+
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ public void setZoomEnabled(boolean enable) {
+ mZoomEnabled = enable;
+ }
+
+ public void setZoomOnly(boolean zoom) {
+ mZoomOnly = zoom;
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public boolean dispatchTouch(MotionEvent m) {
+ if (!mEnabled) {
+ return false;
+ }
+ mCurrent = m;
+ if (MotionEvent.ACTION_DOWN == m.getActionMasked()) {
+ mMode = MODE_NONE;
+ mDown = MotionEvent.obtain(m);
+ }
+
+ // If pie is open, redirects all the touch events to pie.
+ if (mPie != null && mPie.isOpen()) {
+ return sendToPie(m);
+ }
+
+ // If pie is not open, send touch events to gesture detector and scale
+ // listener to recognize the gesture.
+ mGestureDetector.onTouchEvent(m);
+ if (mZoom != null) {
+ mScale.onTouchEvent(m);
+ if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) {
+ mMode = MODE_ZOOM;
+ if (mZoomEnabled) {
+ // Start showing zoom UI as soon as there is a second finger down
+ mZoom.onScaleBegin(mScale);
+ }
+ } else if (MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) {
+ mZoom.onScaleEnd(mScale);
+ }
+ }
+ return true;
+ }
+
+ private MotionEvent makeCancelEvent(MotionEvent m) {
+ MotionEvent c = MotionEvent.obtain(m);
+ c.setAction(MotionEvent.ACTION_CANCEL);
+ return c;
+ }
+
+ private void openPie() {
+ mGestureDetector.onTouchEvent(makeCancelEvent(mDown));
+ mScale.onTouchEvent(makeCancelEvent(mDown));
+ mOverlay.directDispatchTouch(mDown, mPie);
+ }
+
+ private boolean sendToPie(MotionEvent m) {
+ return mOverlay.directDispatchTouch(m, mPie);
+ }
+
+ // OnScaleGestureListener implementation
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ return mZoom.onScale(detector);
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ if (mPie == null || !mPie.isOpen()) {
+ mMode = MODE_ZOOM;
+ mGestureDetector.onTouchEvent(makeCancelEvent(mCurrent));
+ if (!mZoomEnabled) return false;
+ return mZoom.onScaleBegin(detector);
+ }
+ return false;
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mZoom.onScaleEnd(detector);
+ }
+}
+
diff --git a/src/com/android/camera/ProxyLauncher.java b/src/com/android/camera/ProxyLauncher.java
new file mode 100644
index 000000000..8c566214c
--- /dev/null
+++ b/src/com/android/camera/ProxyLauncher.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+public class ProxyLauncher extends Activity {
+
+ public static final int RESULT_USER_CANCELED = -2;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState == null) {
+ Intent intent = getIntent().getParcelableExtra(Intent.EXTRA_INTENT);
+ startActivityForResult(intent, 0);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_CANCELED) {
+ resultCode = RESULT_USER_CANCELED;
+ }
+ setResult(resultCode, data);
+ finish();
+ }
+
+}
diff --git a/src/com/android/camera/RecordLocationPreference.java b/src/com/android/camera/RecordLocationPreference.java
new file mode 100644
index 000000000..9992afabb
--- /dev/null
+++ b/src/com/android/camera/RecordLocationPreference.java
@@ -0,0 +1,58 @@
+/*
+ * 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.camera;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.AttributeSet;
+
+/**
+ * {@code RecordLocationPreference} is used to keep the "store locaiton"
+ * option in {@code SharedPreference}.
+ */
+public class RecordLocationPreference extends IconListPreference {
+
+ public static final String VALUE_NONE = "none";
+ public static final String VALUE_ON = "on";
+ public static final String VALUE_OFF = "off";
+
+ private final ContentResolver mResolver;
+
+ public RecordLocationPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mResolver = context.getContentResolver();
+ }
+
+ @Override
+ public String getValue() {
+ return get(getSharedPreferences(), mResolver) ? VALUE_ON : VALUE_OFF;
+ }
+
+ public static boolean get(
+ SharedPreferences pref, ContentResolver resolver) {
+ String value = pref.getString(
+ CameraSettings.KEY_RECORD_LOCATION, VALUE_NONE);
+ return VALUE_ON.equals(value);
+ }
+
+ public static boolean isSet(SharedPreferences pref) {
+ String value = pref.getString(
+ CameraSettings.KEY_RECORD_LOCATION, VALUE_NONE);
+ return !VALUE_NONE.equals(value);
+ }
+}
diff --git a/src/com/android/camera/RotateDialogController.java b/src/com/android/camera/RotateDialogController.java
new file mode 100644
index 000000000..5d5e5e70f
--- /dev/null
+++ b/src/com/android/camera/RotateDialogController.java
@@ -0,0 +1,169 @@
+/*
+ * 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.camera;
+
+import android.app.Activity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.camera.ui.Rotatable;
+import com.android.camera.ui.RotateLayout;
+import com.android.gallery3d.R;
+
+public class RotateDialogController implements Rotatable {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "RotateDialogController";
+ private static final long ANIM_DURATION = 150; // millis
+
+ private Activity mActivity;
+ private int mLayoutResourceID;
+ private View mDialogRootLayout;
+ private RotateLayout mRotateDialog;
+ private View mRotateDialogTitleLayout;
+ private View mRotateDialogButtonLayout;
+ private TextView mRotateDialogTitle;
+ private ProgressBar mRotateDialogSpinner;
+ private TextView mRotateDialogText;
+ private TextView mRotateDialogButton1;
+ private TextView mRotateDialogButton2;
+
+ private Animation mFadeInAnim, mFadeOutAnim;
+
+ public RotateDialogController(Activity a, int layoutResource) {
+ mActivity = a;
+ mLayoutResourceID = layoutResource;
+ }
+
+ private void inflateDialogLayout() {
+ if (mDialogRootLayout == null) {
+ ViewGroup layoutRoot = (ViewGroup) mActivity.getWindow().getDecorView();
+ LayoutInflater inflater = mActivity.getLayoutInflater();
+ View v = inflater.inflate(mLayoutResourceID, layoutRoot);
+ mDialogRootLayout = v.findViewById(R.id.rotate_dialog_root_layout);
+ mRotateDialog = (RotateLayout) v.findViewById(R.id.rotate_dialog_layout);
+ mRotateDialogTitleLayout = v.findViewById(R.id.rotate_dialog_title_layout);
+ mRotateDialogButtonLayout = v.findViewById(R.id.rotate_dialog_button_layout);
+ mRotateDialogTitle = (TextView) v.findViewById(R.id.rotate_dialog_title);
+ mRotateDialogSpinner = (ProgressBar) v.findViewById(R.id.rotate_dialog_spinner);
+ mRotateDialogText = (TextView) v.findViewById(R.id.rotate_dialog_text);
+ mRotateDialogButton1 = (Button) v.findViewById(R.id.rotate_dialog_button1);
+ mRotateDialogButton2 = (Button) v.findViewById(R.id.rotate_dialog_button2);
+
+ mFadeInAnim = AnimationUtils.loadAnimation(
+ mActivity, android.R.anim.fade_in);
+ mFadeOutAnim = AnimationUtils.loadAnimation(
+ mActivity, android.R.anim.fade_out);
+ mFadeInAnim.setDuration(ANIM_DURATION);
+ mFadeOutAnim.setDuration(ANIM_DURATION);
+ }
+ }
+
+ @Override
+ public void setOrientation(int orientation, boolean animation) {
+ inflateDialogLayout();
+ mRotateDialog.setOrientation(orientation, animation);
+ }
+
+ public void resetRotateDialog() {
+ inflateDialogLayout();
+ mRotateDialogTitleLayout.setVisibility(View.GONE);
+ mRotateDialogSpinner.setVisibility(View.GONE);
+ mRotateDialogButton1.setVisibility(View.GONE);
+ mRotateDialogButton2.setVisibility(View.GONE);
+ mRotateDialogButtonLayout.setVisibility(View.GONE);
+ }
+
+ private void fadeOutDialog() {
+ mDialogRootLayout.startAnimation(mFadeOutAnim);
+ mDialogRootLayout.setVisibility(View.GONE);
+ }
+
+ private void fadeInDialog() {
+ mDialogRootLayout.startAnimation(mFadeInAnim);
+ mDialogRootLayout.setVisibility(View.VISIBLE);
+ }
+
+ public void dismissDialog() {
+ if (mDialogRootLayout != null && mDialogRootLayout.getVisibility() != View.GONE) {
+ fadeOutDialog();
+ }
+ }
+
+ public void showAlertDialog(String title, String msg, String button1Text,
+ final Runnable r1, String button2Text, final Runnable r2) {
+ resetRotateDialog();
+
+ if (title != null) {
+ mRotateDialogTitle.setText(title);
+ mRotateDialogTitleLayout.setVisibility(View.VISIBLE);
+ }
+
+ mRotateDialogText.setText(msg);
+
+ if (button1Text != null) {
+ mRotateDialogButton1.setText(button1Text);
+ mRotateDialogButton1.setContentDescription(button1Text);
+ mRotateDialogButton1.setVisibility(View.VISIBLE);
+ mRotateDialogButton1.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (r1 != null) r1.run();
+ dismissDialog();
+ }
+ });
+ mRotateDialogButtonLayout.setVisibility(View.VISIBLE);
+ }
+ if (button2Text != null) {
+ mRotateDialogButton2.setText(button2Text);
+ mRotateDialogButton2.setContentDescription(button2Text);
+ mRotateDialogButton2.setVisibility(View.VISIBLE);
+ mRotateDialogButton2.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (r2 != null) r2.run();
+ dismissDialog();
+ }
+ });
+ mRotateDialogButtonLayout.setVisibility(View.VISIBLE);
+ }
+
+ fadeInDialog();
+ }
+
+ public void showWaitingDialog(String msg) {
+ resetRotateDialog();
+
+ mRotateDialogText.setText(msg);
+ mRotateDialogSpinner.setVisibility(View.VISIBLE);
+
+ fadeInDialog();
+ }
+
+ public int getVisibility() {
+ if (mDialogRootLayout != null) {
+ return mDialogRootLayout.getVisibility();
+ }
+ return View.INVISIBLE;
+ }
+}
diff --git a/src/com/android/camera/SecureCameraActivity.java b/src/com/android/camera/SecureCameraActivity.java
new file mode 100644
index 000000000..2fa68f8e6
--- /dev/null
+++ b/src/com/android/camera/SecureCameraActivity.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+// Use a different activity for secure camera only. So it can have a different
+// task affinity from others. This makes sure non-secure camera activity is not
+// started in secure lock screen.
+public class SecureCameraActivity extends CameraActivity {
+}
diff --git a/src/com/android/camera/ShutterButton.java b/src/com/android/camera/ShutterButton.java
new file mode 100755
index 000000000..a1bbb1a0d
--- /dev/null
+++ b/src/com/android/camera/ShutterButton.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2008 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.camera;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageView;
+
+/**
+ * A button designed to be used for the on-screen shutter button.
+ * It's currently an {@code ImageView} that can call a delegate when the
+ * pressed state changes.
+ */
+public class ShutterButton extends ImageView {
+
+ private boolean mTouchEnabled = true;
+
+ /**
+ * A callback to be invoked when a ShutterButton's pressed state changes.
+ */
+ public interface OnShutterButtonListener {
+ /**
+ * Called when a ShutterButton has been pressed.
+ *
+ * @param pressed The ShutterButton that was pressed.
+ */
+ void onShutterButtonFocus(boolean pressed);
+ void onShutterButtonClick();
+ }
+
+ private OnShutterButtonListener mListener;
+ private boolean mOldPressed;
+
+ public ShutterButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setOnShutterButtonListener(OnShutterButtonListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent m) {
+ if (mTouchEnabled) {
+ return super.dispatchTouchEvent(m);
+ } else {
+ return false;
+ }
+ }
+
+ public void enableTouch(boolean enable) {
+ mTouchEnabled = enable;
+ }
+
+ /**
+ * Hook into the drawable state changing to get changes to isPressed -- the
+ * onPressed listener doesn't always get called when the pressed state
+ * changes.
+ */
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ final boolean pressed = isPressed();
+ if (pressed != mOldPressed) {
+ if (!pressed) {
+ // When pressing the physical camera button the sequence of
+ // events is:
+ // focus pressed, optional camera pressed, focus released.
+ // We want to emulate this sequence of events with the shutter
+ // button. When clicking using a trackball button, the view
+ // system changes the drawable state before posting click
+ // notification, so the sequence of events is:
+ // pressed(true), optional click, pressed(false)
+ // When clicking using touch events, the view system changes the
+ // drawable state after posting click notification, so the
+ // sequence of events is:
+ // pressed(true), pressed(false), optional click
+ // Since we're emulating the physical camera button, we want to
+ // have the same order of events. So we want the optional click
+ // callback to be delivered before the pressed(false) callback.
+ //
+ // To do this, we delay the posting of the pressed(false) event
+ // slightly by pushing it on the event queue. This moves it
+ // after the optional click notification, so our client always
+ // sees events in this sequence:
+ // pressed(true), optional click, pressed(false)
+ post(new Runnable() {
+ @Override
+ public void run() {
+ callShutterButtonFocus(pressed);
+ }
+ });
+ } else {
+ callShutterButtonFocus(pressed);
+ }
+ mOldPressed = pressed;
+ }
+ }
+
+ private void callShutterButtonFocus(boolean pressed) {
+ if (mListener != null) {
+ mListener.onShutterButtonFocus(pressed);
+ }
+ }
+
+ @Override
+ public boolean performClick() {
+ boolean result = super.performClick();
+ if (mListener != null && getVisibility() == View.VISIBLE) {
+ mListener.onShutterButtonClick();
+ }
+ return result;
+ }
+}
diff --git a/src/com/android/camera/SoundClips.java b/src/com/android/camera/SoundClips.java
new file mode 100644
index 000000000..8155c03dc
--- /dev/null
+++ b/src/com/android/camera/SoundClips.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.MediaActionSound;
+import android.media.SoundPool;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+/*
+ * This class controls the sound playback according to the API level.
+ */
+public class SoundClips {
+ // Sound actions.
+ public static final int FOCUS_COMPLETE = 0;
+ public static final int START_VIDEO_RECORDING = 1;
+ public static final int STOP_VIDEO_RECORDING = 2;
+
+ public interface Player {
+ public void release();
+ public void play(int action);
+ }
+
+ public static Player getPlayer(Context context) {
+ if (ApiHelper.HAS_MEDIA_ACTION_SOUND) {
+ return new MediaActionSoundPlayer();
+ } else {
+ return new SoundPoolPlayer(context);
+ }
+ }
+
+ public static int getAudioTypeForSoundPool() {
+ return ApiHelper.getIntFieldIfExists(AudioManager.class,
+ "STREAM_SYSTEM_ENFORCED", null, AudioManager.STREAM_RING);
+ }
+
+ /**
+ * This class implements SoundClips.Player using MediaActionSound,
+ * which exists since API level 16.
+ */
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ private static class MediaActionSoundPlayer implements Player {
+ private static final String TAG = "MediaActionSoundPlayer";
+ private MediaActionSound mSound;
+
+ @Override
+ public void release() {
+ if (mSound != null) {
+ mSound.release();
+ mSound = null;
+ }
+ }
+
+ public MediaActionSoundPlayer() {
+ mSound = new MediaActionSound();
+ mSound.load(MediaActionSound.START_VIDEO_RECORDING);
+ mSound.load(MediaActionSound.STOP_VIDEO_RECORDING);
+ mSound.load(MediaActionSound.FOCUS_COMPLETE);
+ }
+
+ @Override
+ public synchronized void play(int action) {
+ switch(action) {
+ case FOCUS_COMPLETE:
+ mSound.play(MediaActionSound.FOCUS_COMPLETE);
+ break;
+ case START_VIDEO_RECORDING:
+ mSound.play(MediaActionSound.START_VIDEO_RECORDING);
+ break;
+ case STOP_VIDEO_RECORDING:
+ mSound.play(MediaActionSound.STOP_VIDEO_RECORDING);
+ break;
+ default:
+ Log.w(TAG, "Unrecognized action:" + action);
+ }
+ }
+ }
+
+ /**
+ * This class implements SoundClips.Player using SoundPool, which
+ * exists since API level 1.
+ */
+ private static class SoundPoolPlayer implements
+ Player, SoundPool.OnLoadCompleteListener {
+
+ private static final String TAG = "SoundPoolPlayer";
+ private static final int NUM_SOUND_STREAMS = 1;
+ private static final int[] SOUND_RES = { // Soundtrack res IDs.
+ R.raw.focus_complete,
+ R.raw.video_record
+ };
+
+ // ID returned by load() should be non-zero.
+ private static final int ID_NOT_LOADED = 0;
+
+ // Maps a sound action to the id;
+ private final int[] mSoundRes = {0, 1, 1};
+ // Store the context for lazy loading.
+ private Context mContext;
+ // mSoundPool is created every time load() is called and cleared every
+ // time release() is called.
+ private SoundPool mSoundPool;
+ // Sound ID of each sound resources. Given when the sound is loaded.
+ private final int[] mSoundIDs;
+ private final boolean[] mSoundIDReady;
+ private int mSoundIDToPlay;
+
+ public SoundPoolPlayer(Context context) {
+ mContext = context;
+
+ mSoundIDToPlay = ID_NOT_LOADED;
+
+ mSoundPool = new SoundPool(NUM_SOUND_STREAMS, getAudioTypeForSoundPool(), 0);
+ mSoundPool.setOnLoadCompleteListener(this);
+
+ mSoundIDs = new int[SOUND_RES.length];
+ mSoundIDReady = new boolean[SOUND_RES.length];
+ for (int i = 0; i < SOUND_RES.length; i++) {
+ mSoundIDs[i] = mSoundPool.load(mContext, SOUND_RES[i], 1);
+ mSoundIDReady[i] = false;
+ }
+ }
+
+ @Override
+ public synchronized void release() {
+ if (mSoundPool != null) {
+ mSoundPool.release();
+ mSoundPool = null;
+ }
+ }
+
+ @Override
+ public synchronized void play(int action) {
+ if (action < 0 || action >= mSoundRes.length) {
+ Log.e(TAG, "Resource ID not found for action:" + action + " in play().");
+ return;
+ }
+
+ int index = mSoundRes[action];
+ if (mSoundIDs[index] == ID_NOT_LOADED) {
+ // Not loaded yet, load first and then play when the loading is complete.
+ mSoundIDs[index] = mSoundPool.load(mContext, SOUND_RES[index], 1);
+ mSoundIDToPlay = mSoundIDs[index];
+ } else if (!mSoundIDReady[index]) {
+ // Loading and not ready yet.
+ mSoundIDToPlay = mSoundIDs[index];
+ } else {
+ mSoundPool.play(mSoundIDs[index], 1f, 1f, 0, 0, 1f);
+ }
+ }
+
+ @Override
+ public void onLoadComplete(SoundPool pool, int soundID, int status) {
+ if (status != 0) {
+ Log.e(TAG, "loading sound tracks failed (status=" + status + ")");
+ for (int i = 0; i < mSoundIDs.length; i++ ) {
+ if (mSoundIDs[i] == soundID) {
+ mSoundIDs[i] = ID_NOT_LOADED;
+ break;
+ }
+ }
+ return;
+ }
+
+ for (int i = 0; i < mSoundIDs.length; i++ ) {
+ if (mSoundIDs[i] == soundID) {
+ mSoundIDReady[i] = true;
+ break;
+ }
+ }
+
+ if (soundID == mSoundIDToPlay) {
+ mSoundIDToPlay = ID_NOT_LOADED;
+ mSoundPool.play(soundID, 1f, 1f, 0, 0, 1f);
+ }
+ }
+ }
+}
diff --git a/src/com/android/camera/StaticBitmapScreenNail.java b/src/com/android/camera/StaticBitmapScreenNail.java
new file mode 100644
index 000000000..10788c0fb
--- /dev/null
+++ b/src/com/android/camera/StaticBitmapScreenNail.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.graphics.Bitmap;
+
+import com.android.gallery3d.ui.BitmapScreenNail;
+
+public class StaticBitmapScreenNail extends BitmapScreenNail {
+ public StaticBitmapScreenNail(Bitmap bitmap) {
+ super(bitmap);
+ }
+
+ @Override
+ public void recycle() {
+ // Always keep the bitmap in memory.
+ }
+}
diff --git a/src/com/android/camera/Storage.java b/src/com/android/camera/Storage.java
new file mode 100644
index 000000000..ba995edef
--- /dev/null
+++ b/src/com/android/camera/Storage.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.camera;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.location.Location;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.StatFs;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.MediaColumns;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.exif.ExifInterface;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+public class Storage {
+ private static final String TAG = "CameraStorage";
+
+ public static final String DCIM =
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString();
+
+ public static final String DIRECTORY = DCIM + "/Camera";
+
+ // Match the code in MediaProvider.computeBucketValues().
+ public static final String BUCKET_ID =
+ String.valueOf(DIRECTORY.toLowerCase().hashCode());
+
+ public static final long UNAVAILABLE = -1L;
+ public static final long PREPARING = -2L;
+ public static final long UNKNOWN_SIZE = -3L;
+ public static final long LOW_STORAGE_THRESHOLD = 50000000;
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private static void setImageSize(ContentValues values, int width, int height) {
+ // The two fields are available since ICS but got published in JB
+ if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
+ values.put(MediaColumns.WIDTH, width);
+ values.put(MediaColumns.HEIGHT, height);
+ }
+ }
+
+ public static void writeFile(String path, byte[] data) {
+ FileOutputStream out = null;
+ try {
+ out = new FileOutputStream(path);
+ out.write(data);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to write data", e);
+ } finally {
+ try {
+ out.close();
+ } catch (Exception e) {
+ }
+ }
+ }
+
+ // Save the image and add it to media store.
+ public static Uri addImage(ContentResolver resolver, String title,
+ long date, Location location, int orientation, ExifInterface exif,
+ byte[] jpeg, int width, int height) {
+ // Save the image.
+ String path = generateFilepath(title);
+ if (exif != null) {
+ try {
+ exif.writeExif(jpeg, path);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to write data", e);
+ }
+ } else {
+ writeFile(path, jpeg);
+ }
+ return addImage(resolver, title, date, location, orientation,
+ jpeg.length, path, width, height);
+ }
+
+ // Add the image to media store.
+ public static Uri addImage(ContentResolver resolver, String title,
+ long date, Location location, int orientation, int jpegLength,
+ String path, int width, int height) {
+ // Insert into MediaStore.
+ ContentValues values = new ContentValues(9);
+ values.put(ImageColumns.TITLE, title);
+ values.put(ImageColumns.DISPLAY_NAME, title + ".jpg");
+ values.put(ImageColumns.DATE_TAKEN, date);
+ values.put(ImageColumns.MIME_TYPE, "image/jpeg");
+ // Clockwise rotation in degrees. 0, 90, 180, or 270.
+ values.put(ImageColumns.ORIENTATION, orientation);
+ values.put(ImageColumns.DATA, path);
+ values.put(ImageColumns.SIZE, jpegLength);
+
+ setImageSize(values, width, height);
+
+ if (location != null) {
+ values.put(ImageColumns.LATITUDE, location.getLatitude());
+ values.put(ImageColumns.LONGITUDE, location.getLongitude());
+ }
+
+ Uri uri = null;
+ try {
+ uri = resolver.insert(Images.Media.EXTERNAL_CONTENT_URI, values);
+ } catch (Throwable th) {
+ // This can happen when the external volume is already mounted, but
+ // MediaScanner has not notify MediaProvider to add that volume.
+ // The picture is still safe and MediaScanner will find it and
+ // insert it into MediaProvider. The only problem is that the user
+ // cannot click the thumbnail to review the picture.
+ Log.e(TAG, "Failed to write MediaStore" + th);
+ }
+ return uri;
+ }
+
+ public static void deleteImage(ContentResolver resolver, Uri uri) {
+ try {
+ resolver.delete(uri, null, null);
+ } catch (Throwable th) {
+ Log.e(TAG, "Failed to delete image: " + uri);
+ }
+ }
+
+ public static String generateFilepath(String title) {
+ return DIRECTORY + '/' + title + ".jpg";
+ }
+
+ public static long getAvailableSpace() {
+ String state = Environment.getExternalStorageState();
+ Log.d(TAG, "External storage state=" + state);
+ if (Environment.MEDIA_CHECKING.equals(state)) {
+ return PREPARING;
+ }
+ if (!Environment.MEDIA_MOUNTED.equals(state)) {
+ return UNAVAILABLE;
+ }
+
+ File dir = new File(DIRECTORY);
+ dir.mkdirs();
+ if (!dir.isDirectory() || !dir.canWrite()) {
+ return UNAVAILABLE;
+ }
+
+ try {
+ StatFs stat = new StatFs(DIRECTORY);
+ return stat.getAvailableBlocks() * (long) stat.getBlockSize();
+ } catch (Exception e) {
+ Log.i(TAG, "Fail to access external storage", e);
+ }
+ return UNKNOWN_SIZE;
+ }
+
+ /**
+ * OSX requires plugged-in USB storage to have path /DCIM/NNNAAAAA to be
+ * imported. This is a temporary fix for bug#1655552.
+ */
+ public static void ensureOSXCompatible() {
+ File nnnAAAAA = new File(DCIM, "100ANDRO");
+ if (!(nnnAAAAA.exists() || nnnAAAAA.mkdirs())) {
+ Log.e(TAG, "Failed to create " + nnnAAAAA.getPath());
+ }
+ }
+}
diff --git a/src/com/android/camera/SurfaceTextureRenderer.java b/src/com/android/camera/SurfaceTextureRenderer.java
new file mode 100644
index 000000000..66f7aa219
--- /dev/null
+++ b/src/com/android/camera/SurfaceTextureRenderer.java
@@ -0,0 +1,224 @@
+/*
+ * 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.camera;
+
+import android.graphics.SurfaceTexture;
+import android.os.Handler;
+import android.util.Log;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+import javax.microedition.khronos.opengles.GL10;
+
+public class SurfaceTextureRenderer {
+
+ public interface FrameDrawer {
+ public void onDrawFrame(GL10 gl);
+ }
+
+ private static final String TAG = "CAM_" + SurfaceTextureRenderer.class.getSimpleName();
+ private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+
+ private EGLConfig mEglConfig;
+ private EGLDisplay mEglDisplay;
+ private EGLContext mEglContext;
+ private EGLSurface mEglSurface;
+ private EGL10 mEgl;
+ private GL10 mGl;
+
+ private Handler mEglHandler;
+ private FrameDrawer mFrameDrawer;
+
+ private Object mRenderLock = new Object();
+ private Runnable mRenderTask = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mRenderLock) {
+ mFrameDrawer.onDrawFrame(mGl);
+ mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
+ mRenderLock.notifyAll();
+ }
+ }
+ };
+
+ public class RenderThread extends Thread {
+ private Boolean mRenderStopped = false;
+
+ @Override
+ public void run() {
+ while (true) {
+ synchronized (mRenderStopped) {
+ if (mRenderStopped) return;
+ }
+ draw(true);
+ }
+ }
+
+ public void stopRender() {
+ synchronized (mRenderStopped) {
+ mRenderStopped = true;
+ }
+ }
+ }
+
+ public SurfaceTextureRenderer(SurfaceTexture tex,
+ Handler handler, FrameDrawer renderer) {
+ mEglHandler = handler;
+ mFrameDrawer = renderer;
+
+ initialize(tex);
+ }
+
+ public RenderThread createRenderThread() {
+ return new RenderThread();
+ }
+
+ public void release() {
+ mEglHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
+ mEgl.eglDestroyContext(mEglDisplay, mEglContext);
+ mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE,
+ EGL10.EGL_NO_CONTEXT);
+ mEgl.eglTerminate(mEglDisplay);
+ mEglSurface = null;
+ mEglContext = null;
+ mEglDisplay = null;
+ }
+ });
+ }
+
+ /**
+ * Posts a render request to the GL thread.
+ * @param sync set <code>true</code> if the caller needs it to be
+ * a synchronous call.
+ */
+ public void draw(boolean sync) {
+ synchronized (mRenderLock) {
+ mEglHandler.post(mRenderTask);
+ if (sync) {
+ try {
+ mRenderLock.wait();
+ } catch (InterruptedException ex) {
+ Log.v(TAG, "RenderLock.wait() interrupted");
+ }
+ }
+ }
+ }
+
+ private void initialize(final SurfaceTexture target) {
+ mEglHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mEgl = (EGL10) EGLContext.getEGL();
+ mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+ if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
+ throw new RuntimeException("eglGetDisplay failed");
+ }
+ int[] version = new int[2];
+ if (!mEgl.eglInitialize(mEglDisplay, version)) {
+ throw new RuntimeException("eglInitialize failed");
+ } else {
+ Log.v(TAG, "EGL version: " + version[0] + '.' + version[1]);
+ }
+ int[] attribList = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
+ mEglConfig = chooseConfig(mEgl, mEglDisplay);
+ mEglContext = mEgl.eglCreateContext(
+ mEglDisplay, mEglConfig, EGL10.EGL_NO_CONTEXT, attribList);
+
+ if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
+ throw new RuntimeException("failed to createContext");
+ }
+ mEglSurface = mEgl.eglCreateWindowSurface(
+ mEglDisplay, mEglConfig, target, null);
+ if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
+ throw new RuntimeException("failed to createWindowSurface");
+ }
+
+ if (!mEgl.eglMakeCurrent(
+ mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
+ throw new RuntimeException("failed to eglMakeCurrent");
+ }
+
+ mGl = (GL10) mEglContext.getGL();
+ }
+ });
+ waitDone();
+ }
+
+ private void waitDone() {
+ final Object lock = new Object();
+ synchronized (lock) {
+ mEglHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (lock) {
+ lock.notifyAll();
+ }
+ }
+ });
+ try {
+ lock.wait();
+ } catch (InterruptedException ex) {
+ Log.v(TAG, "waitDone() interrupted");
+ }
+ }
+ }
+
+ private static void checkEglError(String prompt, EGL10 egl) {
+ int error;
+ while ((error = egl.eglGetError()) != EGL10.EGL_SUCCESS) {
+ Log.e(TAG, String.format("%s: EGL error: 0x%x", prompt, error));
+ }
+ }
+
+ private static final int EGL_OPENGL_ES2_BIT = 4;
+ private static final int[] CONFIG_SPEC = new int[] {
+ EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+ EGL10.EGL_RED_SIZE, 8,
+ EGL10.EGL_GREEN_SIZE, 8,
+ EGL10.EGL_BLUE_SIZE, 8,
+ EGL10.EGL_ALPHA_SIZE, 0,
+ EGL10.EGL_DEPTH_SIZE, 0,
+ EGL10.EGL_STENCIL_SIZE, 0,
+ EGL10.EGL_NONE
+ };
+
+ private static EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
+ int[] numConfig = new int[1];
+ if (!egl.eglChooseConfig(display, CONFIG_SPEC, null, 0, numConfig)) {
+ throw new IllegalArgumentException("eglChooseConfig failed");
+ }
+
+ int numConfigs = numConfig[0];
+ if (numConfigs <= 0) {
+ throw new IllegalArgumentException("No configs match configSpec");
+ }
+
+ EGLConfig[] configs = new EGLConfig[numConfigs];
+ if (!egl.eglChooseConfig(
+ display, CONFIG_SPEC, configs, numConfigs, numConfig)) {
+ throw new IllegalArgumentException("eglChooseConfig#2 failed");
+ }
+
+ return configs[0];
+ }
+}
diff --git a/src/com/android/camera/SwitchAnimManager.java b/src/com/android/camera/SwitchAnimManager.java
new file mode 100644
index 000000000..6ec88223e
--- /dev/null
+++ b/src/com/android/camera/SwitchAnimManager.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+
+/**
+ * Class to handle the animation when switching between back and front cameras.
+ * An image of the previous camera zooms in and fades out. The preview of the
+ * new camera zooms in and fades in. The image of the previous camera is called
+ * review in this class.
+ */
+public class SwitchAnimManager {
+ private static final String TAG = "SwitchAnimManager";
+ // The amount of change for zooming in and out.
+ private static final float ZOOM_DELTA_PREVIEW = 0.2f;
+ private static final float ZOOM_DELTA_REVIEW = 0.5f;
+ private static final float ANIMATION_DURATION = 400; // ms
+ public static final float INITIAL_DARKEN_ALPHA = 0.8f;
+
+ private long mAnimStartTime; // milliseconds.
+ // The drawing width and height of the review image. This is saved when the
+ // texture is copied.
+ private int mReviewDrawingWidth;
+ private int mReviewDrawingHeight;
+ // The maximum width of the camera screen nail width from onDraw. We need to
+ // know how much the preview is scaled and scale the review the same amount.
+ // For example, the preview is not full screen in film strip mode.
+ private int mPreviewFrameLayoutWidth;
+
+ public SwitchAnimManager() {
+ }
+
+ public void setReviewDrawingSize(int width, int height) {
+ mReviewDrawingWidth = width;
+ mReviewDrawingHeight = height;
+ }
+
+ // width: the width of PreviewFrameLayout view.
+ // height: the height of PreviewFrameLayout view. Not used. Kept for
+ // consistency.
+ public void setPreviewFrameLayoutSize(int width, int height) {
+ mPreviewFrameLayoutWidth = width;
+ }
+
+ // w and h: the rectangle area where the animation takes place.
+ public void startAnimation() {
+ mAnimStartTime = SystemClock.uptimeMillis();
+ }
+
+ // Returns true if the animation has been drawn.
+ // preview: camera preview view.
+ // review: snapshot of the preview before switching the camera.
+ public boolean drawAnimation(GLCanvas canvas, int x, int y, int width,
+ int height, CameraScreenNail preview, RawTexture review) {
+ long timeDiff = SystemClock.uptimeMillis() - mAnimStartTime;
+ if (timeDiff > ANIMATION_DURATION) return false;
+ float fraction = timeDiff / ANIMATION_DURATION;
+
+ // Calculate the position and the size of the preview.
+ float centerX = x + width / 2f;
+ float centerY = y + height / 2f;
+ float previewAnimScale = 1 - ZOOM_DELTA_PREVIEW * (1 - fraction);
+ float previewWidth = width * previewAnimScale;
+ float previewHeight = height * previewAnimScale;
+ int previewX = Math.round(centerX - previewWidth / 2);
+ int previewY = Math.round(centerY - previewHeight / 2);
+
+ // Calculate the position and the size of the review.
+ float reviewAnimScale = 1 + ZOOM_DELTA_REVIEW * fraction;
+
+ // Calculate how much preview is scaled.
+ // The scaling is done by PhotoView in Gallery so we don't have the
+ // scaling information but only the width and the height passed to this
+ // method. The inference of the scale ratio is done by matching the
+ // current width and the original width we have at first when the camera
+ // layout is inflated.
+ float scaleRatio = 1;
+ if (mPreviewFrameLayoutWidth != 0) {
+ scaleRatio = (float) width / mPreviewFrameLayoutWidth;
+ } else {
+ Log.e(TAG, "mPreviewFrameLayoutWidth is 0.");
+ }
+ float reviewWidth = mReviewDrawingWidth * reviewAnimScale * scaleRatio;
+ float reviewHeight = mReviewDrawingHeight * reviewAnimScale * scaleRatio;
+ int reviewX = Math.round(centerX - reviewWidth / 2);
+ int reviewY = Math.round(centerY - reviewHeight / 2);
+
+ // Draw the preview.
+ float alpha = canvas.getAlpha();
+ canvas.setAlpha(fraction); // fade in
+ preview.directDraw(canvas, previewX, previewY, Math.round(previewWidth),
+ Math.round(previewHeight));
+
+ // Draw the review.
+ canvas.setAlpha((1f - fraction) * INITIAL_DARKEN_ALPHA); // fade out
+ review.draw(canvas, reviewX, reviewY, Math.round(reviewWidth),
+ Math.round(reviewHeight));
+ canvas.setAlpha(alpha);
+ return true;
+ }
+
+ public boolean drawDarkPreview(GLCanvas canvas, int x, int y, int width,
+ int height, RawTexture review) {
+ // Calculate the position and the size.
+ float centerX = x + width / 2f;
+ float centerY = y + height / 2f;
+ float scaleRatio = 1;
+ if (mPreviewFrameLayoutWidth != 0) {
+ scaleRatio = (float) width / mPreviewFrameLayoutWidth;
+ } else {
+ Log.e(TAG, "mPreviewFrameLayoutWidth is 0.");
+ }
+ float reviewWidth = mReviewDrawingWidth * scaleRatio;
+ float reviewHeight = mReviewDrawingHeight * scaleRatio;
+ int reviewX = Math.round(centerX - reviewWidth / 2);
+ int reviewY = Math.round(centerY - reviewHeight / 2);
+
+ // Draw the review.
+ float alpha = canvas.getAlpha();
+ canvas.setAlpha(INITIAL_DARKEN_ALPHA);
+ review.draw(canvas, reviewX, reviewY, Math.round(reviewWidth),
+ Math.round(reviewHeight));
+ canvas.setAlpha(alpha);
+ return true;
+ }
+
+}
diff --git a/src/com/android/camera/Thumbnail.java b/src/com/android/camera/Thumbnail.java
new file mode 100644
index 000000000..5f8483d6c
--- /dev/null
+++ b/src/com/android/camera/Thumbnail.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.camera;
+
+import android.graphics.Bitmap;
+import android.media.MediaMetadataRetriever;
+
+import java.io.FileDescriptor;
+
+public class Thumbnail {
+ public static Bitmap createVideoThumbnailBitmap(FileDescriptor fd, int targetWidth) {
+ return createVideoThumbnailBitmap(null, fd, targetWidth);
+ }
+
+ public static Bitmap createVideoThumbnailBitmap(String filePath, int targetWidth) {
+ return createVideoThumbnailBitmap(filePath, null, targetWidth);
+ }
+
+ private static Bitmap createVideoThumbnailBitmap(String filePath, FileDescriptor fd,
+ int targetWidth) {
+ Bitmap bitmap = null;
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+ try {
+ if (filePath != null) {
+ retriever.setDataSource(filePath);
+ } else {
+ retriever.setDataSource(fd);
+ }
+ bitmap = retriever.getFrameAtTime(-1);
+ } catch (IllegalArgumentException ex) {
+ // Assume this is a corrupt video file
+ } catch (RuntimeException ex) {
+ // Assume this is a corrupt video file.
+ } finally {
+ try {
+ retriever.release();
+ } catch (RuntimeException ex) {
+ // Ignore failures while cleaning up.
+ }
+ }
+ if (bitmap == null) return null;
+
+ // Scale down the bitmap if it is bigger than we need.
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ if (width > targetWidth) {
+ float scale = (float) targetWidth / width;
+ int w = Math.round(scale * width);
+ int h = Math.round(scale * height);
+ bitmap = Bitmap.createScaledBitmap(bitmap, w, h, true);
+ }
+ return bitmap;
+ }
+}
diff --git a/src/com/android/camera/Util.java b/src/com/android/camera/Util.java
new file mode 100644
index 000000000..ccc2d9079
--- /dev/null
+++ b/src/com/android/camera/Util.java
@@ -0,0 +1,804 @@
+/*
+ * 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.camera;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.admin.DevicePolicyManager;
+import android.content.ActivityNotFoundException;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.hardware.Camera;
+import android.hardware.Camera.CameraInfo;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.location.Location;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.telephony.TelephonyManager;
+import android.util.DisplayMetrics;
+import android.util.FloatMath;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.MovieActivity;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * Collection of utility functions used in this package.
+ */
+public class Util {
+ private static final String TAG = "Util";
+
+ // Orientation hysteresis amount used in rounding, in degrees
+ public static final int ORIENTATION_HYSTERESIS = 5;
+
+ public static final String REVIEW_ACTION = "com.android.camera.action.REVIEW";
+ // See android.hardware.Camera.ACTION_NEW_PICTURE.
+ public static final String ACTION_NEW_PICTURE = "android.hardware.action.NEW_PICTURE";
+ // See android.hardware.Camera.ACTION_NEW_VIDEO.
+ public static final String ACTION_NEW_VIDEO = "android.hardware.action.NEW_VIDEO";
+
+ // Fields from android.hardware.Camera.Parameters
+ public static final String FOCUS_MODE_CONTINUOUS_PICTURE = "continuous-picture";
+ public static final String RECORDING_HINT = "recording-hint";
+ private static final String AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported";
+ private static final String AUTO_WHITE_BALANCE_LOCK_SUPPORTED = "auto-whitebalance-lock-supported";
+ private static final String VIDEO_SNAPSHOT_SUPPORTED = "video-snapshot-supported";
+ public static final String SCENE_MODE_HDR = "hdr";
+ public static final String TRUE = "true";
+ public static final String FALSE = "false";
+
+ public static boolean isSupported(String value, List<String> supported) {
+ return supported == null ? false : supported.indexOf(value) >= 0;
+ }
+
+ public static boolean isAutoExposureLockSupported(Parameters params) {
+ return TRUE.equals(params.get(AUTO_EXPOSURE_LOCK_SUPPORTED));
+ }
+
+ public static boolean isAutoWhiteBalanceLockSupported(Parameters params) {
+ return TRUE.equals(params.get(AUTO_WHITE_BALANCE_LOCK_SUPPORTED));
+ }
+
+ public static boolean isVideoSnapshotSupported(Parameters params) {
+ return TRUE.equals(params.get(VIDEO_SNAPSHOT_SUPPORTED));
+ }
+
+ public static boolean isCameraHdrSupported(Parameters params) {
+ List<String> supported = params.getSupportedSceneModes();
+ return (supported != null) && supported.contains(SCENE_MODE_HDR);
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ public static boolean isMeteringAreaSupported(Parameters params) {
+ if (ApiHelper.HAS_CAMERA_METERING_AREA) {
+ return params.getMaxNumMeteringAreas() > 0;
+ }
+ return false;
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ public static boolean isFocusAreaSupported(Parameters params) {
+ if (ApiHelper.HAS_CAMERA_FOCUS_AREA) {
+ return (params.getMaxNumFocusAreas() > 0
+ && isSupported(Parameters.FOCUS_MODE_AUTO,
+ params.getSupportedFocusModes()));
+ }
+ return false;
+ }
+
+ // Private intent extras. Test only.
+ private static final String EXTRAS_CAMERA_FACING =
+ "android.intent.extras.CAMERA_FACING";
+
+ private static float sPixelDensity = 1;
+ private static ImageFileNamer sImageFileNamer;
+
+ private Util() {
+ }
+
+ public static void initialize(Context context) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager)
+ context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metrics);
+ sPixelDensity = metrics.density;
+ sImageFileNamer = new ImageFileNamer(
+ context.getString(R.string.image_file_name_format));
+ }
+
+ public static int dpToPixel(int dp) {
+ return Math.round(sPixelDensity * dp);
+ }
+
+ // Rotates the bitmap by the specified degree.
+ // If a new bitmap is created, the original bitmap is recycled.
+ public static Bitmap rotate(Bitmap b, int degrees) {
+ return rotateAndMirror(b, degrees, false);
+ }
+
+ // Rotates and/or mirrors the bitmap. If a new bitmap is created, the
+ // original bitmap is recycled.
+ public static Bitmap rotateAndMirror(Bitmap b, int degrees, boolean mirror) {
+ if ((degrees != 0 || mirror) && b != null) {
+ Matrix m = new Matrix();
+ // Mirror first.
+ // horizontal flip + rotation = -rotation + horizontal flip
+ if (mirror) {
+ m.postScale(-1, 1);
+ degrees = (degrees + 360) % 360;
+ if (degrees == 0 || degrees == 180) {
+ m.postTranslate(b.getWidth(), 0);
+ } else if (degrees == 90 || degrees == 270) {
+ m.postTranslate(b.getHeight(), 0);
+ } else {
+ throw new IllegalArgumentException("Invalid degrees=" + degrees);
+ }
+ }
+ if (degrees != 0) {
+ // clockwise
+ m.postRotate(degrees,
+ (float) b.getWidth() / 2, (float) b.getHeight() / 2);
+ }
+
+ try {
+ Bitmap b2 = Bitmap.createBitmap(
+ b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+ if (b != b2) {
+ b.recycle();
+ b = b2;
+ }
+ } catch (OutOfMemoryError ex) {
+ // We have no memory to rotate. Return the original bitmap.
+ }
+ }
+ return b;
+ }
+
+ /*
+ * Compute the sample size as a function of minSideLength
+ * and maxNumOfPixels.
+ * minSideLength is used to specify that minimal width or height of a
+ * bitmap.
+ * maxNumOfPixels is used to specify the maximal size in pixels that is
+ * tolerable in terms of memory usage.
+ *
+ * The function returns a sample size based on the constraints.
+ * Both size and minSideLength can be passed in as -1
+ * which indicates no care of the corresponding constraint.
+ * The functions prefers returning a sample size that
+ * generates a smaller bitmap, unless minSideLength = -1.
+ *
+ * Also, the function rounds up the sample size to a power of 2 or multiple
+ * of 8 because BitmapFactory only honors sample size this way.
+ * For example, BitmapFactory downsamples an image by 2 even though the
+ * request is 3. So we round up the sample size to avoid OOM.
+ */
+ public static int computeSampleSize(BitmapFactory.Options options,
+ int minSideLength, int maxNumOfPixels) {
+ int initialSize = computeInitialSampleSize(options, minSideLength,
+ maxNumOfPixels);
+
+ int roundedSize;
+ if (initialSize <= 8) {
+ roundedSize = 1;
+ while (roundedSize < initialSize) {
+ roundedSize <<= 1;
+ }
+ } else {
+ roundedSize = (initialSize + 7) / 8 * 8;
+ }
+
+ return roundedSize;
+ }
+
+ private static int computeInitialSampleSize(BitmapFactory.Options options,
+ int minSideLength, int maxNumOfPixels) {
+ double w = options.outWidth;
+ double h = options.outHeight;
+
+ int lowerBound = (maxNumOfPixels < 0) ? 1 :
+ (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
+ int upperBound = (minSideLength < 0) ? 128 :
+ (int) Math.min(Math.floor(w / minSideLength),
+ Math.floor(h / minSideLength));
+
+ if (upperBound < lowerBound) {
+ // return the larger one when there is no overlapping zone.
+ return lowerBound;
+ }
+
+ if (maxNumOfPixels < 0 && minSideLength < 0) {
+ return 1;
+ } else if (minSideLength < 0) {
+ return lowerBound;
+ } else {
+ return upperBound;
+ }
+ }
+
+ public static Bitmap makeBitmap(byte[] jpegData, int maxNumOfPixels) {
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length,
+ options);
+ if (options.mCancel || options.outWidth == -1
+ || options.outHeight == -1) {
+ return null;
+ }
+ options.inSampleSize = computeSampleSize(
+ options, -1, maxNumOfPixels);
+ options.inJustDecodeBounds = false;
+
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ return BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length,
+ options);
+ } catch (OutOfMemoryError ex) {
+ Log.e(TAG, "Got oom exception ", ex);
+ return null;
+ }
+ }
+
+ public static void closeSilently(Closeable c) {
+ if (c == null) return;
+ try {
+ c.close();
+ } catch (Throwable t) {
+ // do nothing
+ }
+ }
+
+ public static void Assert(boolean cond) {
+ if (!cond) {
+ throw new AssertionError();
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private static void throwIfCameraDisabled(Activity activity) throws CameraDisabledException {
+ // Check if device policy has disabled the camera.
+ if (ApiHelper.HAS_GET_CAMERA_DISABLED) {
+ DevicePolicyManager dpm = (DevicePolicyManager) activity.getSystemService(
+ Context.DEVICE_POLICY_SERVICE);
+ if (dpm.getCameraDisabled(null)) {
+ throw new CameraDisabledException();
+ }
+ }
+ }
+
+ public static CameraManager.CameraProxy openCamera(
+ Activity activity, int cameraId)
+ throws CameraHardwareException, CameraDisabledException {
+ throwIfCameraDisabled(activity);
+
+ try {
+ return CameraHolder.instance().open(cameraId);
+ } catch (CameraHardwareException e) {
+ // In eng build, we throw the exception so that test tool
+ // can detect it and report it
+ if ("eng".equals(Build.TYPE)) {
+ throw new RuntimeException("openCamera failed", e);
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ public static void showErrorAndFinish(final Activity activity, int msgId) {
+ DialogInterface.OnClickListener buttonListener =
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ activity.finish();
+ }
+ };
+ TypedValue out = new TypedValue();
+ activity.getTheme().resolveAttribute(android.R.attr.alertDialogIcon, out, true);
+ new AlertDialog.Builder(activity)
+ .setCancelable(false)
+ .setTitle(R.string.camera_error_title)
+ .setMessage(msgId)
+ .setNeutralButton(R.string.dialog_ok, buttonListener)
+ .setIcon(out.resourceId)
+ .show();
+ }
+
+ public static <T> T checkNotNull(T object) {
+ if (object == null) throw new NullPointerException();
+ return object;
+ }
+
+ public static boolean equals(Object a, Object b) {
+ return (a == b) || (a == null ? false : a.equals(b));
+ }
+
+ public static int nextPowerOf2(int n) {
+ n -= 1;
+ n |= n >>> 16;
+ n |= n >>> 8;
+ n |= n >>> 4;
+ n |= n >>> 2;
+ n |= n >>> 1;
+ return n + 1;
+ }
+
+ public static float distance(float x, float y, float sx, float sy) {
+ float dx = x - sx;
+ float dy = y - sy;
+ return FloatMath.sqrt(dx * dx + dy * dy);
+ }
+
+ public static int clamp(int x, int min, int max) {
+ if (x > max) return max;
+ if (x < min) return min;
+ return x;
+ }
+
+ public static int getDisplayRotation(Activity activity) {
+ int rotation = activity.getWindowManager().getDefaultDisplay()
+ .getRotation();
+ switch (rotation) {
+ case Surface.ROTATION_0: return 0;
+ case Surface.ROTATION_90: return 90;
+ case Surface.ROTATION_180: return 180;
+ case Surface.ROTATION_270: return 270;
+ }
+ return 0;
+ }
+
+ public static int getDisplayOrientation(int degrees, int cameraId) {
+ // See android.hardware.Camera.setDisplayOrientation for
+ // documentation.
+ Camera.CameraInfo info = new Camera.CameraInfo();
+ Camera.getCameraInfo(cameraId, info);
+ int result;
+ if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ result = (info.orientation + degrees) % 360;
+ result = (360 - result) % 360; // compensate the mirror
+ } else { // back-facing
+ result = (info.orientation - degrees + 360) % 360;
+ }
+ return result;
+ }
+
+ public static int getCameraOrientation(int cameraId) {
+ Camera.CameraInfo info = new Camera.CameraInfo();
+ Camera.getCameraInfo(cameraId, info);
+ return info.orientation;
+ }
+
+ public static int roundOrientation(int orientation, int orientationHistory) {
+ boolean changeOrientation = false;
+ if (orientationHistory == OrientationEventListener.ORIENTATION_UNKNOWN) {
+ changeOrientation = true;
+ } else {
+ int dist = Math.abs(orientation - orientationHistory);
+ dist = Math.min( dist, 360 - dist );
+ changeOrientation = ( dist >= 45 + ORIENTATION_HYSTERESIS );
+ }
+ if (changeOrientation) {
+ return ((orientation + 45) / 90 * 90) % 360;
+ }
+ return orientationHistory;
+ }
+
+ @SuppressWarnings("deprecation")
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
+ private static Point getDefaultDisplaySize(Activity activity, Point size) {
+ Display d = activity.getWindowManager().getDefaultDisplay();
+ if (Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.HONEYCOMB_MR2) {
+ d.getSize(size);
+ } else {
+ size.set(d.getWidth(), d.getHeight());
+ }
+ return size;
+ }
+
+ public static Size getOptimalPreviewSize(Activity currentActivity,
+ List<Size> sizes, double targetRatio) {
+ // Use a very small tolerance because we want an exact match.
+ final double ASPECT_TOLERANCE = 0.001;
+ if (sizes == null) return null;
+
+ Size optimalSize = null;
+ double minDiff = Double.MAX_VALUE;
+
+ // Because of bugs of overlay and layout, we sometimes will try to
+ // layout the viewfinder in the portrait orientation and thus get the
+ // wrong size of preview surface. When we change the preview size, the
+ // new overlay will be created before the old one closed, which causes
+ // an exception. For now, just get the screen size.
+ Point point = getDefaultDisplaySize(currentActivity, new Point());
+ int targetHeight = Math.min(point.x, point.y);
+ // Try to find an size match aspect ratio and size
+ for (Size size : sizes) {
+ double ratio = (double) size.width / size.height;
+ if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
+ if (Math.abs(size.height - targetHeight) < minDiff) {
+ optimalSize = size;
+ minDiff = Math.abs(size.height - targetHeight);
+ }
+ }
+ // Cannot find the one match the aspect ratio. This should not happen.
+ // Ignore the requirement.
+ if (optimalSize == null) {
+ Log.w(TAG, "No preview size match the aspect ratio");
+ minDiff = Double.MAX_VALUE;
+ for (Size size : sizes) {
+ if (Math.abs(size.height - targetHeight) < minDiff) {
+ optimalSize = size;
+ minDiff = Math.abs(size.height - targetHeight);
+ }
+ }
+ }
+ return optimalSize;
+ }
+
+ // Returns the largest picture size which matches the given aspect ratio.
+ public static Size getOptimalVideoSnapshotPictureSize(
+ List<Size> sizes, double targetRatio) {
+ // Use a very small tolerance because we want an exact match.
+ final double ASPECT_TOLERANCE = 0.001;
+ if (sizes == null) return null;
+
+ Size optimalSize = null;
+
+ // Try to find a size matches aspect ratio and has the largest width
+ for (Size size : sizes) {
+ double ratio = (double) size.width / size.height;
+ if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
+ if (optimalSize == null || size.width > optimalSize.width) {
+ optimalSize = size;
+ }
+ }
+
+ // Cannot find one that matches the aspect ratio. This should not happen.
+ // Ignore the requirement.
+ if (optimalSize == null) {
+ Log.w(TAG, "No picture size match the aspect ratio");
+ for (Size size : sizes) {
+ if (optimalSize == null || size.width > optimalSize.width) {
+ optimalSize = size;
+ }
+ }
+ }
+ return optimalSize;
+ }
+
+ public static void dumpParameters(Parameters parameters) {
+ String flattened = parameters.flatten();
+ StringTokenizer tokenizer = new StringTokenizer(flattened, ";");
+ Log.d(TAG, "Dump all camera parameters:");
+ while (tokenizer.hasMoreElements()) {
+ Log.d(TAG, tokenizer.nextToken());
+ }
+ }
+
+ /**
+ * Returns whether the device is voice-capable (meaning, it can do MMS).
+ */
+ public static boolean isMmsCapable(Context context) {
+ TelephonyManager telephonyManager = (TelephonyManager)
+ context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (telephonyManager == null) {
+ return false;
+ }
+
+ try {
+ Class<?> partypes[] = new Class[0];
+ Method sIsVoiceCapable = TelephonyManager.class.getMethod(
+ "isVoiceCapable", partypes);
+
+ Object arglist[] = new Object[0];
+ Object retobj = sIsVoiceCapable.invoke(telephonyManager, arglist);
+ return (Boolean) retobj;
+ } catch (java.lang.reflect.InvocationTargetException ite) {
+ // Failure, must be another device.
+ // Assume that it is voice capable.
+ } catch (IllegalAccessException iae) {
+ // Failure, must be an other device.
+ // Assume that it is voice capable.
+ } catch (NoSuchMethodException nsme) {
+ }
+ return true;
+ }
+
+ // This is for test only. Allow the camera to launch the specific camera.
+ public static int getCameraFacingIntentExtras(Activity currentActivity) {
+ int cameraId = -1;
+
+ int intentCameraId =
+ currentActivity.getIntent().getIntExtra(Util.EXTRAS_CAMERA_FACING, -1);
+
+ if (isFrontCameraIntent(intentCameraId)) {
+ // Check if the front camera exist
+ int frontCameraId = CameraHolder.instance().getFrontCameraId();
+ if (frontCameraId != -1) {
+ cameraId = frontCameraId;
+ }
+ } else if (isBackCameraIntent(intentCameraId)) {
+ // Check if the back camera exist
+ int backCameraId = CameraHolder.instance().getBackCameraId();
+ if (backCameraId != -1) {
+ cameraId = backCameraId;
+ }
+ }
+ return cameraId;
+ }
+
+ private static boolean isFrontCameraIntent(int intentCameraId) {
+ return (intentCameraId == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT);
+ }
+
+ private static boolean isBackCameraIntent(int intentCameraId) {
+ return (intentCameraId == android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK);
+ }
+
+ private static int sLocation[] = new int[2];
+
+ // This method is not thread-safe.
+ public static boolean pointInView(float x, float y, View v) {
+ v.getLocationInWindow(sLocation);
+ return x >= sLocation[0] && x < (sLocation[0] + v.getWidth())
+ && y >= sLocation[1] && y < (sLocation[1] + v.getHeight());
+ }
+
+ public static int[] getRelativeLocation(View reference, View view) {
+ reference.getLocationInWindow(sLocation);
+ int referenceX = sLocation[0];
+ int referenceY = sLocation[1];
+ view.getLocationInWindow(sLocation);
+ sLocation[0] -= referenceX;
+ sLocation[1] -= referenceY;
+ return sLocation;
+ }
+
+ public static boolean isUriValid(Uri uri, ContentResolver resolver) {
+ if (uri == null) return false;
+
+ try {
+ ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r");
+ if (pfd == null) {
+ Log.e(TAG, "Fail to open URI. URI=" + uri);
+ return false;
+ }
+ pfd.close();
+ } catch (IOException ex) {
+ return false;
+ }
+ return true;
+ }
+
+ public static void viewUri(Uri uri, Context context) {
+ if (!isUriValid(uri, context.getContentResolver())) {
+ Log.e(TAG, "Uri invalid. uri=" + uri);
+ return;
+ }
+
+ try {
+ context.startActivity(new Intent(Util.REVIEW_ACTION, uri));
+ } catch (ActivityNotFoundException ex) {
+ try {
+ context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "review image fail. uri=" + uri, e);
+ }
+ }
+ }
+
+ public static void dumpRect(RectF rect, String msg) {
+ Log.v(TAG, msg + "=(" + rect.left + "," + rect.top
+ + "," + rect.right + "," + rect.bottom + ")");
+ }
+
+ public static void rectFToRect(RectF rectF, Rect rect) {
+ rect.left = Math.round(rectF.left);
+ rect.top = Math.round(rectF.top);
+ rect.right = Math.round(rectF.right);
+ rect.bottom = Math.round(rectF.bottom);
+ }
+
+ public static void prepareMatrix(Matrix matrix, boolean mirror, int displayOrientation,
+ int viewWidth, int viewHeight) {
+ // Need mirror for front camera.
+ matrix.setScale(mirror ? -1 : 1, 1);
+ // This is the value for android.hardware.Camera.setDisplayOrientation.
+ matrix.postRotate(displayOrientation);
+ // Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
+ // UI coordinates range from (0, 0) to (width, height).
+ matrix.postScale(viewWidth / 2000f, viewHeight / 2000f);
+ matrix.postTranslate(viewWidth / 2f, viewHeight / 2f);
+ }
+
+ public static String createJpegName(long dateTaken) {
+ synchronized (sImageFileNamer) {
+ return sImageFileNamer.generateName(dateTaken);
+ }
+ }
+
+ public static void broadcastNewPicture(Context context, Uri uri) {
+ context.sendBroadcast(new Intent(ACTION_NEW_PICTURE, uri));
+ // Keep compatibility
+ context.sendBroadcast(new Intent("com.android.camera.NEW_PICTURE", uri));
+ }
+
+ public static void fadeIn(View view, float startAlpha, float endAlpha, long duration) {
+ if (view.getVisibility() == View.VISIBLE) return;
+
+ view.setVisibility(View.VISIBLE);
+ Animation animation = new AlphaAnimation(startAlpha, endAlpha);
+ animation.setDuration(duration);
+ view.startAnimation(animation);
+ }
+
+ public static void fadeIn(View view) {
+ fadeIn(view, 0F, 1F, 400);
+
+ // We disabled the button in fadeOut(), so enable it here.
+ view.setEnabled(true);
+ }
+
+ public static void fadeOut(View view) {
+ if (view.getVisibility() != View.VISIBLE) return;
+
+ // Since the button is still clickable before fade-out animation
+ // ends, we disable the button first to block click.
+ view.setEnabled(false);
+ Animation animation = new AlphaAnimation(1F, 0F);
+ animation.setDuration(400);
+ view.startAnimation(animation);
+ view.setVisibility(View.GONE);
+ }
+
+ public static int getJpegRotation(int cameraId, int orientation) {
+ // See android.hardware.Camera.Parameters.setRotation for
+ // documentation.
+ int rotation = 0;
+ if (orientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
+ CameraInfo info = CameraHolder.instance().getCameraInfo()[cameraId];
+ if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
+ rotation = (info.orientation - orientation + 360) % 360;
+ } else { // back-facing camera
+ rotation = (info.orientation + orientation) % 360;
+ }
+ }
+ return rotation;
+ }
+
+ public static void setGpsParameters(Parameters parameters, Location loc) {
+ // Clear previous GPS location from the parameters.
+ parameters.removeGpsData();
+
+ // We always encode GpsTimeStamp
+ parameters.setGpsTimestamp(System.currentTimeMillis() / 1000);
+
+ // Set GPS location.
+ if (loc != null) {
+ double lat = loc.getLatitude();
+ double lon = loc.getLongitude();
+ boolean hasLatLon = (lat != 0.0d) || (lon != 0.0d);
+
+ if (hasLatLon) {
+ Log.d(TAG, "Set gps location");
+ parameters.setGpsLatitude(lat);
+ parameters.setGpsLongitude(lon);
+ parameters.setGpsProcessingMethod(loc.getProvider().toUpperCase());
+ if (loc.hasAltitude()) {
+ parameters.setGpsAltitude(loc.getAltitude());
+ } else {
+ // for NETWORK_PROVIDER location provider, we may have
+ // no altitude information, but the driver needs it, so
+ // we fake one.
+ parameters.setGpsAltitude(0);
+ }
+ if (loc.getTime() != 0) {
+ // Location.getTime() is UTC in milliseconds.
+ // gps-timestamp is UTC in seconds.
+ long utcTimeSeconds = loc.getTime() / 1000;
+ parameters.setGpsTimestamp(utcTimeSeconds);
+ }
+ } else {
+ loc = null;
+ }
+ }
+ }
+
+
+ public static int[] getMaxPreviewFpsRange(Parameters params) {
+ List<int[]> frameRates = params.getSupportedPreviewFpsRange();
+ if (frameRates != null && frameRates.size() > 0) {
+ // The list is sorted. Return the last element.
+ return frameRates.get(frameRates.size() - 1);
+ }
+ return new int[0];
+ }
+
+ private static class ImageFileNamer {
+ private SimpleDateFormat mFormat;
+
+ // The date (in milliseconds) used to generate the last name.
+ private long mLastDate;
+
+ // Number of names generated for the same second.
+ private int mSameSecondCount;
+
+ public ImageFileNamer(String format) {
+ mFormat = new SimpleDateFormat(format);
+ }
+
+ public String generateName(long dateTaken) {
+ Date date = new Date(dateTaken);
+ String result = mFormat.format(date);
+
+ // If the last name was generated for the same second,
+ // we append _1, _2, etc to the name.
+ if (dateTaken / 1000 == mLastDate / 1000) {
+ mSameSecondCount++;
+ result += "_" + mSameSecondCount;
+ } else {
+ mLastDate = dateTaken;
+ mSameSecondCount = 0;
+ }
+
+ return result;
+ }
+ }
+
+ public static void playVideo(Context context, Uri uri, String title) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW)
+ .setDataAndType(uri, "video/*")
+ .putExtra(Intent.EXTRA_TITLE, title)
+ .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true);
+ context.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(context, context.getString(R.string.video_err),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+}
diff --git a/src/com/android/camera/VideoController.java b/src/com/android/camera/VideoController.java
new file mode 100644
index 000000000..e84654821
--- /dev/null
+++ b/src/com/android/camera/VideoController.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2013 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.camera;
+
+import android.view.View;
+
+import com.android.camera.ShutterButton.OnShutterButtonListener;
+
+public interface VideoController extends OnShutterButtonListener {
+
+ public void onReviewDoneClicked(View view);
+ public void onReviewCancelClicked(View viwe);
+ public void onReviewPlayClicked(View view);
+
+ public boolean isVideoCaptureIntent();
+ public boolean isInReviewMode();
+ public int onZoomChanged(int index);
+
+ public void onSingleTapUp(View view, int x, int y);
+
+ public void stopPreview();
+
+ public void updateCameraOrientation();
+
+ // Callbacks for camera preview UI events.
+ public void onPreviewUIReady();
+ public void onPreviewUIDestroyed();
+}
diff --git a/src/com/android/camera/VideoMenu.java b/src/com/android/camera/VideoMenu.java
new file mode 100644
index 000000000..da0bde10e
--- /dev/null
+++ b/src/com/android/camera/VideoMenu.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.LayoutInflater;
+
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.ListPrefSettingPopup;
+import com.android.camera.ui.MoreSettingPopup;
+import com.android.camera.ui.PieItem;
+import com.android.camera.ui.PieItem.OnClickListener;
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.TimeIntervalPopup;
+import com.android.gallery3d.R;
+
+public class VideoMenu extends PieController
+ implements MoreSettingPopup.Listener,
+ ListPrefSettingPopup.Listener,
+ TimeIntervalPopup.Listener {
+
+ private static String TAG = "CAM_VideoMenu";
+
+ private VideoUI mUI;
+ private String[] mOtherKeys;
+ private AbstractSettingPopup mPopup;
+
+ private static final int POPUP_NONE = 0;
+ private static final int POPUP_FIRST_LEVEL = 1;
+ private static final int POPUP_SECOND_LEVEL = 2;
+ private int mPopupStatus;
+ private CameraActivity mActivity;
+
+ public VideoMenu(CameraActivity activity, VideoUI ui, PieRenderer pie) {
+ super(activity, pie);
+ mUI = ui;
+ mActivity = activity;
+ }
+
+
+ public void initialize(PreferenceGroup group) {
+ super.initialize(group);
+ mPopup = null;
+ mPopupStatus = POPUP_NONE;
+ PieItem item = null;
+ // white balance
+ if (group.findPreference(CameraSettings.KEY_WHITE_BALANCE) != null) {
+ item = makeItem(CameraSettings.KEY_WHITE_BALANCE);
+ mRenderer.addItem(item);
+ }
+ // settings popup
+ mOtherKeys = new String[] {
+ CameraSettings.KEY_VIDEO_EFFECT,
+ CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL,
+ CameraSettings.KEY_VIDEO_QUALITY,
+ CameraSettings.KEY_RECORD_LOCATION
+ };
+ item = makeItem(R.drawable.ic_settings_holo_light);
+ item.setLabel(mActivity.getResources().getString(R.string.camera_menu_settings_label));
+ item.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(PieItem item) {
+ if (mPopup == null || mPopupStatus != POPUP_FIRST_LEVEL) {
+ initializePopup();
+ mPopupStatus = POPUP_FIRST_LEVEL;
+ }
+ mUI.showPopup(mPopup);
+ }
+ });
+ mRenderer.addItem(item);
+ // camera switcher
+ if (group.findPreference(CameraSettings.KEY_CAMERA_ID) != null) {
+ item = makeItem(R.drawable.ic_switch_back);
+ IconListPreference lpref = (IconListPreference) group.findPreference(
+ CameraSettings.KEY_CAMERA_ID);
+ item.setLabel(lpref.getLabel());
+ item.setImageResource(mActivity,
+ ((IconListPreference) lpref).getIconIds()
+ [lpref.findIndexOfValue(lpref.getValue())]);
+
+ final PieItem fitem = item;
+ item.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(PieItem item) {
+ // Find the index of next camera.
+ ListPreference pref =
+ mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+ if (pref != null) {
+ int index = pref.findIndexOfValue(pref.getValue());
+ CharSequence[] values = pref.getEntryValues();
+ index = (index + 1) % values.length;
+ int newCameraId = Integer.parseInt((String) values[index]);
+ fitem.setImageResource(mActivity,
+ ((IconListPreference) pref).getIconIds()[index]);
+ fitem.setLabel(pref.getLabel());
+ mListener.onCameraPickerClicked(newCameraId);
+ }
+ }
+ });
+ mRenderer.addItem(item);
+ }
+ // flash
+ if (group.findPreference(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE) != null) {
+ item = makeItem(CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE);
+ mRenderer.addItem(item);
+ }
+ }
+
+ @Override
+ public void reloadPreferences() {
+ super.reloadPreferences();
+ if (mPopup != null) {
+ mPopup.reloadPreference();
+ }
+ }
+
+ @Override
+ public void overrideSettings(final String ... keyvalues) {
+ super.overrideSettings(keyvalues);
+ if (mPopup == null || mPopupStatus != POPUP_FIRST_LEVEL) {
+ mPopupStatus = POPUP_FIRST_LEVEL;
+ initializePopup();
+ }
+ ((MoreSettingPopup) mPopup).overrideSettings(keyvalues);
+ }
+
+ @Override
+ // Hit when an item in the second-level popup gets selected
+ public void onListPrefChanged(ListPreference pref) {
+ if (mPopup != null) {
+ if (mPopupStatus == POPUP_SECOND_LEVEL) {
+ mUI.dismissPopup(true);
+ }
+ }
+ super.onSettingChanged(pref);
+ }
+
+ protected void initializePopup() {
+ LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+
+ MoreSettingPopup popup = (MoreSettingPopup) inflater.inflate(
+ R.layout.more_setting_popup, null, false);
+ popup.setSettingChangedListener(this);
+ popup.initialize(mPreferenceGroup, mOtherKeys);
+ if (mActivity.isSecureCamera()) {
+ // Prevent location preference from getting changed in secure camera mode
+ popup.setPreferenceEnabled(CameraSettings.KEY_RECORD_LOCATION, false);
+ }
+ mPopup = popup;
+ }
+
+ public void popupDismissed(boolean topPopupOnly) {
+ // if the 2nd level popup gets dismissed
+ if (mPopupStatus == POPUP_SECOND_LEVEL) {
+ initializePopup();
+ mPopupStatus = POPUP_FIRST_LEVEL;
+ if (topPopupOnly) mUI.showPopup(mPopup);
+ }
+ }
+
+ @Override
+ // Hit when an item in the first-level popup gets selected, then bring up
+ // the second-level popup
+ public void onPreferenceClicked(ListPreference pref) {
+ if (mPopupStatus != POPUP_FIRST_LEVEL) return;
+
+ LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+
+ if (CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL.equals(pref.getKey())) {
+ TimeIntervalPopup timeInterval = (TimeIntervalPopup) inflater.inflate(
+ R.layout.time_interval_popup, null, false);
+ timeInterval.initialize((IconListPreference) pref);
+ timeInterval.setSettingChangedListener(this);
+ mUI.dismissPopup(true);
+ mPopup = timeInterval;
+ } else {
+ ListPrefSettingPopup basic = (ListPrefSettingPopup) inflater.inflate(
+ R.layout.list_pref_setting_popup, null, false);
+ basic.initialize(pref);
+ basic.setSettingChangedListener(this);
+ mUI.dismissPopup(true);
+ mPopup = basic;
+ }
+ mUI.showPopup(mPopup);
+ mPopupStatus = POPUP_SECOND_LEVEL;
+ }
+}
diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java
new file mode 100644
index 000000000..956890e5e
--- /dev/null
+++ b/src/com/android/camera/VideoModule.java
@@ -0,0 +1,2233 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences.Editor;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera.CameraInfo;
+import android.hardware.Camera.Parameters;
+import android.hardware.Camera.Size;
+import android.location.Location;
+import android.media.CamcorderProfile;
+import android.media.CameraProfile;
+import android.media.MediaRecorder;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.provider.MediaStore.MediaColumns;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.android.camera.CameraManager.CameraPictureCallback;
+import com.android.camera.CameraManager.CameraProxy;
+import com.android.camera.ui.PopupManager;
+import com.android.camera.ui.RotateTextToast;
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.OrientationManager;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.util.AccessibilityUtils;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+
+public class VideoModule implements CameraModule,
+ VideoController,
+ CameraPreference.OnPreferenceChangedListener,
+ ShutterButton.OnShutterButtonListener,
+ MediaRecorder.OnErrorListener,
+ MediaRecorder.OnInfoListener,
+ EffectsRecorder.EffectsListener {
+
+ private static final String TAG = "CAM_VideoModule";
+
+ // We number the request code from 1000 to avoid collision with Gallery.
+ private static final int REQUEST_EFFECT_BACKDROPPER = 1000;
+
+ private static final int CHECK_DISPLAY_ROTATION = 3;
+ private static final int CLEAR_SCREEN_DELAY = 4;
+ private static final int UPDATE_RECORD_TIME = 5;
+ private static final int ENABLE_SHUTTER_BUTTON = 6;
+ private static final int SHOW_TAP_TO_SNAPSHOT_TOAST = 7;
+ private static final int SWITCH_CAMERA = 8;
+ private static final int SWITCH_CAMERA_START_ANIMATION = 9;
+ private static final int HIDE_SURFACE_VIEW = 10;
+ private static final int CAPTURE_ANIMATION_DONE = 11;
+
+ private static final int SCREEN_DELAY = 2 * 60 * 1000;
+
+ private static final long SHUTTER_BUTTON_TIMEOUT = 500L; // 500ms
+
+ /**
+ * An unpublished intent flag requesting to start recording straight away
+ * and return as soon as recording is stopped.
+ * TODO: consider publishing by moving into MediaStore.
+ */
+ private static final String EXTRA_QUICK_CAPTURE =
+ "android.intent.extra.quickCapture";
+
+ private static final int MIN_THUMB_SIZE = 64;
+ // module fields
+ private CameraActivity mActivity;
+ private boolean mPaused;
+ private int mCameraId;
+ private Parameters mParameters;
+
+ private boolean mIsInReviewMode;
+ private boolean mSnapshotInProgress = false;
+
+ private static final String EFFECT_BG_FROM_GALLERY = "gallery";
+
+ private final CameraErrorCallback mErrorCallback = new CameraErrorCallback();
+
+ private ComboPreferences mPreferences;
+ private PreferenceGroup mPreferenceGroup;
+ // Preference must be read before starting preview. We check this before starting
+ // preview.
+ private boolean mPreferenceRead;
+
+ private boolean mIsVideoCaptureIntent;
+ private boolean mQuickCapture;
+
+ private MediaRecorder mMediaRecorder;
+ private EffectsRecorder mEffectsRecorder;
+ private boolean mEffectsDisplayResult;
+
+ private int mEffectType = EffectsRecorder.EFFECT_NONE;
+ private Object mEffectParameter = null;
+ private String mEffectUriFromGallery = null;
+ private String mPrefVideoEffectDefault;
+ private boolean mResetEffect = true;
+
+ private boolean mSwitchingCamera;
+ private boolean mMediaRecorderRecording = false;
+ private long mRecordingStartTime;
+ private boolean mRecordingTimeCountsDown = false;
+ private long mOnResumeTime;
+ // The video file that the hardware camera is about to record into
+ // (or is recording into.)
+ private String mVideoFilename;
+ private ParcelFileDescriptor mVideoFileDescriptor;
+
+ // The video file that has already been recorded, and that is being
+ // examined by the user.
+ private String mCurrentVideoFilename;
+ private Uri mCurrentVideoUri;
+ private ContentValues mCurrentVideoValues;
+
+ private CamcorderProfile mProfile;
+
+ // The video duration limit. 0 menas no limit.
+ private int mMaxVideoDurationInMs;
+
+ // Time Lapse parameters.
+ private boolean mCaptureTimeLapse = false;
+ // Default 0. If it is larger than 0, the camcorder is in time lapse mode.
+ private int mTimeBetweenTimeLapseFrameCaptureMs = 0;
+
+ boolean mPreviewing = false; // True if preview is started.
+ // The display rotation in degrees. This is only valid when mPreviewing is
+ // true.
+ private int mDisplayRotation;
+ private int mCameraDisplayOrientation;
+
+ private int mDesiredPreviewWidth;
+ private int mDesiredPreviewHeight;
+ private ContentResolver mContentResolver;
+
+ private LocationManager mLocationManager;
+ private OrientationManager mOrientationManager;
+
+ private Surface mSurface;
+ private int mPendingSwitchCameraId;
+ private boolean mOpenCameraFail;
+ private boolean mCameraDisabled;
+ private final Handler mHandler = new MainHandler();
+ private VideoUI mUI;
+ private CameraProxy mCameraDevice;
+
+ // The degrees of the device rotated clockwise from its natural orientation.
+ private int mOrientation = OrientationEventListener.ORIENTATION_UNKNOWN;
+
+ private int mZoomValue; // The current zoom value.
+
+ private boolean mRestoreFlash; // This is used to check if we need to restore the flash
+ // status when going back from gallery.
+
+ private final MediaSaveService.OnMediaSavedListener mOnVideoSavedListener =
+ new MediaSaveService.OnMediaSavedListener() {
+ @Override
+ public void onMediaSaved(Uri uri) {
+ if (uri != null) {
+ mActivity.sendBroadcast(
+ new Intent(Util.ACTION_NEW_VIDEO, uri));
+ Util.broadcastNewPicture(mActivity, uri);
+ }
+ }
+ };
+
+ private final MediaSaveService.OnMediaSavedListener mOnPhotoSavedListener =
+ new MediaSaveService.OnMediaSavedListener() {
+ @Override
+ public void onMediaSaved(Uri uri) {
+ if (uri != null) {
+ Util.broadcastNewPicture(mActivity, uri);
+ }
+ }
+ };
+
+
+ protected class CameraOpenThread extends Thread {
+ @Override
+ public void run() {
+ openCamera();
+ }
+ }
+
+ private void openCamera() {
+ try {
+ if (mCameraDevice == null) {
+ mCameraDevice = Util.openCamera(mActivity, mCameraId);
+ }
+ mParameters = mCameraDevice.getParameters();
+ } catch (CameraHardwareException e) {
+ mOpenCameraFail = true;
+ } catch (CameraDisabledException e) {
+ mCameraDisabled = true;
+ }
+ }
+
+ // This Handler is used to post message back onto the main thread of the
+ // application
+ private class MainHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+
+ case ENABLE_SHUTTER_BUTTON:
+ mUI.enableShutter(true);
+ break;
+
+ case CLEAR_SCREEN_DELAY: {
+ mActivity.getWindow().clearFlags(
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ break;
+ }
+
+ case UPDATE_RECORD_TIME: {
+ updateRecordingTime();
+ break;
+ }
+
+ case CHECK_DISPLAY_ROTATION: {
+ // Restart the preview if display rotation has changed.
+ // Sometimes this happens when the device is held upside
+ // down and camera app is opened. Rotation animation will
+ // take some time and the rotation value we have got may be
+ // wrong. Framework does not have a callback for this now.
+ if ((Util.getDisplayRotation(mActivity) != mDisplayRotation)
+ && !mMediaRecorderRecording && !mSwitchingCamera) {
+ startPreview();
+ }
+ if (SystemClock.uptimeMillis() - mOnResumeTime < 5000) {
+ mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100);
+ }
+ break;
+ }
+
+ case SHOW_TAP_TO_SNAPSHOT_TOAST: {
+ showTapToSnapshotToast();
+ break;
+ }
+
+ case SWITCH_CAMERA: {
+ switchCamera();
+ break;
+ }
+
+ case SWITCH_CAMERA_START_ANIMATION: {
+ //TODO:
+ //((CameraScreenNail) mActivity.mCameraScreenNail).animateSwitchCamera();
+
+ // Enable all camera controls.
+ mSwitchingCamera = false;
+ break;
+ }
+
+ case CAPTURE_ANIMATION_DONE: {
+ mUI.enablePreviewThumb(false);
+ break;
+ }
+
+ default:
+ Log.v(TAG, "Unhandled message: " + msg.what);
+ break;
+ }
+ }
+ }
+
+ private BroadcastReceiver mReceiver = null;
+
+ private class MyBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
+ stopVideoRecording();
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
+ Toast.makeText(mActivity,
+ mActivity.getResources().getString(R.string.wait), Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+
+ private String createName(long dateTaken) {
+ Date date = new Date(dateTaken);
+ SimpleDateFormat dateFormat = new SimpleDateFormat(
+ mActivity.getString(R.string.video_file_name_format));
+
+ return dateFormat.format(date);
+ }
+
+ private int getPreferredCameraId(ComboPreferences preferences) {
+ int intentCameraId = Util.getCameraFacingIntentExtras(mActivity);
+ if (intentCameraId != -1) {
+ // Testing purpose. Launch a specific camera through the intent
+ // extras.
+ return intentCameraId;
+ } else {
+ return CameraSettings.readPreferredCameraId(preferences);
+ }
+ }
+
+ private void initializeSurfaceView() {
+ if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) { // API level < 16
+ mUI.initializeSurfaceView();
+ }
+ }
+
+ @Override
+ public void init(CameraActivity activity, View root) {
+ mActivity = activity;
+ mUI = new VideoUI(activity, this, root);
+ mPreferences = new ComboPreferences(mActivity);
+ CameraSettings.upgradeGlobalPreferences(mPreferences.getGlobal());
+ mCameraId = getPreferredCameraId(mPreferences);
+
+ mPreferences.setLocalId(mActivity, mCameraId);
+ CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+
+ mPrefVideoEffectDefault = mActivity.getString(R.string.pref_video_effect_default);
+ resetEffect();
+ mOrientationManager = new OrientationManager(mActivity);
+
+ /*
+ * To reduce startup time, we start the preview in another thread.
+ * We make sure the preview is started at the end of onCreate.
+ */
+ CameraOpenThread cameraOpenThread = new CameraOpenThread();
+ cameraOpenThread.start();
+
+ mContentResolver = mActivity.getContentResolver();
+
+ // Surface texture is from camera screen nail and startPreview needs it.
+ // This must be done before startPreview.
+ mIsVideoCaptureIntent = isVideoCaptureIntent();
+ initializeSurfaceView();
+
+ // Make sure camera device is opened.
+ try {
+ cameraOpenThread.join();
+ if (mOpenCameraFail) {
+ Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+ return;
+ } else if (mCameraDisabled) {
+ Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+ return;
+ }
+ } catch (InterruptedException ex) {
+ // ignore
+ }
+
+ readVideoPreferences();
+ mUI.setPrefChangedListener(this);
+
+ mQuickCapture = mActivity.getIntent().getBooleanExtra(EXTRA_QUICK_CAPTURE, false);
+ mLocationManager = new LocationManager(mActivity, null);
+
+ mUI.setOrientationIndicator(0, false);
+ setDisplayOrientation();
+
+ mUI.showTimeLapseUI(mCaptureTimeLapse);
+ initializeVideoSnapshot();
+ resizeForPreviewAspectRatio();
+
+ initializeVideoControl();
+ mPendingSwitchCameraId = -1;
+ mUI.updateOnScreenIndicators(mParameters, mPreferences);
+
+ // Disable the shutter button if effects are ON since it might take
+ // a little more time for the effects preview to be ready. We do not
+ // want to allow recording before that happens. The shutter button
+ // will be enabled when we get the message from effectsrecorder that
+ // the preview is running. This becomes critical when the camera is
+ // swapped.
+ if (effectsActive()) {
+ mUI.enableShutter(false);
+ }
+ }
+
+ // SingleTapListener
+ // Preview area is touched. Take a picture.
+ @Override
+ public void onSingleTapUp(View view, int x, int y) {
+ if (mMediaRecorderRecording && effectsActive()) {
+ new RotateTextToast(mActivity, R.string.disable_video_snapshot_hint,
+ mOrientation).show();
+ return;
+ }
+
+ MediaSaveService s = mActivity.getMediaSaveService();
+ if (mPaused || mSnapshotInProgress || effectsActive() || s == null || s.isQueueFull()) {
+ return;
+ }
+
+ if (!mMediaRecorderRecording) {
+ // check for dismissing popup
+ mUI.dismissPopup(true);
+ return;
+ }
+
+ // Set rotation and gps data.
+ int rotation = Util.getJpegRotation(mCameraId, mOrientation);
+ mParameters.setRotation(rotation);
+ Location loc = mLocationManager.getCurrentLocation();
+ Util.setGpsParameters(mParameters, loc);
+ mCameraDevice.setParameters(mParameters);
+
+ Log.v(TAG, "Video snapshot start");
+ mCameraDevice.takePicture(mHandler,
+ null, null, null, new JpegPictureCallback(loc));
+ showVideoSnapshotUI(true);
+ mSnapshotInProgress = true;
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
+ UsageStatistics.ACTION_CAPTURE_DONE, "VideoSnapshot");
+ }
+
+ @Override
+ public void onStop() {}
+
+ private void loadCameraPreferences() {
+ CameraSettings settings = new CameraSettings(mActivity, mParameters,
+ mCameraId, CameraHolder.instance().getCameraInfo());
+ // Remove the video quality preference setting when the quality is given in the intent.
+ mPreferenceGroup = filterPreferenceScreenByIntent(
+ settings.getPreferenceGroup(R.xml.video_preferences));
+ }
+
+ private void initializeVideoControl() {
+ loadCameraPreferences();
+ mUI.initializePopup(mPreferenceGroup);
+ if (effectsActive()) {
+ mUI.overrideSettings(
+ CameraSettings.KEY_VIDEO_QUALITY,
+ Integer.toString(CamcorderProfile.QUALITY_480P));
+ }
+ }
+
+ @Override
+ public void onOrientationChanged(int orientation) {
+ // We keep the last known orientation. So if the user first orient
+ // the camera then point the camera to floor or sky, we still have
+ // the correct orientation.
+ if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) return;
+ int newOrientation = Util.roundOrientation(orientation, mOrientation);
+
+ if (mOrientation != newOrientation) {
+ mOrientation = newOrientation;
+ // The input of effects recorder is affected by
+ // android.hardware.Camera.setDisplayOrientation. Its value only
+ // compensates the camera orientation (no Display.getRotation).
+ // So the orientation hint here should only consider sensor
+ // orientation.
+ if (effectsActive()) {
+ mEffectsRecorder.setOrientationHint(mOrientation);
+ }
+ }
+
+ // Show the toast after getting the first orientation changed.
+ if (mHandler.hasMessages(SHOW_TAP_TO_SNAPSHOT_TOAST)) {
+ mHandler.removeMessages(SHOW_TAP_TO_SNAPSHOT_TOAST);
+ showTapToSnapshotToast();
+ }
+ }
+
+ private void startPlayVideoActivity() {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(mCurrentVideoUri, convertOutputFormatToMimeType(mProfile.fileFormat));
+ try {
+ mActivity.startActivity(intent);
+ } catch (ActivityNotFoundException ex) {
+ Log.e(TAG, "Couldn't view video " + mCurrentVideoUri, ex);
+ }
+ }
+
+ @OnClickAttr
+ public void onReviewPlayClicked(View v) {
+ startPlayVideoActivity();
+ }
+
+ @OnClickAttr
+ public void onReviewDoneClicked(View v) {
+ mIsInReviewMode = false;
+ doReturnToCaller(true);
+ }
+
+ @OnClickAttr
+ public void onReviewCancelClicked(View v) {
+ mIsInReviewMode = false;
+ stopVideoRecording();
+ doReturnToCaller(false);
+ }
+
+ @Override
+ public boolean isInReviewMode() {
+ return mIsInReviewMode;
+ }
+
+ private void onStopVideoRecording() {
+ mEffectsDisplayResult = true;
+ boolean recordFail = stopVideoRecording();
+ if (mIsVideoCaptureIntent) {
+ if (!effectsActive()) {
+ if (mQuickCapture) {
+ doReturnToCaller(!recordFail);
+ } else if (!recordFail) {
+ showCaptureResult();
+ }
+ }
+ } else if (!recordFail){
+ // Start capture animation.
+ if (!mPaused && ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+ // The capture animation is disabled on ICS because we use SurfaceView
+ // for preview during recording. When the recording is done, we switch
+ // back to use SurfaceTexture for preview and we need to stop then start
+ // the preview. This will cause the preview flicker since the preview
+ // will not be continuous for a short period of time.
+ // TODO: need to get the capture animation to work
+ // ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation);
+
+ mUI.enablePreviewThumb(true);
+
+ // Make sure to disable the thumbnail preview after the
+ // animation is done to disable the click target.
+ mHandler.removeMessages(CAPTURE_ANIMATION_DONE);
+ mHandler.sendEmptyMessageDelayed(CAPTURE_ANIMATION_DONE,
+ CaptureAnimManager.getAnimationDuration());
+ }
+ }
+ }
+
+ public void onProtectiveCurtainClick(View v) {
+ // Consume clicks
+ }
+
+ @Override
+ public void onShutterButtonClick() {
+ if (mUI.collapseCameraControls() || mSwitchingCamera) return;
+
+ boolean stop = mMediaRecorderRecording;
+
+ if (stop) {
+ onStopVideoRecording();
+ } else {
+ startVideoRecording();
+ }
+ mUI.enableShutter(false);
+
+ // Keep the shutter button disabled when in video capture intent
+ // mode and recording is stopped. It'll be re-enabled when
+ // re-take button is clicked.
+ if (!(mIsVideoCaptureIntent && stop)) {
+ mHandler.sendEmptyMessageDelayed(
+ ENABLE_SHUTTER_BUTTON, SHUTTER_BUTTON_TIMEOUT);
+ }
+ }
+
+ @Override
+ public void onShutterButtonFocus(boolean pressed) {
+ mUI.setShutterPressed(pressed);
+ }
+
+ private void readVideoPreferences() {
+ // The preference stores values from ListPreference and is thus string type for all values.
+ // We need to convert it to int manually.
+ String videoQuality = mPreferences.getString(CameraSettings.KEY_VIDEO_QUALITY,
+ null);
+ if (videoQuality == null) {
+ // check for highest quality before setting default value
+ videoQuality = CameraSettings.getSupportedHighestVideoQuality(mCameraId,
+ mActivity.getResources().getString(R.string.pref_video_quality_default));
+ mPreferences.edit().putString(CameraSettings.KEY_VIDEO_QUALITY, videoQuality);
+ }
+ int quality = Integer.valueOf(videoQuality);
+
+ // Set video quality.
+ Intent intent = mActivity.getIntent();
+ if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
+ int extraVideoQuality =
+ intent.getIntExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
+ if (extraVideoQuality > 0) {
+ quality = CamcorderProfile.QUALITY_HIGH;
+ } else { // 0 is mms.
+ quality = CamcorderProfile.QUALITY_LOW;
+ }
+ }
+
+ // Set video duration limit. The limit is read from the preference,
+ // unless it is specified in the intent.
+ if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) {
+ int seconds =
+ intent.getIntExtra(MediaStore.EXTRA_DURATION_LIMIT, 0);
+ mMaxVideoDurationInMs = 1000 * seconds;
+ } else {
+ mMaxVideoDurationInMs = CameraSettings.getMaxVideoDuration(mActivity);
+ }
+
+ // Set effect
+ mEffectType = CameraSettings.readEffectType(mPreferences);
+ if (mEffectType != EffectsRecorder.EFFECT_NONE) {
+ mEffectParameter = CameraSettings.readEffectParameter(mPreferences);
+ // Set quality to be no higher than 480p.
+ CamcorderProfile profile = CamcorderProfile.get(mCameraId, quality);
+ if (profile.videoFrameHeight > 480) {
+ quality = CamcorderProfile.QUALITY_480P;
+ }
+ } else {
+ mEffectParameter = null;
+ }
+ // Read time lapse recording interval.
+ if (ApiHelper.HAS_TIME_LAPSE_RECORDING) {
+ String frameIntervalStr = mPreferences.getString(
+ CameraSettings.KEY_VIDEO_TIME_LAPSE_FRAME_INTERVAL,
+ mActivity.getString(R.string.pref_video_time_lapse_frame_interval_default));
+ mTimeBetweenTimeLapseFrameCaptureMs = Integer.parseInt(frameIntervalStr);
+ mCaptureTimeLapse = (mTimeBetweenTimeLapseFrameCaptureMs != 0);
+ }
+ // TODO: This should be checked instead directly +1000.
+ if (mCaptureTimeLapse) quality += 1000;
+ mProfile = CamcorderProfile.get(mCameraId, quality);
+ getDesiredPreviewSize();
+ mPreferenceRead = true;
+ }
+
+ private void writeDefaultEffectToPrefs() {
+ ComboPreferences.Editor editor = mPreferences.edit();
+ editor.putString(CameraSettings.KEY_VIDEO_EFFECT,
+ mActivity.getString(R.string.pref_video_effect_default));
+ editor.apply();
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ private void getDesiredPreviewSize() {
+ mParameters = mCameraDevice.getParameters();
+ if (ApiHelper.HAS_GET_SUPPORTED_VIDEO_SIZE) {
+ if (mParameters.getSupportedVideoSizes() == null || effectsActive()) {
+ mDesiredPreviewWidth = mProfile.videoFrameWidth;
+ mDesiredPreviewHeight = mProfile.videoFrameHeight;
+ } else { // Driver supports separates outputs for preview and video.
+ List<Size> sizes = mParameters.getSupportedPreviewSizes();
+ Size preferred = mParameters.getPreferredPreviewSizeForVideo();
+ int product = preferred.width * preferred.height;
+ Iterator<Size> it = sizes.iterator();
+ // Remove the preview sizes that are not preferred.
+ while (it.hasNext()) {
+ Size size = it.next();
+ if (size.width * size.height > product) {
+ it.remove();
+ }
+ }
+ Size optimalSize = Util.getOptimalPreviewSize(mActivity, sizes,
+ (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight);
+ mDesiredPreviewWidth = optimalSize.width;
+ mDesiredPreviewHeight = optimalSize.height;
+ }
+ } else {
+ mDesiredPreviewWidth = mProfile.videoFrameWidth;
+ mDesiredPreviewHeight = mProfile.videoFrameHeight;
+ }
+ mUI.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight);
+ Log.v(TAG, "mDesiredPreviewWidth=" + mDesiredPreviewWidth +
+ ". mDesiredPreviewHeight=" + mDesiredPreviewHeight);
+ }
+
+ private void resizeForPreviewAspectRatio() {
+ mUI.setAspectRatio(
+ (double) mProfile.videoFrameWidth / mProfile.videoFrameHeight);
+ }
+
+ @Override
+ public void installIntentFilter() {
+ // install an intent filter to receive SD card related events.
+ IntentFilter intentFilter =
+ new IntentFilter(Intent.ACTION_MEDIA_EJECT);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+ intentFilter.addDataScheme("file");
+ mReceiver = new MyBroadcastReceiver();
+ mActivity.registerReceiver(mReceiver, intentFilter);
+ }
+
+ @Override
+ public void onResumeBeforeSuper() {
+ mPaused = false;
+ }
+
+ @Override
+ public void onResumeAfterSuper() {
+ if (mOpenCameraFail || mCameraDisabled)
+ return;
+ mUI.enableShutter(false);
+ mZoomValue = 0;
+
+ showVideoSnapshotUI(false);
+
+ if (!mPreviewing) {
+ resetEffect();
+ openCamera();
+ if (mOpenCameraFail) {
+ Util.showErrorAndFinish(mActivity,
+ R.string.cannot_connect_camera);
+ return;
+ } else if (mCameraDisabled) {
+ Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+ return;
+ }
+ readVideoPreferences();
+ resizeForPreviewAspectRatio();
+ startPreview();
+ } else {
+ // preview already started
+ mUI.enableShutter(true);
+ }
+
+ // Initializing it here after the preview is started.
+ mUI.initializeZoom(mParameters);
+
+ keepScreenOnAwhile();
+
+ // Initialize location service.
+ boolean recordLocation = RecordLocationPreference.get(mPreferences,
+ mContentResolver);
+ mLocationManager.recordLocation(recordLocation);
+
+ if (mPreviewing) {
+ mOnResumeTime = SystemClock.uptimeMillis();
+ mHandler.sendEmptyMessageDelayed(CHECK_DISPLAY_ROTATION, 100);
+ }
+ // Dismiss open menu if exists.
+ PopupManager.getInstance(mActivity).notifyShowPopup(null);
+
+ UsageStatistics.onContentViewChanged(
+ UsageStatistics.COMPONENT_CAMERA, "VideoModule");
+ }
+
+ private void setDisplayOrientation() {
+ mDisplayRotation = Util.getDisplayRotation(mActivity);
+ mCameraDisplayOrientation = Util.getDisplayOrientation(mDisplayRotation, mCameraId);
+ // Change the camera display orientation
+ if (mCameraDevice != null) {
+ mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
+ }
+ }
+
+ @Override
+ public void updateCameraOrientation() {
+ if (mMediaRecorderRecording) return;
+ if (mDisplayRotation != Util.getDisplayRotation(mActivity)) {
+ setDisplayOrientation();
+ }
+ }
+
+ @Override
+ public int onZoomChanged(int index) {
+ // Not useful to change zoom value when the activity is paused.
+ if (mPaused) return index;
+ mZoomValue = index;
+ if (mParameters == null || mCameraDevice == null) return index;
+ // Set zoom parameters asynchronously
+ mParameters.setZoom(mZoomValue);
+ mCameraDevice.setParameters(mParameters);
+ Parameters p = mCameraDevice.getParameters();
+ if (p != null) return p.getZoom();
+ return index;
+ }
+
+ private void startPreview() {
+ Log.v(TAG, "startPreview");
+
+ SurfaceTexture surfaceTexture = mUI.getSurfaceTexture();
+ if (!mPreferenceRead || surfaceTexture == null || mPaused == true) return;
+
+ mCameraDevice.setErrorCallback(mErrorCallback);
+ if (mPreviewing == true) {
+ stopPreview();
+ if (effectsActive() && mEffectsRecorder != null) {
+ mEffectsRecorder.release();
+ mEffectsRecorder = null;
+ }
+ }
+
+ setDisplayOrientation();
+ mCameraDevice.setDisplayOrientation(mCameraDisplayOrientation);
+ setCameraParameters();
+
+ try {
+ if (!effectsActive()) {
+ mCameraDevice.setPreviewTexture(surfaceTexture);
+ mCameraDevice.startPreview();
+ mPreviewing = true;
+ onPreviewStarted();
+ } else {
+ initializeEffectsPreview();
+ mEffectsRecorder.startPreview();
+ mPreviewing = true;
+ onPreviewStarted();
+ }
+ } catch (Throwable ex) {
+ closeCamera();
+ throw new RuntimeException("startPreview failed", ex);
+ } finally {
+ if (mOpenCameraFail) {
+ Util.showErrorAndFinish(mActivity, R.string.cannot_connect_camera);
+ } else if (mCameraDisabled) {
+ Util.showErrorAndFinish(mActivity, R.string.camera_disabled);
+ }
+ }
+
+ }
+
+ private void onPreviewStarted() {
+ mUI.enableShutter(true);
+ }
+
+ @Override
+ public void stopPreview() {
+ if (!mPreviewing) return;
+ mCameraDevice.stopPreview();
+ mPreviewing = false;
+ }
+
+ // Closing the effects out. Will shut down the effects graph.
+ private void closeEffects() {
+ Log.v(TAG, "Closing effects");
+ mEffectType = EffectsRecorder.EFFECT_NONE;
+ if (mEffectsRecorder == null) {
+ Log.d(TAG, "Effects are already closed. Nothing to do");
+ return;
+ }
+ // This call can handle the case where the camera is already released
+ // after the recording has been stopped.
+ mEffectsRecorder.release();
+ mEffectsRecorder = null;
+ }
+
+ // By default, we want to close the effects as well with the camera.
+ private void closeCamera() {
+ closeCamera(true);
+ }
+
+ // In certain cases, when the effects are active, we may want to shutdown
+ // only the camera related parts, and handle closing the effects in the
+ // effectsUpdate callback.
+ // For example, in onPause, we want to make the camera available to
+ // outside world immediately, however, want to wait till the effects
+ // callback to shut down the effects. In such a case, we just disconnect
+ // the effects from the camera by calling disconnectCamera. That way
+ // the effects can handle that when shutting down.
+ //
+ // @param closeEffectsAlso - indicates whether we want to close the
+ // effects also along with the camera.
+ private void closeCamera(boolean closeEffectsAlso) {
+ Log.v(TAG, "closeCamera");
+ if (mCameraDevice == null) {
+ Log.d(TAG, "already stopped.");
+ return;
+ }
+
+ if (mEffectsRecorder != null) {
+ // Disconnect the camera from effects so that camera is ready to
+ // be released to the outside world.
+ mEffectsRecorder.disconnectCamera();
+ }
+ if (closeEffectsAlso) closeEffects();
+ mCameraDevice.setZoomChangeListener(null);
+ mCameraDevice.setErrorCallback(null);
+ CameraHolder.instance().release();
+ mCameraDevice = null;
+ mPreviewing = false;
+ mSnapshotInProgress = false;
+ }
+
+ private void releasePreviewResources() {
+ if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+ mUI.hideSurfaceView();
+ }
+ }
+
+ @Override
+ public void onPauseBeforeSuper() {
+ mPaused = true;
+
+ if (mMediaRecorderRecording) {
+ // Camera will be released in onStopVideoRecording.
+ onStopVideoRecording();
+ } else {
+ closeCamera();
+ if (!effectsActive()) releaseMediaRecorder();
+ }
+ if (effectsActive()) {
+ // If the effects are active, make sure we tell the graph that the
+ // surfacetexture is not valid anymore. Disconnect the graph from
+ // the display. This should be done before releasing the surface
+ // texture.
+ mEffectsRecorder.disconnectDisplay();
+ } else {
+ // Close the file descriptor and clear the video namer only if the
+ // effects are not active. If effects are active, we need to wait
+ // till we get the callback from the Effects that the graph is done
+ // recording. That also needs a change in the stopVideoRecording()
+ // call to not call closeCamera if the effects are active, because
+ // that will close down the effects are well, thus making this if
+ // condition invalid.
+ closeVideoFileDescriptor();
+ }
+
+ releasePreviewResources();
+
+ if (mReceiver != null) {
+ mActivity.unregisterReceiver(mReceiver);
+ mReceiver = null;
+ }
+ resetScreenOn();
+
+ if (mLocationManager != null) mLocationManager.recordLocation(false);
+
+ mHandler.removeMessages(CHECK_DISPLAY_ROTATION);
+ mHandler.removeMessages(SWITCH_CAMERA);
+ mHandler.removeMessages(SWITCH_CAMERA_START_ANIMATION);
+ mPendingSwitchCameraId = -1;
+ mSwitchingCamera = false;
+ mPreferenceRead = false;
+ // Call onPause after stopping video recording. So the camera can be
+ // released as soon as possible.
+ }
+
+ @Override
+ public void onPauseAfterSuper() {
+ }
+
+ @Override
+ public void onUserInteraction() {
+ if (!mMediaRecorderRecording && !mActivity.isFinishing()) {
+ keepScreenOnAwhile();
+ }
+ }
+
+ @Override
+ public boolean onBackPressed() {
+ if (mPaused) return true;
+ if (mMediaRecorderRecording) {
+ onStopVideoRecording();
+ return true;
+ } else if (mUI.hidePieRenderer()) {
+ return true;
+ } else {
+ return mUI.removeTopLevelPopup();
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // Do not handle any key if the activity is paused.
+ if (mPaused) {
+ return true;
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CAMERA:
+ if (event.getRepeatCount() == 0) {
+ mUI.clickShutter();
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (event.getRepeatCount() == 0) {
+ mUI.clickShutter();
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_MENU:
+ if (mMediaRecorderRecording) return true;
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CAMERA:
+ mUI.pressShutter(false);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isVideoCaptureIntent() {
+ String action = mActivity.getIntent().getAction();
+ return (MediaStore.ACTION_VIDEO_CAPTURE.equals(action));
+ }
+
+ private void doReturnToCaller(boolean valid) {
+ Intent resultIntent = new Intent();
+ int resultCode;
+ if (valid) {
+ resultCode = Activity.RESULT_OK;
+ resultIntent.setData(mCurrentVideoUri);
+ } else {
+ resultCode = Activity.RESULT_CANCELED;
+ }
+ mActivity.setResultEx(resultCode, resultIntent);
+ mActivity.finish();
+ }
+
+ private void cleanupEmptyFile() {
+ if (mVideoFilename != null) {
+ File f = new File(mVideoFilename);
+ if (f.length() == 0 && f.delete()) {
+ Log.v(TAG, "Empty video file deleted: " + mVideoFilename);
+ mVideoFilename = null;
+ }
+ }
+ }
+
+ private void setupMediaRecorderPreviewDisplay() {
+ // Nothing to do here if using SurfaceTexture.
+ if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+ // We stop the preview here before unlocking the device because we
+ // need to change the SurfaceTexture to SurfaceView for preview.
+ stopPreview();
+ mCameraDevice.setPreviewDisplay(mUI.getSurfaceHolder());
+ // The orientation for SurfaceTexture is different from that for
+ // SurfaceView. For SurfaceTexture we don't need to consider the
+ // display rotation. Just consider the sensor's orientation and we
+ // will set the orientation correctly when showing the texture.
+ // Gallery will handle the orientation for the preview. For
+ // SurfaceView we will have to take everything into account so the
+ // display rotation is considered.
+ mCameraDevice.setDisplayOrientation(
+ Util.getDisplayOrientation(mDisplayRotation, mCameraId));
+ mCameraDevice.startPreview();
+ mPreviewing = true;
+ mMediaRecorder.setPreviewDisplay(mUI.getSurfaceHolder().getSurface());
+ }
+ }
+
+ // Prepares media recorder.
+ private void initializeRecorder() {
+ Log.v(TAG, "initializeRecorder");
+ // If the mCameraDevice is null, then this activity is going to finish
+ if (mCameraDevice == null) return;
+
+ if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+ // Set the SurfaceView to visible so the surface gets created.
+ // surfaceCreated() is called immediately when the visibility is
+ // changed to visible. Thus, mSurfaceViewReady should become true
+ // right after calling setVisibility().
+ mUI.showSurfaceView();
+ }
+
+ Intent intent = mActivity.getIntent();
+ Bundle myExtras = intent.getExtras();
+
+ long requestedSizeLimit = 0;
+ closeVideoFileDescriptor();
+ if (mIsVideoCaptureIntent && myExtras != null) {
+ Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
+ if (saveUri != null) {
+ try {
+ mVideoFileDescriptor =
+ mContentResolver.openFileDescriptor(saveUri, "rw");
+ mCurrentVideoUri = saveUri;
+ } catch (java.io.FileNotFoundException ex) {
+ // invalid uri
+ Log.e(TAG, ex.toString());
+ }
+ }
+ requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT);
+ }
+ mMediaRecorder = new MediaRecorder();
+
+ setupMediaRecorderPreviewDisplay();
+ // Unlock the camera object before passing it to media recorder.
+ mCameraDevice.unlock();
+ mMediaRecorder.setCamera(mCameraDevice.getCamera());
+ if (!mCaptureTimeLapse) {
+ mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
+ }
+ mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
+ mMediaRecorder.setProfile(mProfile);
+ mMediaRecorder.setMaxDuration(mMaxVideoDurationInMs);
+ if (mCaptureTimeLapse) {
+ double fps = 1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs;
+ setCaptureRate(mMediaRecorder, fps);
+ }
+
+ setRecordLocation();
+
+ // Set output file.
+ // Try Uri in the intent first. If it doesn't exist, use our own
+ // instead.
+ if (mVideoFileDescriptor != null) {
+ mMediaRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor());
+ } else {
+ generateVideoFilename(mProfile.fileFormat);
+ mMediaRecorder.setOutputFile(mVideoFilename);
+ }
+
+ // Set maximum file size.
+ long maxFileSize = mActivity.getStorageSpace() - Storage.LOW_STORAGE_THRESHOLD;
+ if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) {
+ maxFileSize = requestedSizeLimit;
+ }
+
+ try {
+ mMediaRecorder.setMaxFileSize(maxFileSize);
+ } catch (RuntimeException exception) {
+ // We are going to ignore failure of setMaxFileSize here, as
+ // a) The composer selected may simply not support it, or
+ // b) The underlying media framework may not handle 64-bit range
+ // on the size restriction.
+ }
+
+ // See android.hardware.Camera.Parameters.setRotation for
+ // documentation.
+ // Note that mOrientation here is the device orientation, which is the opposite of
+ // what activity.getWindowManager().getDefaultDisplay().getRotation() would return,
+ // which is the orientation the graphics need to rotate in order to render correctly.
+ int rotation = 0;
+ if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
+ CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+ if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
+ rotation = (info.orientation - mOrientation + 360) % 360;
+ } else { // back-facing camera
+ rotation = (info.orientation + mOrientation) % 360;
+ }
+ }
+ mMediaRecorder.setOrientationHint(rotation);
+
+ try {
+ mMediaRecorder.prepare();
+ } catch (IOException e) {
+ Log.e(TAG, "prepare failed for " + mVideoFilename, e);
+ releaseMediaRecorder();
+ throw new RuntimeException(e);
+ }
+
+ mMediaRecorder.setOnErrorListener(this);
+ mMediaRecorder.setOnInfoListener(this);
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ private static void setCaptureRate(MediaRecorder recorder, double fps) {
+ recorder.setCaptureRate(fps);
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void setRecordLocation() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ Location loc = mLocationManager.getCurrentLocation();
+ if (loc != null) {
+ mMediaRecorder.setLocation((float) loc.getLatitude(),
+ (float) loc.getLongitude());
+ }
+ }
+ }
+
+ private void initializeEffectsPreview() {
+ Log.v(TAG, "initializeEffectsPreview");
+ // If the mCameraDevice is null, then this activity is going to finish
+ if (mCameraDevice == null) return;
+
+ boolean inLandscape = (mActivity.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE);
+
+ CameraInfo info = CameraHolder.instance().getCameraInfo()[mCameraId];
+
+ mEffectsDisplayResult = false;
+ mEffectsRecorder = new EffectsRecorder(mActivity);
+
+ // TODO: Confirm none of the following need to go to initializeEffectsRecording()
+ // and none of these change even when the preview is not refreshed.
+ mEffectsRecorder.setCameraDisplayOrientation(mCameraDisplayOrientation);
+ mEffectsRecorder.setCamera(mCameraDevice);
+ mEffectsRecorder.setCameraFacing(info.facing);
+ mEffectsRecorder.setProfile(mProfile);
+ mEffectsRecorder.setEffectsListener(this);
+ mEffectsRecorder.setOnInfoListener(this);
+ mEffectsRecorder.setOnErrorListener(this);
+
+ // The input of effects recorder is affected by
+ // android.hardware.Camera.setDisplayOrientation. Its value only
+ // compensates the camera orientation (no Display.getRotation). So the
+ // orientation hint here should only consider sensor orientation.
+ int orientation = 0;
+ if (mOrientation != OrientationEventListener.ORIENTATION_UNKNOWN) {
+ orientation = mOrientation;
+ }
+ mEffectsRecorder.setOrientationHint(orientation);
+
+ mEffectsRecorder.setPreviewSurfaceTexture(mUI.getSurfaceTexture(),
+ mUI.getPreviewWidth(), mUI.getPreviewHeight());
+
+ if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER &&
+ ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) {
+ mEffectsRecorder.setEffect(mEffectType, mEffectUriFromGallery);
+ } else {
+ mEffectsRecorder.setEffect(mEffectType, mEffectParameter);
+ }
+ }
+
+ private void initializeEffectsRecording() {
+ Log.v(TAG, "initializeEffectsRecording");
+
+ Intent intent = mActivity.getIntent();
+ Bundle myExtras = intent.getExtras();
+
+ long requestedSizeLimit = 0;
+ closeVideoFileDescriptor();
+ if (mIsVideoCaptureIntent && myExtras != null) {
+ Uri saveUri = (Uri) myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
+ if (saveUri != null) {
+ try {
+ mVideoFileDescriptor =
+ mContentResolver.openFileDescriptor(saveUri, "rw");
+ mCurrentVideoUri = saveUri;
+ } catch (java.io.FileNotFoundException ex) {
+ // invalid uri
+ Log.e(TAG, ex.toString());
+ }
+ }
+ requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT);
+ }
+
+ mEffectsRecorder.setProfile(mProfile);
+ // important to set the capture rate to zero if not timelapsed, since the
+ // effectsrecorder object does not get created again for each recording
+ // session
+ if (mCaptureTimeLapse) {
+ mEffectsRecorder.setCaptureRate((1000 / (double) mTimeBetweenTimeLapseFrameCaptureMs));
+ } else {
+ mEffectsRecorder.setCaptureRate(0);
+ }
+
+ // Set output file
+ if (mVideoFileDescriptor != null) {
+ mEffectsRecorder.setOutputFile(mVideoFileDescriptor.getFileDescriptor());
+ } else {
+ generateVideoFilename(mProfile.fileFormat);
+ mEffectsRecorder.setOutputFile(mVideoFilename);
+ }
+
+ // Set maximum file size.
+ long maxFileSize = mActivity.getStorageSpace() - Storage.LOW_STORAGE_THRESHOLD;
+ if (requestedSizeLimit > 0 && requestedSizeLimit < maxFileSize) {
+ maxFileSize = requestedSizeLimit;
+ }
+ mEffectsRecorder.setMaxFileSize(maxFileSize);
+ mEffectsRecorder.setMaxDuration(mMaxVideoDurationInMs);
+ }
+
+
+ private void releaseMediaRecorder() {
+ Log.v(TAG, "Releasing media recorder.");
+ if (mMediaRecorder != null) {
+ cleanupEmptyFile();
+ mMediaRecorder.reset();
+ mMediaRecorder.release();
+ mMediaRecorder = null;
+ }
+ mVideoFilename = null;
+ }
+
+ private void releaseEffectsRecorder() {
+ Log.v(TAG, "Releasing effects recorder.");
+ if (mEffectsRecorder != null) {
+ cleanupEmptyFile();
+ mEffectsRecorder.release();
+ mEffectsRecorder = null;
+ }
+ mEffectType = EffectsRecorder.EFFECT_NONE;
+ mVideoFilename = null;
+ }
+
+ private void generateVideoFilename(int outputFileFormat) {
+ long dateTaken = System.currentTimeMillis();
+ String title = createName(dateTaken);
+ // Used when emailing.
+ String filename = title + convertOutputFormatToFileExt(outputFileFormat);
+ String mime = convertOutputFormatToMimeType(outputFileFormat);
+ String path = Storage.DIRECTORY + '/' + filename;
+ String tmpPath = path + ".tmp";
+ mCurrentVideoValues = new ContentValues(9);
+ mCurrentVideoValues.put(Video.Media.TITLE, title);
+ mCurrentVideoValues.put(Video.Media.DISPLAY_NAME, filename);
+ mCurrentVideoValues.put(Video.Media.DATE_TAKEN, dateTaken);
+ mCurrentVideoValues.put(MediaColumns.DATE_MODIFIED, dateTaken / 1000);
+ mCurrentVideoValues.put(Video.Media.MIME_TYPE, mime);
+ mCurrentVideoValues.put(Video.Media.DATA, path);
+ mCurrentVideoValues.put(Video.Media.RESOLUTION,
+ Integer.toString(mProfile.videoFrameWidth) + "x" +
+ Integer.toString(mProfile.videoFrameHeight));
+ Location loc = mLocationManager.getCurrentLocation();
+ if (loc != null) {
+ mCurrentVideoValues.put(Video.Media.LATITUDE, loc.getLatitude());
+ mCurrentVideoValues.put(Video.Media.LONGITUDE, loc.getLongitude());
+ }
+ mVideoFilename = tmpPath;
+ Log.v(TAG, "New video filename: " + mVideoFilename);
+ }
+
+ private void saveVideo() {
+ if (mVideoFileDescriptor == null) {
+ long duration = SystemClock.uptimeMillis() - mRecordingStartTime;
+ if (duration > 0) {
+ if (mCaptureTimeLapse) {
+ duration = getTimeLapseVideoLength(duration);
+ }
+ } else {
+ Log.w(TAG, "Video duration <= 0 : " + duration);
+ }
+ mActivity.getMediaSaveService().addVideo(mCurrentVideoFilename,
+ duration, mCurrentVideoValues,
+ mOnVideoSavedListener, mContentResolver);
+ }
+ mCurrentVideoValues = null;
+ }
+
+ private void deleteVideoFile(String fileName) {
+ Log.v(TAG, "Deleting video " + fileName);
+ File f = new File(fileName);
+ if (!f.delete()) {
+ Log.v(TAG, "Could not delete " + fileName);
+ }
+ }
+
+ private PreferenceGroup filterPreferenceScreenByIntent(
+ PreferenceGroup screen) {
+ Intent intent = mActivity.getIntent();
+ if (intent.hasExtra(MediaStore.EXTRA_VIDEO_QUALITY)) {
+ CameraSettings.removePreferenceFromScreen(screen,
+ CameraSettings.KEY_VIDEO_QUALITY);
+ }
+
+ if (intent.hasExtra(MediaStore.EXTRA_DURATION_LIMIT)) {
+ CameraSettings.removePreferenceFromScreen(screen,
+ CameraSettings.KEY_VIDEO_QUALITY);
+ }
+ return screen;
+ }
+
+ // from MediaRecorder.OnErrorListener
+ @Override
+ public void onError(MediaRecorder mr, int what, int extra) {
+ Log.e(TAG, "MediaRecorder error. what=" + what + ". extra=" + extra);
+ if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) {
+ // We may have run out of space on the sdcard.
+ stopVideoRecording();
+ mActivity.updateStorageSpaceAndHint();
+ }
+ }
+
+ // from MediaRecorder.OnInfoListener
+ @Override
+ public void onInfo(MediaRecorder mr, int what, int extra) {
+ if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
+ if (mMediaRecorderRecording) onStopVideoRecording();
+ } else if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
+ if (mMediaRecorderRecording) onStopVideoRecording();
+
+ // Show the toast.
+ Toast.makeText(mActivity, R.string.video_reach_size_limit,
+ Toast.LENGTH_LONG).show();
+ }
+ }
+
+ /*
+ * Make sure we're not recording music playing in the background, ask the
+ * MediaPlaybackService to pause playback.
+ */
+ private void pauseAudioPlayback() {
+ // Shamelessly copied from MediaPlaybackService.java, which
+ // should be public, but isn't.
+ Intent i = new Intent("com.android.music.musicservicecommand");
+ i.putExtra("command", "pause");
+
+ mActivity.sendBroadcast(i);
+ }
+
+ // For testing.
+ public boolean isRecording() {
+ return mMediaRecorderRecording;
+ }
+
+ private void startVideoRecording() {
+ Log.v(TAG, "startVideoRecording");
+ mUI.enablePreviewThumb(false);
+ mUI.setSwipingEnabled(false);
+
+ mActivity.updateStorageSpaceAndHint();
+ if (mActivity.getStorageSpace() <= Storage.LOW_STORAGE_THRESHOLD) {
+ Log.v(TAG, "Storage issue, ignore the start request");
+ return;
+ }
+
+ //??
+ //if (!mCameraDevice.waitDone()) return;
+ mCurrentVideoUri = null;
+ if (effectsActive()) {
+ initializeEffectsRecording();
+ if (mEffectsRecorder == null) {
+ Log.e(TAG, "Fail to initialize effect recorder");
+ return;
+ }
+ } else {
+ initializeRecorder();
+ if (mMediaRecorder == null) {
+ Log.e(TAG, "Fail to initialize media recorder");
+ return;
+ }
+ }
+
+ pauseAudioPlayback();
+
+ if (effectsActive()) {
+ try {
+ mEffectsRecorder.startRecording();
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Could not start effects recorder. ", e);
+ releaseEffectsRecorder();
+ return;
+ }
+ } else {
+ try {
+ mMediaRecorder.start(); // Recording is now started
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Could not start media recorder. ", e);
+ releaseMediaRecorder();
+ // If start fails, frameworks will not lock the camera for us.
+ mCameraDevice.lock();
+ return;
+ }
+ }
+
+ // Make sure the video recording has started before announcing
+ // this in accessibility.
+ AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(),
+ mActivity.getString(R.string.video_recording_started));
+
+ // The parameters might have been altered by MediaRecorder already.
+ // We need to force mCameraDevice to refresh before getting it.
+ mCameraDevice.refreshParameters();
+ // The parameters may have been changed by MediaRecorder upon starting
+ // recording. We need to alter the parameters if we support camcorder
+ // zoom. To reduce latency when setting the parameters during zoom, we
+ // update mParameters here once.
+ if (ApiHelper.HAS_ZOOM_WHEN_RECORDING) {
+ mParameters = mCameraDevice.getParameters();
+ }
+
+ mUI.enableCameraControls(false);
+
+ mMediaRecorderRecording = true;
+ mOrientationManager.lockOrientation();
+ mRecordingStartTime = SystemClock.uptimeMillis();
+ mUI.showRecordingUI(true, mParameters.isZoomSupported());
+
+ updateRecordingTime();
+ keepScreenOn();
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
+ UsageStatistics.ACTION_CAPTURE_START, "Video");
+ }
+
+ private void showCaptureResult() {
+ mIsInReviewMode = true;
+ Bitmap bitmap = null;
+ if (mVideoFileDescriptor != null) {
+ bitmap = Thumbnail.createVideoThumbnailBitmap(mVideoFileDescriptor.getFileDescriptor(),
+ mDesiredPreviewWidth);
+ } else if (mCurrentVideoFilename != null) {
+ bitmap = Thumbnail.createVideoThumbnailBitmap(mCurrentVideoFilename,
+ mDesiredPreviewWidth);
+ }
+ if (bitmap != null) {
+ // MetadataRetriever already rotates the thumbnail. We should rotate
+ // it to match the UI orientation (and mirror if it is front-facing camera).
+ CameraInfo[] info = CameraHolder.instance().getCameraInfo();
+ boolean mirror = (info[mCameraId].facing == CameraInfo.CAMERA_FACING_FRONT);
+ bitmap = Util.rotateAndMirror(bitmap, 0, mirror);
+ mUI.showReviewImage(bitmap);
+ }
+
+ mUI.showReviewControls();
+ mUI.enableCameraControls(false);
+ mUI.showTimeLapseUI(false);
+ }
+
+ private void hideAlert() {
+ mUI.enableCameraControls(true);
+ mUI.hideReviewUI();
+ if (mCaptureTimeLapse) {
+ mUI.showTimeLapseUI(true);
+ }
+ }
+
+ private boolean stopVideoRecording() {
+ Log.v(TAG, "stopVideoRecording");
+ mUI.setSwipingEnabled(true);
+ mUI.showSwitcher();
+
+ boolean fail = false;
+ if (mMediaRecorderRecording) {
+ boolean shouldAddToMediaStoreNow = false;
+
+ try {
+ if (effectsActive()) {
+ // This is asynchronous, so we can't add to media store now because thumbnail
+ // may not be ready. In such case saveVideo() is called later
+ // through a callback from the MediaEncoderFilter to EffectsRecorder,
+ // and then to the VideoModule.
+ mEffectsRecorder.stopRecording();
+ } else {
+ mMediaRecorder.setOnErrorListener(null);
+ mMediaRecorder.setOnInfoListener(null);
+ mMediaRecorder.stop();
+ shouldAddToMediaStoreNow = true;
+ }
+ mCurrentVideoFilename = mVideoFilename;
+ Log.v(TAG, "stopVideoRecording: Setting current video filename: "
+ + mCurrentVideoFilename);
+ AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(),
+ mActivity.getString(R.string.video_recording_stopped));
+ } catch (RuntimeException e) {
+ Log.e(TAG, "stop fail", e);
+ if (mVideoFilename != null) deleteVideoFile(mVideoFilename);
+ fail = true;
+ }
+ mMediaRecorderRecording = false;
+ mOrientationManager.unlockOrientation();
+
+ // If the activity is paused, this means activity is interrupted
+ // during recording. Release the camera as soon as possible because
+ // face unlock or other applications may need to use the camera.
+ // However, if the effects are active, then we can only release the
+ // camera and cannot release the effects recorder since that will
+ // stop the graph. It is possible to separate out the Camera release
+ // part and the effects release part. However, the effects recorder
+ // does hold on to the camera, hence, it needs to be "disconnected"
+ // from the camera in the closeCamera call.
+ if (mPaused) {
+ // Closing only the camera part if effects active. Effects will
+ // be closed in the callback from effects.
+ boolean closeEffects = !effectsActive();
+ closeCamera(closeEffects);
+ }
+
+ mUI.showRecordingUI(false, mParameters.isZoomSupported());
+ if (!mIsVideoCaptureIntent) {
+ mUI.enableCameraControls(true);
+ }
+ // The orientation was fixed during video recording. Now make it
+ // reflect the device orientation as video recording is stopped.
+ mUI.setOrientationIndicator(0, true);
+ keepScreenOnAwhile();
+ if (shouldAddToMediaStoreNow) {
+ saveVideo();
+ }
+ }
+ // always release media recorder if no effects running
+ if (!effectsActive()) {
+ releaseMediaRecorder();
+ if (!mPaused) {
+ mCameraDevice.lock();
+ if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
+ stopPreview();
+ mUI.hideSurfaceView();
+ // Switch back to use SurfaceTexture for preview.
+ startPreview();
+ }
+ }
+ }
+ // Update the parameters here because the parameters might have been altered
+ // by MediaRecorder.
+ if (!mPaused) mParameters = mCameraDevice.getParameters();
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
+ fail ? UsageStatistics.ACTION_CAPTURE_FAIL :
+ UsageStatistics.ACTION_CAPTURE_DONE, "Video",
+ SystemClock.uptimeMillis() - mRecordingStartTime);
+ return fail;
+ }
+
+ private void resetScreenOn() {
+ mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+ mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ private void keepScreenOnAwhile() {
+ mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+ }
+
+ private void keepScreenOn() {
+ mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+ mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ private static String millisecondToTimeString(long milliSeconds, boolean displayCentiSeconds) {
+ long seconds = milliSeconds / 1000; // round down to compute seconds
+ long minutes = seconds / 60;
+ long hours = minutes / 60;
+ long remainderMinutes = minutes - (hours * 60);
+ long remainderSeconds = seconds - (minutes * 60);
+
+ StringBuilder timeStringBuilder = new StringBuilder();
+
+ // Hours
+ if (hours > 0) {
+ if (hours < 10) {
+ timeStringBuilder.append('0');
+ }
+ timeStringBuilder.append(hours);
+
+ timeStringBuilder.append(':');
+ }
+
+ // Minutes
+ if (remainderMinutes < 10) {
+ timeStringBuilder.append('0');
+ }
+ timeStringBuilder.append(remainderMinutes);
+ timeStringBuilder.append(':');
+
+ // Seconds
+ if (remainderSeconds < 10) {
+ timeStringBuilder.append('0');
+ }
+ timeStringBuilder.append(remainderSeconds);
+
+ // Centi seconds
+ if (displayCentiSeconds) {
+ timeStringBuilder.append('.');
+ long remainderCentiSeconds = (milliSeconds - seconds * 1000) / 10;
+ if (remainderCentiSeconds < 10) {
+ timeStringBuilder.append('0');
+ }
+ timeStringBuilder.append(remainderCentiSeconds);
+ }
+
+ return timeStringBuilder.toString();
+ }
+
+ private long getTimeLapseVideoLength(long deltaMs) {
+ // For better approximation calculate fractional number of frames captured.
+ // This will update the video time at a higher resolution.
+ double numberOfFrames = (double) deltaMs / mTimeBetweenTimeLapseFrameCaptureMs;
+ return (long) (numberOfFrames / mProfile.videoFrameRate * 1000);
+ }
+
+ private void updateRecordingTime() {
+ if (!mMediaRecorderRecording) {
+ return;
+ }
+ long now = SystemClock.uptimeMillis();
+ long delta = now - mRecordingStartTime;
+
+ // Starting a minute before reaching the max duration
+ // limit, we'll countdown the remaining time instead.
+ boolean countdownRemainingTime = (mMaxVideoDurationInMs != 0
+ && delta >= mMaxVideoDurationInMs - 60000);
+
+ long deltaAdjusted = delta;
+ if (countdownRemainingTime) {
+ deltaAdjusted = Math.max(0, mMaxVideoDurationInMs - deltaAdjusted) + 999;
+ }
+ String text;
+
+ long targetNextUpdateDelay;
+ if (!mCaptureTimeLapse) {
+ text = millisecondToTimeString(deltaAdjusted, false);
+ targetNextUpdateDelay = 1000;
+ } else {
+ // The length of time lapse video is different from the length
+ // of the actual wall clock time elapsed. Display the video length
+ // only in format hh:mm:ss.dd, where dd are the centi seconds.
+ text = millisecondToTimeString(getTimeLapseVideoLength(delta), true);
+ targetNextUpdateDelay = mTimeBetweenTimeLapseFrameCaptureMs;
+ }
+
+ mUI.setRecordingTime(text);
+
+ if (mRecordingTimeCountsDown != countdownRemainingTime) {
+ // Avoid setting the color on every update, do it only
+ // when it needs changing.
+ mRecordingTimeCountsDown = countdownRemainingTime;
+
+ int color = mActivity.getResources().getColor(countdownRemainingTime
+ ? R.color.recording_time_remaining_text
+ : R.color.recording_time_elapsed_text);
+
+ mUI.setRecordingTimeTextColor(color);
+ }
+
+ long actualNextUpdateDelay = targetNextUpdateDelay - (delta % targetNextUpdateDelay);
+ mHandler.sendEmptyMessageDelayed(
+ UPDATE_RECORD_TIME, actualNextUpdateDelay);
+ }
+
+ private static boolean isSupported(String value, List<String> supported) {
+ return supported == null ? false : supported.indexOf(value) >= 0;
+ }
+
+ @SuppressWarnings("deprecation")
+ private void setCameraParameters() {
+ mParameters.setPreviewSize(mDesiredPreviewWidth, mDesiredPreviewHeight);
+ int[] fpsRange = Util.getMaxPreviewFpsRange(mParameters);
+ if (fpsRange.length > 0) {
+ mParameters.setPreviewFpsRange(
+ fpsRange[Parameters.PREVIEW_FPS_MIN_INDEX],
+ fpsRange[Parameters.PREVIEW_FPS_MAX_INDEX]);
+ } else {
+ mParameters.setPreviewFrameRate(mProfile.videoFrameRate);
+ }
+
+ // Set flash mode.
+ String flashMode;
+ if (mUI.isVisible()) {
+ flashMode = mPreferences.getString(
+ CameraSettings.KEY_VIDEOCAMERA_FLASH_MODE,
+ mActivity.getString(R.string.pref_camera_video_flashmode_default));
+ } else {
+ flashMode = Parameters.FLASH_MODE_OFF;
+ }
+ List<String> supportedFlash = mParameters.getSupportedFlashModes();
+ if (isSupported(flashMode, supportedFlash)) {
+ mParameters.setFlashMode(flashMode);
+ } else {
+ flashMode = mParameters.getFlashMode();
+ if (flashMode == null) {
+ flashMode = mActivity.getString(
+ R.string.pref_camera_flashmode_no_flash);
+ }
+ }
+
+ // Set white balance parameter.
+ String whiteBalance = mPreferences.getString(
+ CameraSettings.KEY_WHITE_BALANCE,
+ mActivity.getString(R.string.pref_camera_whitebalance_default));
+ if (isSupported(whiteBalance,
+ mParameters.getSupportedWhiteBalance())) {
+ mParameters.setWhiteBalance(whiteBalance);
+ } else {
+ whiteBalance = mParameters.getWhiteBalance();
+ if (whiteBalance == null) {
+ whiteBalance = Parameters.WHITE_BALANCE_AUTO;
+ }
+ }
+
+ // Set zoom.
+ if (mParameters.isZoomSupported()) {
+ mParameters.setZoom(mZoomValue);
+ }
+
+ // Set continuous autofocus.
+ List<String> supportedFocus = mParameters.getSupportedFocusModes();
+ if (isSupported(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, supportedFocus)) {
+ mParameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
+ }
+
+ mParameters.set(Util.RECORDING_HINT, Util.TRUE);
+
+ // Enable video stabilization. Convenience methods not available in API
+ // level <= 14
+ String vstabSupported = mParameters.get("video-stabilization-supported");
+ if ("true".equals(vstabSupported)) {
+ mParameters.set("video-stabilization", "true");
+ }
+
+ // Set picture size.
+ // The logic here is different from the logic in still-mode camera.
+ // There we determine the preview size based on the picture size, but
+ // here we determine the picture size based on the preview size.
+ List<Size> supported = mParameters.getSupportedPictureSizes();
+ Size optimalSize = Util.getOptimalVideoSnapshotPictureSize(supported,
+ (double) mDesiredPreviewWidth / mDesiredPreviewHeight);
+ Size original = mParameters.getPictureSize();
+ if (!original.equals(optimalSize)) {
+ mParameters.setPictureSize(optimalSize.width, optimalSize.height);
+ }
+ Log.v(TAG, "Video snapshot size is " + optimalSize.width + "x" +
+ optimalSize.height);
+
+ // Set JPEG quality.
+ int jpegQuality = CameraProfile.getJpegEncodingQualityParameter(mCameraId,
+ CameraProfile.QUALITY_HIGH);
+ mParameters.setJpegQuality(jpegQuality);
+
+ mCameraDevice.setParameters(mParameters);
+ // Keep preview size up to date.
+ mParameters = mCameraDevice.getParameters();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_EFFECT_BACKDROPPER:
+ if (resultCode == Activity.RESULT_OK) {
+ // onActivityResult() runs before onResume(), so this parameter will be
+ // seen by startPreview from onResume()
+ mEffectUriFromGallery = data.getData().toString();
+ Log.v(TAG, "Received URI from gallery: " + mEffectUriFromGallery);
+ mResetEffect = false;
+ } else {
+ mEffectUriFromGallery = null;
+ Log.w(TAG, "No URI from gallery");
+ mResetEffect = true;
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void onEffectsUpdate(int effectId, int effectMsg) {
+ Log.v(TAG, "onEffectsUpdate. Effect Message = " + effectMsg);
+ if (effectMsg == EffectsRecorder.EFFECT_MSG_EFFECTS_STOPPED) {
+ // Effects have shut down. Hide learning message if any,
+ // and restart regular preview.
+ checkQualityAndStartPreview();
+ } else if (effectMsg == EffectsRecorder.EFFECT_MSG_RECORDING_DONE) {
+ // This follows the codepath from onStopVideoRecording.
+ if (mEffectsDisplayResult) {
+ saveVideo();
+ if (mIsVideoCaptureIntent) {
+ if (mQuickCapture) {
+ doReturnToCaller(true);
+ } else {
+ showCaptureResult();
+ }
+ }
+ }
+ mEffectsDisplayResult = false;
+ // In onPause, these were not called if the effects were active. We
+ // had to wait till the effects recording is complete to do this.
+ if (mPaused) {
+ closeVideoFileDescriptor();
+ }
+ } else if (effectMsg == EffectsRecorder.EFFECT_MSG_PREVIEW_RUNNING) {
+ // Enable the shutter button once the preview is complete.
+ mUI.enableShutter(true);
+ }
+ // In onPause, this was not called if the effects were active. We had to
+ // wait till the effects completed to do this.
+ if (mPaused) {
+ Log.v(TAG, "OnEffectsUpdate: closing effects if activity paused");
+ closeEffects();
+ }
+ }
+
+ public void onCancelBgTraining(View v) {
+ // Write default effect out to shared prefs
+ writeDefaultEffectToPrefs();
+ // Tell VideoCamer to re-init based on new shared pref values.
+ onSharedPreferenceChanged();
+ }
+
+ @Override
+ public synchronized void onEffectsError(Exception exception, String fileName) {
+ // TODO: Eventually we may want to show the user an error dialog, and then restart the
+ // camera and encoder gracefully. For now, we just delete the file and bail out.
+ if (fileName != null && new File(fileName).exists()) {
+ deleteVideoFile(fileName);
+ }
+ try {
+ if (Class.forName("android.filterpacks.videosink.MediaRecorderStopException")
+ .isInstance(exception)) {
+ Log.w(TAG, "Problem recoding video file. Removing incomplete file.");
+ return;
+ }
+ } catch (ClassNotFoundException ex) {
+ Log.w(TAG, ex);
+ }
+ throw new RuntimeException("Error during recording!", exception);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ Log.v(TAG, "onConfigurationChanged");
+ setDisplayOrientation();
+ }
+
+ @Override
+ public void onOverriddenPreferencesClicked() {
+ }
+
+ @Override
+ // TODO: Delete this after old camera code is removed
+ public void onRestorePreferencesClicked() {
+ }
+
+ private boolean effectsActive() {
+ return (mEffectType != EffectsRecorder.EFFECT_NONE);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged() {
+ // ignore the events after "onPause()" or preview has not started yet
+ if (mPaused) return;
+ synchronized (mPreferences) {
+ // If mCameraDevice is not ready then we can set the parameter in
+ // startPreview().
+ if (mCameraDevice == null) return;
+
+ boolean recordLocation = RecordLocationPreference.get(
+ mPreferences, mContentResolver);
+ mLocationManager.recordLocation(recordLocation);
+
+ // Check if the current effects selection has changed
+ if (updateEffectSelection()) return;
+
+ readVideoPreferences();
+ mUI.showTimeLapseUI(mCaptureTimeLapse);
+ // We need to restart the preview if preview size is changed.
+ Size size = mParameters.getPreviewSize();
+ if (size.width != mDesiredPreviewWidth
+ || size.height != mDesiredPreviewHeight) {
+ if (!effectsActive()) {
+ stopPreview();
+ } else {
+ mEffectsRecorder.release();
+ mEffectsRecorder = null;
+ }
+ resizeForPreviewAspectRatio();
+ startPreview(); // Parameters will be set in startPreview().
+ } else {
+ setCameraParameters();
+ }
+ mUI.updateOnScreenIndicators(mParameters, mPreferences);
+ }
+ }
+
+ protected void setCameraId(int cameraId) {
+ ListPreference pref = mPreferenceGroup.findPreference(CameraSettings.KEY_CAMERA_ID);
+ pref.setValue("" + cameraId);
+ }
+
+ private void switchCamera() {
+ if (mPaused) return;
+
+ Log.d(TAG, "Start to switch camera.");
+ mCameraId = mPendingSwitchCameraId;
+ mPendingSwitchCameraId = -1;
+ setCameraId(mCameraId);
+
+ closeCamera();
+ mUI.collapseCameraControls();
+ // Restart the camera and initialize the UI. From onCreate.
+ mPreferences.setLocalId(mActivity, mCameraId);
+ CameraSettings.upgradeLocalPreferences(mPreferences.getLocal());
+ openCamera();
+ readVideoPreferences();
+ startPreview();
+ initializeVideoSnapshot();
+ resizeForPreviewAspectRatio();
+ initializeVideoControl();
+
+ // From onResume
+ mZoomValue = 0;
+ mUI.initializeZoom(mParameters);
+ mUI.setOrientationIndicator(0, false);
+
+ // Start switch camera animation. Post a message because
+ // onFrameAvailable from the old camera may already exist.
+ mHandler.sendEmptyMessage(SWITCH_CAMERA_START_ANIMATION);
+ mUI.updateOnScreenIndicators(mParameters, mPreferences);
+ }
+
+ // Preview texture has been copied. Now camera can be released and the
+ // animation can be started.
+ @Override
+ public void onPreviewTextureCopied() {
+ mHandler.sendEmptyMessage(SWITCH_CAMERA);
+ }
+
+ @Override
+ public void onCaptureTextureCopied() {
+ }
+
+ private boolean updateEffectSelection() {
+ int previousEffectType = mEffectType;
+ Object previousEffectParameter = mEffectParameter;
+ mEffectType = CameraSettings.readEffectType(mPreferences);
+ mEffectParameter = CameraSettings.readEffectParameter(mPreferences);
+
+ if (mEffectType == previousEffectType) {
+ if (mEffectType == EffectsRecorder.EFFECT_NONE) return false;
+ if (mEffectParameter.equals(previousEffectParameter)) return false;
+ }
+ Log.v(TAG, "New effect selection: " + mPreferences.getString(
+ CameraSettings.KEY_VIDEO_EFFECT, "none"));
+
+ if (mEffectType == EffectsRecorder.EFFECT_NONE) {
+ // Stop effects and return to normal preview
+ mEffectsRecorder.stopPreview();
+ mPreviewing = false;
+ return true;
+ }
+ if (mEffectType == EffectsRecorder.EFFECT_BACKDROPPER &&
+ ((String) mEffectParameter).equals(EFFECT_BG_FROM_GALLERY)) {
+ // Request video from gallery to use for background
+ Intent i = new Intent(Intent.ACTION_PICK);
+ i.setDataAndType(Video.Media.EXTERNAL_CONTENT_URI,
+ "video/*");
+ i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
+ mActivity.startActivityForResult(i, REQUEST_EFFECT_BACKDROPPER);
+ return true;
+ }
+ if (previousEffectType == EffectsRecorder.EFFECT_NONE) {
+ // Stop regular preview and start effects.
+ stopPreview();
+ checkQualityAndStartPreview();
+ } else {
+ // Switch currently running effect
+ mEffectsRecorder.setEffect(mEffectType, mEffectParameter);
+ }
+ return true;
+ }
+
+ // Verifies that the current preview view size is correct before starting
+ // preview. If not, resets the surface texture and resizes the view.
+ private void checkQualityAndStartPreview() {
+ readVideoPreferences();
+ mUI.showTimeLapseUI(mCaptureTimeLapse);
+ Size size = mParameters.getPreviewSize();
+ if (size.width != mDesiredPreviewWidth
+ || size.height != mDesiredPreviewHeight) {
+ resizeForPreviewAspectRatio();
+ }
+ // Start up preview again
+ startPreview();
+ }
+
+ private void initializeVideoSnapshot() {
+ if (mParameters == null) return;
+ if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) {
+ // Show the tap to focus toast if this is the first start.
+ if (mPreferences.getBoolean(
+ CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, true)) {
+ // Delay the toast for one second to wait for orientation.
+ mHandler.sendEmptyMessageDelayed(SHOW_TAP_TO_SNAPSHOT_TOAST, 1000);
+ }
+ }
+ }
+
+ void showVideoSnapshotUI(boolean enabled) {
+ if (mParameters == null) return;
+ if (Util.isVideoSnapshotSupported(mParameters) && !mIsVideoCaptureIntent) {
+ if (enabled) {
+ // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).animateCapture(mDisplayRotation);
+ } else {
+ mUI.showPreviewBorder(enabled);
+ }
+ mUI.enableShutter(!enabled);
+ }
+ }
+
+ @Override
+ public void updateCameraAppView() {
+ if (!mPreviewing || mParameters.getFlashMode() == null) return;
+
+ // When going to and back from gallery, we need to turn off/on the flash.
+ if (!mUI.isVisible()) {
+ if (mParameters.getFlashMode().equals(Parameters.FLASH_MODE_OFF)) {
+ mRestoreFlash = false;
+ return;
+ }
+ mRestoreFlash = true;
+ setCameraParameters();
+ } else if (mRestoreFlash) {
+ mRestoreFlash = false;
+ setCameraParameters();
+ }
+ }
+
+ @Override
+ public void onSwitchMode(boolean toCamera) {
+ mUI.onSwitchMode(toCamera);
+ }
+
+ private final class JpegPictureCallback implements CameraPictureCallback {
+ Location mLocation;
+
+ public JpegPictureCallback(Location loc) {
+ mLocation = loc;
+ }
+
+ @Override
+ public void onPictureTaken(byte [] jpegData, CameraProxy camera) {
+ Log.v(TAG, "onPictureTaken");
+ mSnapshotInProgress = false;
+ showVideoSnapshotUI(false);
+ storeImage(jpegData, mLocation);
+ }
+ }
+
+ private void storeImage(final byte[] data, Location loc) {
+ long dateTaken = System.currentTimeMillis();
+ String title = Util.createJpegName(dateTaken);
+ ExifInterface exif = Exif.getExif(data);
+ int orientation = Exif.getOrientation(exif);
+ Size s = mParameters.getPictureSize();
+ mActivity.getMediaSaveService().addImage(
+ data, title, dateTaken, loc, s.width, s.height, orientation,
+ exif, mOnPhotoSavedListener, mContentResolver);
+ }
+
+ private boolean resetEffect() {
+ if (mResetEffect) {
+ String value = mPreferences.getString(CameraSettings.KEY_VIDEO_EFFECT,
+ mPrefVideoEffectDefault);
+ if (!mPrefVideoEffectDefault.equals(value)) {
+ writeDefaultEffectToPrefs();
+ return true;
+ }
+ }
+ mResetEffect = true;
+ return false;
+ }
+
+ private String convertOutputFormatToMimeType(int outputFileFormat) {
+ if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
+ return "video/mp4";
+ }
+ return "video/3gpp";
+ }
+
+ private String convertOutputFormatToFileExt(int outputFileFormat) {
+ if (outputFileFormat == MediaRecorder.OutputFormat.MPEG_4) {
+ return ".mp4";
+ }
+ return ".3gp";
+ }
+
+ private void closeVideoFileDescriptor() {
+ if (mVideoFileDescriptor != null) {
+ try {
+ mVideoFileDescriptor.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Fail to close fd", e);
+ }
+ mVideoFileDescriptor = null;
+ }
+ }
+
+ private void showTapToSnapshotToast() {
+ new RotateTextToast(mActivity, R.string.video_snapshot_hint, 0)
+ .show();
+ // Clear the preference.
+ Editor editor = mPreferences.edit();
+ editor.putBoolean(CameraSettings.KEY_VIDEO_FIRST_USE_HINT_SHOWN, false);
+ editor.apply();
+ }
+
+ @Override
+ public boolean updateStorageHintOnResume() {
+ return true;
+ }
+
+ // required by OnPreferenceChangedListener
+ @Override
+ public void onCameraPickerClicked(int cameraId) {
+ if (mPaused || mPendingSwitchCameraId != -1) return;
+
+ mPendingSwitchCameraId = cameraId;
+ Log.d(TAG, "Start to copy texture.");
+ // We need to keep a preview frame for the animation before
+ // releasing the camera. This will trigger onPreviewTextureCopied.
+ // TODO: ((CameraScreenNail) mActivity.mCameraScreenNail).copyTexture();
+ // Disable all camera controls.
+ mSwitchingCamera = true;
+
+ }
+
+ @Override
+ public void onShowSwitcherPopup() {
+ mUI.onShowSwitcherPopup();
+ }
+
+ @Override
+ public void onMediaSaveServiceConnected(MediaSaveService s) {
+ // do nothing.
+ }
+
+ @Override
+ public void onPreviewUIReady() {
+ startPreview();
+ }
+
+ @Override
+ public void onPreviewUIDestroyed() {
+ stopPreview();
+ }
+}
diff --git a/src/com/android/camera/VideoUI.java b/src/com/android/camera/VideoUI.java
new file mode 100644
index 000000000..551b72596
--- /dev/null
+++ b/src/com/android/camera/VideoUI.java
@@ -0,0 +1,698 @@
+/*
+ * Copyright (C) 2012 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.camera;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera.Parameters;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.TextureView.SurfaceTextureListener;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.camera.CameraPreference.OnPreferenceChangedListener;
+import com.android.camera.ui.AbstractSettingPopup;
+import com.android.camera.ui.CameraControls;
+import com.android.camera.ui.CameraRootView;
+import com.android.camera.ui.CameraSwitcher;
+import com.android.camera.ui.CameraSwitcher.CameraSwitchListener;
+import com.android.camera.ui.PieRenderer;
+import com.android.camera.ui.RenderOverlay;
+import com.android.camera.ui.RotateLayout;
+import com.android.camera.ui.ZoomRenderer;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.List;
+
+public class VideoUI implements PieRenderer.PieListener,
+ PreviewGestures.SingleTapListener,
+ CameraRootView.MyDisplayListener,
+ SurfaceTextureListener, SurfaceHolder.Callback {
+ private static final String TAG = "CAM_VideoUI";
+ private static final int UPDATE_TRANSFORM_MATRIX = 1;
+ // module fields
+ private CameraActivity mActivity;
+ private View mRootView;
+ private TextureView mTextureView;
+ // An review image having same size as preview. It is displayed when
+ // recording is stopped in capture intent.
+ private ImageView mReviewImage;
+ private View mReviewCancelButton;
+ private View mReviewDoneButton;
+ private View mReviewPlayButton;
+ private ShutterButton mShutterButton;
+ private CameraSwitcher mSwitcher;
+ private TextView mRecordingTimeView;
+ private LinearLayout mLabelsLinearLayout;
+ private View mTimeLapseLabel;
+ private RenderOverlay mRenderOverlay;
+ private PieRenderer mPieRenderer;
+ private VideoMenu mVideoMenu;
+ private CameraControls mCameraControls;
+ private AbstractSettingPopup mPopup;
+ private ZoomRenderer mZoomRenderer;
+ private PreviewGestures mGestures;
+ private View mMenuButton;
+ private View mBlocker;
+ private OnScreenIndicators mOnScreenIndicators;
+ private RotateLayout mRecordingTimeRect;
+ private final Object mLock = new Object();
+ private SurfaceTexture mSurfaceTexture;
+ private VideoController mController;
+ private int mZoomMax;
+ private List<Integer> mZoomRatios;
+ private View mPreviewThumb;
+
+ private SurfaceView mSurfaceView = null;
+ private int mPreviewWidth = 0;
+ private int mPreviewHeight = 0;
+ private float mSurfaceTextureUncroppedWidth;
+ private float mSurfaceTextureUncroppedHeight;
+ private float mAspectRatio = 4f / 3f;
+ private Matrix mMatrix = null;
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case UPDATE_TRANSFORM_MATRIX:
+ setTransformMatrix(mPreviewWidth, mPreviewHeight);
+ break;
+ default:
+ break;
+ }
+ }
+ };
+ private OnLayoutChangeListener mLayoutListener = new OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right,
+ int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ int width = right - left;
+ int height = bottom - top;
+ // Full-screen screennail
+ int w = width;
+ int h = height;
+ if (Util.getDisplayRotation(mActivity) % 180 != 0) {
+ w = height;
+ h = width;
+ }
+ if (mPreviewWidth != width || mPreviewHeight != height) {
+ mPreviewWidth = width;
+ mPreviewHeight = height;
+ onScreenSizeChanged(width, height, w, h);
+ }
+ }
+ };
+
+ public VideoUI(CameraActivity activity, VideoController controller, View parent) {
+ mActivity = activity;
+ mController = controller;
+ mRootView = parent;
+ mActivity.getLayoutInflater().inflate(R.layout.video_module, (ViewGroup) mRootView, true);
+ mTextureView = (TextureView) mRootView.findViewById(R.id.preview_content);
+ mTextureView.setSurfaceTextureListener(this);
+ mRootView.addOnLayoutChangeListener(mLayoutListener);
+ ((CameraRootView) mRootView).setDisplayChangeListener(this);
+ mShutterButton = (ShutterButton) mRootView.findViewById(R.id.shutter_button);
+ mSwitcher = (CameraSwitcher) mRootView.findViewById(R.id.camera_switcher);
+ mSwitcher.setCurrentIndex(CameraSwitcher.VIDEO_MODULE_INDEX);
+ mSwitcher.setSwitchListener((CameraSwitchListener) mActivity);
+ initializeMiscControls();
+ initializeControlByIntent();
+ initializeOverlay();
+ }
+
+
+ public void initializeSurfaceView() {
+ mSurfaceView = new SurfaceView(mActivity);
+ ((ViewGroup) mRootView).addView(mSurfaceView, 0);
+ mSurfaceView.getHolder().addCallback(this);
+ }
+
+ private void initializeControlByIntent() {
+ mBlocker = mActivity.findViewById(R.id.blocker);
+ mMenuButton = mActivity.findViewById(R.id.menu);
+ mMenuButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mPieRenderer != null) {
+ mPieRenderer.showInCenter();
+ }
+ }
+ });
+
+ mCameraControls = (CameraControls) mActivity.findViewById(R.id.camera_controls);
+ mOnScreenIndicators = new OnScreenIndicators(mActivity,
+ mActivity.findViewById(R.id.on_screen_indicators));
+ mOnScreenIndicators.resetToDefault();
+ if (mController.isVideoCaptureIntent()) {
+ hideSwitcher();
+ mActivity.getLayoutInflater().inflate(R.layout.review_module_control,
+ (ViewGroup) mCameraControls);
+ // Cannot use RotateImageView for "done" and "cancel" button because
+ // the tablet layout uses RotateLayout, which cannot be cast to
+ // RotateImageView.
+ mReviewDoneButton = mActivity.findViewById(R.id.btn_done);
+ mReviewCancelButton = mActivity.findViewById(R.id.btn_cancel);
+ mReviewPlayButton = mActivity.findViewById(R.id.btn_play);
+ mReviewCancelButton.setVisibility(View.VISIBLE);
+ mReviewDoneButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mController.onReviewDoneClicked(v);
+ }
+ });
+ mReviewCancelButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mController.onReviewCancelClicked(v);
+ }
+ });
+ mReviewPlayButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mController.onReviewPlayClicked(v);
+ }
+ });
+ }
+ }
+
+ public void setPreviewSize(int width, int height) {
+ if (width == 0 || height == 0) {
+ Log.w(TAG, "Preview size should not be 0.");
+ return;
+ }
+ if (width > height) {
+ mAspectRatio = (float) width / height;
+ } else {
+ mAspectRatio = (float) height / width;
+ }
+ mHandler.sendEmptyMessage(UPDATE_TRANSFORM_MATRIX);
+ }
+
+ public int getPreviewWidth() {
+ return mPreviewWidth;
+ }
+
+ public int getPreviewHeight() {
+ return mPreviewHeight;
+ }
+
+ public void onScreenSizeChanged(int width, int height, int previewWidth, int previewHeight) {
+ setTransformMatrix(width, height);
+ }
+
+ private void setTransformMatrix(int width, int height) {
+ mMatrix = mTextureView.getTransform(mMatrix);
+ int orientation = Util.getDisplayRotation(mActivity);
+ float scaleX = 1f, scaleY = 1f;
+ float scaledTextureWidth, scaledTextureHeight;
+ if (width > height) {
+ scaledTextureWidth = Math.max(width,
+ (int) (height * mAspectRatio));
+ scaledTextureHeight = Math.max(height,
+ (int)(width / mAspectRatio));
+ } else {
+ scaledTextureWidth = Math.max(width,
+ (int) (height / mAspectRatio));
+ scaledTextureHeight = Math.max(height,
+ (int) (width * mAspectRatio));
+ }
+
+ if (mSurfaceTextureUncroppedWidth != scaledTextureWidth ||
+ mSurfaceTextureUncroppedHeight != scaledTextureHeight) {
+ mSurfaceTextureUncroppedWidth = scaledTextureWidth;
+ mSurfaceTextureUncroppedHeight = scaledTextureHeight;
+ }
+ scaleX = scaledTextureWidth / width;
+ scaleY = scaledTextureHeight / height;
+ mMatrix.setScale(scaleX, scaleY, (float) width / 2, (float) height / 2);
+ mTextureView.setTransform(mMatrix);
+
+ if (mSurfaceView != null && mSurfaceView.getVisibility() == View.VISIBLE) {
+ LayoutParams lp = (LayoutParams) mSurfaceView.getLayoutParams();
+ lp.width = (int) mSurfaceTextureUncroppedWidth;
+ lp.height = (int) mSurfaceTextureUncroppedHeight;
+ lp.gravity = Gravity.CENTER;
+ mSurfaceView.requestLayout();
+ }
+ }
+
+ public void hideUI() {
+ mCameraControls.setVisibility(View.INVISIBLE);
+ mSwitcher.closePopup();
+ }
+
+ public void showUI() {
+ mCameraControls.setVisibility(View.VISIBLE);
+ }
+
+ public void hideSwitcher() {
+ mSwitcher.closePopup();
+ mSwitcher.setVisibility(View.INVISIBLE);
+ }
+
+ public void showSwitcher() {
+ mSwitcher.setVisibility(View.VISIBLE);
+ }
+
+ public boolean collapseCameraControls() {
+ boolean ret = false;
+ if (mPopup != null) {
+ dismissPopup(false);
+ ret = true;
+ }
+ return ret;
+ }
+
+ public boolean removeTopLevelPopup() {
+ if (mPopup != null) {
+ dismissPopup(true);
+ return true;
+ }
+ return false;
+ }
+
+ public void enableCameraControls(boolean enable) {
+ if (mGestures != null) {
+ mGestures.setZoomOnly(!enable);
+ }
+ if (mPieRenderer != null && mPieRenderer.showsItems()) {
+ mPieRenderer.hide();
+ }
+ }
+
+ public void overrideSettings(final String... keyvalues) {
+ mVideoMenu.overrideSettings(keyvalues);
+ }
+
+ public void setOrientationIndicator(int orientation, boolean animation) {
+ // We change the orientation of the linearlayout only for phone UI
+ // because when in portrait the width is not enough.
+ if (mLabelsLinearLayout != null) {
+ if (((orientation / 90) & 1) == 0) {
+ mLabelsLinearLayout.setOrientation(LinearLayout.VERTICAL);
+ } else {
+ mLabelsLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
+ }
+ }
+ mRecordingTimeRect.setOrientation(0, animation);
+ }
+
+ public SurfaceHolder getSurfaceHolder() {
+ return mSurfaceView.getHolder();
+ }
+
+ public void hideSurfaceView() {
+ mSurfaceView.setVisibility(View.GONE);
+ mTextureView.setVisibility(View.VISIBLE);
+ setTransformMatrix(mPreviewWidth, mPreviewHeight);
+ }
+
+ public void showSurfaceView() {
+ mSurfaceView.setVisibility(View.VISIBLE);
+ mTextureView.setVisibility(View.GONE);
+ setTransformMatrix(mPreviewWidth, mPreviewHeight);
+ }
+
+ private void initializeOverlay() {
+ mRenderOverlay = (RenderOverlay) mRootView.findViewById(R.id.render_overlay);
+ if (mPieRenderer == null) {
+ mPieRenderer = new PieRenderer(mActivity);
+ mVideoMenu = new VideoMenu(mActivity, this, mPieRenderer);
+ mPieRenderer.setPieListener(this);
+ }
+ mRenderOverlay.addRenderer(mPieRenderer);
+ if (mZoomRenderer == null) {
+ mZoomRenderer = new ZoomRenderer(mActivity);
+ }
+ mRenderOverlay.addRenderer(mZoomRenderer);
+ if (mGestures == null) {
+ mGestures = new PreviewGestures(mActivity, this, mZoomRenderer, mPieRenderer);
+ mRenderOverlay.setGestures(mGestures);
+ }
+ mGestures.setRenderOverlay(mRenderOverlay);
+
+ mPreviewThumb = mActivity.findViewById(R.id.preview_thumb);
+ mPreviewThumb.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // TODO: Go to filmstrip view
+ }
+ });
+ }
+
+ public void setPrefChangedListener(OnPreferenceChangedListener listener) {
+ mVideoMenu.setListener(listener);
+ }
+
+ private void initializeMiscControls() {
+ mReviewImage = (ImageView) mRootView.findViewById(R.id.review_image);
+ mShutterButton.setImageResource(R.drawable.btn_new_shutter_video);
+ mShutterButton.setOnShutterButtonListener(mController);
+ mShutterButton.setVisibility(View.VISIBLE);
+ mShutterButton.requestFocus();
+ mShutterButton.enableTouch(true);
+ mRecordingTimeView = (TextView) mRootView.findViewById(R.id.recording_time);
+ mRecordingTimeRect = (RotateLayout) mRootView.findViewById(R.id.recording_time_rect);
+ mTimeLapseLabel = mRootView.findViewById(R.id.time_lapse_label);
+ // The R.id.labels can only be found in phone layout.
+ // That is, mLabelsLinearLayout should be null in tablet layout.
+ mLabelsLinearLayout = (LinearLayout) mRootView.findViewById(R.id.labels);
+ }
+
+ public void updateOnScreenIndicators(Parameters param, ComboPreferences prefs) {
+ mOnScreenIndicators.updateFlashOnScreenIndicator(param.getFlashMode());
+ boolean location = RecordLocationPreference.get(
+ prefs, mActivity.getContentResolver());
+ mOnScreenIndicators.updateLocationIndicator(location);
+
+ }
+
+ public void setAspectRatio(double ratio) {
+ // mPreviewFrameLayout.setAspectRatio(ratio);
+ }
+
+ public void showTimeLapseUI(boolean enable) {
+ if (mTimeLapseLabel != null) {
+ mTimeLapseLabel.setVisibility(enable ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ private void openMenu() {
+ if (mPieRenderer != null) {
+ mPieRenderer.showInCenter();
+ }
+ }
+
+ public void showPopup(AbstractSettingPopup popup) {
+ hideUI();
+ mBlocker.setVisibility(View.INVISIBLE);
+ setShowMenu(false);
+ mPopup = popup;
+ mPopup.setVisibility(View.VISIBLE);
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ lp.gravity = Gravity.CENTER;
+ ((FrameLayout) mRootView).addView(mPopup, lp);
+ }
+
+ public void dismissPopup(boolean topLevelOnly) {
+ dismissPopup(topLevelOnly, true);
+ }
+
+ public void dismissPopup(boolean topLevelPopupOnly, boolean fullScreen) {
+ // In review mode, we do not want to bring up the camera UI
+ if (mController.isInReviewMode()) return;
+
+ if (fullScreen) {
+ showUI();
+ mBlocker.setVisibility(View.VISIBLE);
+ }
+ setShowMenu(fullScreen);
+ if (mPopup != null) {
+ ((FrameLayout) mRootView).removeView(mPopup);
+ mPopup = null;
+ }
+ mVideoMenu.popupDismissed(topLevelPopupOnly);
+ }
+
+ public void onShowSwitcherPopup() {
+ hidePieRenderer();
+ }
+
+ public boolean hidePieRenderer() {
+ if (mPieRenderer != null && mPieRenderer.showsItems()) {
+ mPieRenderer.hide();
+ return true;
+ }
+ return false;
+ }
+
+ // disable preview gestures after shutter is pressed
+ public void setShutterPressed(boolean pressed) {
+ if (mGestures == null) return;
+ mGestures.setEnabled(!pressed);
+ }
+
+ public void enableShutter(boolean enable) {
+ if (mShutterButton != null) {
+ mShutterButton.setEnabled(enable);
+ }
+ }
+
+ // PieListener
+ @Override
+ public void onPieOpened(int centerX, int centerY) {
+ setSwipingEnabled(false);
+ dismissPopup(false, true);
+ }
+
+ @Override
+ public void onPieClosed() {
+ setSwipingEnabled(true);
+ }
+
+ public void setSwipingEnabled(boolean enable) {
+ mActivity.setSwipingEnabled(enable);
+ }
+
+ public void showPreviewBorder(boolean enable) {
+ // TODO: mPreviewFrameLayout.showBorder(enable);
+ }
+
+ // SingleTapListener
+ // Preview area is touched. Take a picture.
+ @Override
+ public void onSingleTapUp(View view, int x, int y) {
+ mController.onSingleTapUp(view, x, y);
+ }
+
+ public void showRecordingUI(boolean recording, boolean zoomSupported) {
+ mMenuButton.setVisibility(recording ? View.GONE : View.VISIBLE);
+ mOnScreenIndicators.setVisibility(recording ? View.GONE : View.VISIBLE);
+ if (recording) {
+ mShutterButton.setImageResource(R.drawable.btn_shutter_video_recording);
+ hideSwitcher();
+ mRecordingTimeView.setText("");
+ mRecordingTimeView.setVisibility(View.VISIBLE);
+ // The camera is not allowed to be accessed in older api levels during
+ // recording. It is therefore necessary to hide the zoom UI on older
+ // platforms.
+ // See the documentation of android.media.MediaRecorder.start() for
+ // further explanation.
+ if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING && zoomSupported) {
+ // TODO: disable zoom UI here.
+ }
+ } else {
+ mShutterButton.setImageResource(R.drawable.btn_new_shutter_video);
+ showSwitcher();
+ mRecordingTimeView.setVisibility(View.GONE);
+ if (!ApiHelper.HAS_ZOOM_WHEN_RECORDING && zoomSupported) {
+ // TODO: enable zoom UI here.
+ }
+ }
+ }
+
+ public void showReviewImage(Bitmap bitmap) {
+ mReviewImage.setImageBitmap(bitmap);
+ mReviewImage.setVisibility(View.VISIBLE);
+ }
+
+ public void showReviewControls() {
+ Util.fadeOut(mShutterButton);
+ Util.fadeIn(mReviewDoneButton);
+ Util.fadeIn(mReviewPlayButton);
+ mReviewImage.setVisibility(View.VISIBLE);
+ mMenuButton.setVisibility(View.GONE);
+ mOnScreenIndicators.setVisibility(View.GONE);
+ }
+
+ public void hideReviewUI() {
+ mReviewImage.setVisibility(View.GONE);
+ mShutterButton.setEnabled(true);
+ mMenuButton.setVisibility(View.VISIBLE);
+ mOnScreenIndicators.setVisibility(View.VISIBLE);
+ Util.fadeOut(mReviewDoneButton);
+ Util.fadeOut(mReviewPlayButton);
+ Util.fadeIn(mShutterButton);
+ }
+
+ private void setShowMenu(boolean show) {
+ if (mOnScreenIndicators != null) {
+ mOnScreenIndicators.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+ if (mMenuButton != null) {
+ mMenuButton.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ public void onSwitchMode(boolean toCamera) {
+ if (toCamera) {
+ showUI();
+ } else {
+ hideUI();
+ }
+ if (mGestures != null) {
+ mGestures.setEnabled(toCamera);
+ }
+ if (mPopup != null) {
+ dismissPopup(false, toCamera);
+ }
+ if (mRenderOverlay != null) {
+ // this can not happen in capture mode
+ mRenderOverlay.setVisibility(toCamera ? View.VISIBLE : View.GONE);
+ }
+ setShowMenu(toCamera);
+ }
+
+ public void initializePopup(PreferenceGroup pref) {
+ mVideoMenu.initialize(pref);
+ }
+
+ public void initializeZoom(Parameters param) {
+ if (param == null || !param.isZoomSupported()) {
+ mGestures.setZoomEnabled(false);
+ return;
+ }
+ mGestures.setZoomEnabled(true);
+ mZoomMax = param.getMaxZoom();
+ mZoomRatios = param.getZoomRatios();
+ // Currently we use immediate zoom for fast zooming to get better UX and
+ // there is no plan to take advantage of the smooth zoom.
+ mZoomRenderer.setZoomMax(mZoomMax);
+ mZoomRenderer.setZoom(param.getZoom());
+ mZoomRenderer.setZoomValue(mZoomRatios.get(param.getZoom()));
+ mZoomRenderer.setOnZoomChangeListener(new ZoomChangeListener());
+ }
+
+ public void clickShutter() {
+ mShutterButton.performClick();
+ }
+
+ public void pressShutter(boolean pressed) {
+ mShutterButton.setPressed(pressed);
+ }
+
+ public View getShutterButton() {
+ return mShutterButton;
+ }
+
+ public void setRecordingTime(String text) {
+ mRecordingTimeView.setText(text);
+ }
+
+ public void setRecordingTimeTextColor(int color) {
+ mRecordingTimeView.setTextColor(color);
+ }
+
+ public boolean isVisible() {
+ return mTextureView.getVisibility() == View.VISIBLE;
+ }
+
+ public void onDisplayChanged() {
+ mCameraControls.checkLayoutFlip();
+ mController.updateCameraOrientation();
+ }
+
+ /**
+ * Enable or disable the preview thumbnail for click events.
+ */
+ public void enablePreviewThumb(boolean enabled) {
+ if (enabled) {
+ mPreviewThumb.setVisibility(View.VISIBLE);
+ } else {
+ mPreviewThumb.setVisibility(View.GONE);
+ }
+ }
+
+ private class ZoomChangeListener implements ZoomRenderer.OnZoomChangedListener {
+ @Override
+ public void onZoomValueChanged(int index) {
+ int newZoom = mController.onZoomChanged(index);
+ if (mZoomRenderer != null) {
+ mZoomRenderer.setZoomValue(mZoomRatios.get(newZoom));
+ }
+ }
+
+ @Override
+ public void onZoomStart() {
+ }
+
+ @Override
+ public void onZoomEnd() {
+ }
+ }
+
+ public SurfaceTexture getSurfaceTexture() {
+ return mSurfaceTexture;
+ }
+
+ // SurfaceTexture callbacks
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ mSurfaceTexture = surface;
+ mController.onPreviewUIReady();
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ mSurfaceTexture = null;
+ mController.onPreviewUIDestroyed();
+ Log.d(TAG, "surfaceTexture is destroyed");
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ }
+
+ // SurfaceHolder callbacks
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ Log.v(TAG, "Surface changed. width=" + width + ". height=" + height);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ Log.v(TAG, "Surface created");
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ Log.v(TAG, "Surface destroyed");
+ mController.stopPreview();
+ }
+}
diff --git a/src/com/android/camera/data/AbstractLocalDataAdapterWrapper.java b/src/com/android/camera/data/AbstractLocalDataAdapterWrapper.java
new file mode 100644
index 000000000..66c55850a
--- /dev/null
+++ b/src/com/android/camera/data/AbstractLocalDataAdapterWrapper.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013 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.camera.data;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+
+/**
+ * An abstract {@link LocalDataAdapter} implementation to wrap another
+ * {@link LocalDataAdapter}. All implementations related to data id is not
+ * addressed in this abstract class since wrapping another data adapter
+ * surely makes things different for data id.
+ *
+ * @see FixedFirstDataAdapter
+ * @see FixedLastDataAdapter
+ */
+public abstract class AbstractLocalDataAdapterWrapper implements LocalDataAdapter {
+
+ protected final LocalDataAdapter mAdapter;
+ protected int mSuggestedWidth;
+ protected int mSuggestedHeight;
+
+ /**
+ * Constructor.
+ *
+ * @param wrappedAdapter The {@link LocalDataAdapter} to be wrapped.
+ */
+ AbstractLocalDataAdapterWrapper(LocalDataAdapter wrappedAdapter) {
+ if (wrappedAdapter == null) {
+ throw new AssertionError("data adapter is null");
+ }
+ mAdapter = wrappedAdapter;
+ }
+
+ @Override
+ public void suggestViewSizeBound(int w, int h) {
+ mSuggestedWidth = w;
+ mSuggestedHeight = h;
+ }
+
+ @Override
+ public void setListener(Listener listener) {
+ mAdapter.setListener(listener);
+ }
+
+ @Override
+ public void requestLoad(ContentResolver resolver) {
+ mAdapter.requestLoad(resolver);
+ }
+
+ @Override
+ public void addNewVideo(ContentResolver resolver, Uri uri) {
+ mAdapter.addNewVideo(resolver, uri);
+ }
+
+ @Override
+ public void addNewPhoto(ContentResolver resolver, Uri uri) {
+ mAdapter.addNewPhoto(resolver, uri);
+ }
+
+ @Override
+ public void flush() {
+ mAdapter.flush();
+ }
+
+ @Override
+ public boolean executeDeletion(Context context) {
+ return mAdapter.executeDeletion(context);
+ }
+
+ @Override
+ public boolean undoDataRemoval() {
+ return mAdapter.undoDataRemoval();
+ }
+}
diff --git a/src/com/android/camera/data/CameraDataAdapter.java b/src/com/android/camera/data/CameraDataAdapter.java
new file mode 100644
index 000000000..3605f7190
--- /dev/null
+++ b/src/com/android/camera/data/CameraDataAdapter.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2013 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.camera.data;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.View;
+
+import com.android.camera.Storage;
+import com.android.camera.ui.FilmStripView.ImageData;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * A {@link LocalDataAdapter} that provides data in the camera folder.
+ */
+public class CameraDataAdapter implements LocalDataAdapter {
+ private static final String TAG = CameraDataAdapter.class.getSimpleName();
+
+ private static final int DEFAULT_DECODE_SIZE = 3000;
+ private static final String[] CAMERA_PATH = { Storage.DIRECTORY + "%" };
+
+ private List<LocalData> mImages;
+
+ private Listener mListener;
+ private Drawable mPlaceHolder;
+
+ private int mSuggestedWidth = DEFAULT_DECODE_SIZE;
+ private int mSuggestedHeight = DEFAULT_DECODE_SIZE;
+
+ private LocalData mLocalDataToDelete;
+
+ public CameraDataAdapter(Drawable placeHolder) {
+ mPlaceHolder = placeHolder;
+ }
+
+ @Override
+ public void requestLoad(ContentResolver resolver) {
+ QueryTask qtask = new QueryTask();
+ qtask.execute(resolver);
+ }
+
+ @Override
+ public int getTotalNumber() {
+ if (mImages == null) {
+ return 0;
+ }
+ return mImages.size();
+ }
+
+ @Override
+ public ImageData getImageData(int id) {
+ return getData(id);
+ }
+
+ @Override
+ public void suggestViewSizeBound(int w, int h) {
+ if (w <= 0 || h <= 0) {
+ mSuggestedWidth = mSuggestedHeight = DEFAULT_DECODE_SIZE;
+ } else {
+ mSuggestedWidth = (w < DEFAULT_DECODE_SIZE ? w : DEFAULT_DECODE_SIZE);
+ mSuggestedHeight = (h < DEFAULT_DECODE_SIZE ? h : DEFAULT_DECODE_SIZE);
+ }
+ }
+
+ @Override
+ public View getView(Context c, int dataID) {
+ if (mImages == null) {
+ return null;
+ }
+ if (dataID >= mImages.size() || dataID < 0) {
+ return null;
+ }
+
+ return mImages.get(dataID).getView(
+ c, mSuggestedWidth, mSuggestedHeight,
+ mPlaceHolder.getConstantState().newDrawable());
+ }
+
+ @Override
+ public void setListener(Listener listener) {
+ mListener = listener;
+ if (mImages != null) {
+ mListener.onDataLoaded();
+ }
+ }
+
+ @Override
+ public void onDataFullScreen(int dataID, boolean fullScreen) {
+ if (dataID < mImages.size() && dataID >= 0) {
+ mImages.get(dataID).onFullScreen(fullScreen);
+ }
+ }
+
+ @Override
+ public void onDataCentered(int dataID, boolean centered) {
+ // do nothing.
+ }
+
+ @Override
+ public boolean canSwipeInFullScreen(int dataID) {
+ if (dataID < mImages.size() && dataID > 0) {
+ return mImages.get(dataID).canSwipeInFullScreen();
+ }
+ return true;
+ }
+
+ @Override
+ public void removeData(Context c, int dataID) {
+ if (dataID >= mImages.size()) return;
+ LocalData d = mImages.remove(dataID);
+ // Delete previously removed data first.
+ executeDeletion(c);
+ mLocalDataToDelete = d;
+ mListener.onDataRemoved(dataID, d);
+ }
+
+ private void insertData(LocalData data) {
+ if (mImages == null) {
+ mImages = new ArrayList<LocalData>();
+ }
+
+ // Since this function is mostly for adding the newest data,
+ // a simple linear search should yield the best performance over a
+ // binary search.
+ int pos = 0;
+ Comparator<LocalData> comp = new LocalData.NewestFirstComparator();
+ for (; pos < mImages.size()
+ && comp.compare(data, mImages.get(pos)) > 0; pos++);
+ mImages.add(pos, data);
+ if (mListener != null) {
+ mListener.onDataInserted(pos, data);
+ }
+ }
+
+ @Override
+ public void addNewVideo(ContentResolver cr, Uri uri) {
+ Cursor c = cr.query(uri,
+ LocalData.Video.QUERY_PROJECTION,
+ MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
+ LocalData.Video.QUERY_ORDER);
+ if (c != null && c.moveToFirst()) {
+ insertData(LocalData.Video.buildFromCursor(c));
+ }
+ }
+
+ @Override
+ public void addNewPhoto(ContentResolver cr, Uri uri) {
+ Cursor c = cr.query(uri,
+ LocalData.Photo.QUERY_PROJECTION,
+ MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
+ LocalData.Photo.QUERY_ORDER);
+ if (c != null && c.moveToFirst()) {
+ insertData(LocalData.Photo.buildFromCursor(c));
+ }
+ }
+
+ @Override
+ public int findDataByContentUri(Uri uri) {
+ // TODO: find the data.
+ return -1;
+ }
+
+ @Override
+ public boolean undoDataRemoval() {
+ if (mLocalDataToDelete == null) return false;
+ LocalData d = mLocalDataToDelete;
+ mLocalDataToDelete = null;
+ insertData(d);
+ return true;
+ }
+
+ @Override
+ public boolean executeDeletion(Context c) {
+ if (mLocalDataToDelete == null) return false;
+
+ DeletionTask task = new DeletionTask(c);
+ task.execute(mLocalDataToDelete);
+ mLocalDataToDelete = null;
+ return true;
+ }
+
+ @Override
+ public void flush() {
+ replaceData(null);
+ }
+
+ private LocalData getData(int id) {
+ if (mImages == null || id >= mImages.size() || id < 0) {
+ return null;
+ }
+ return mImages.get(id);
+ }
+
+ // Update all the data but keep the camera data if already set.
+ private void replaceData(List<LocalData> list) {
+ boolean changed = (list != mImages);
+ LocalData cameraData = null;
+ if (mImages != null && mImages.size() > 0) {
+ cameraData = mImages.get(0);
+ if (cameraData.getType() != ImageData.TYPE_CAMERA_PREVIEW) {
+ cameraData = null;
+ }
+ }
+
+ mImages = list;
+ if (cameraData != null) {
+ // camera view exists, so we make sure at least 1 data is in the list.
+ if (mImages == null) {
+ mImages = new ArrayList<LocalData>();
+ }
+ mImages.add(0, cameraData);
+ if (mListener != null) {
+ // Only the camera data is not changed, everything else is changed.
+ mListener.onDataUpdated(new UpdateReporter() {
+ @Override
+ public boolean isDataRemoved(int id) {
+ return false;
+ }
+
+ @Override
+ public boolean isDataUpdated(int id) {
+ if (id == 0) return false;
+ return true;
+ }
+ });
+ }
+ } else {
+ // both might be null.
+ if (changed) {
+ mListener.onDataLoaded();
+ }
+ }
+ }
+
+ private class QueryTask extends AsyncTask<ContentResolver, Void, List<LocalData>> {
+ @Override
+ protected List<LocalData> doInBackground(ContentResolver... resolver) {
+ List<LocalData> l = new ArrayList<LocalData>();
+ // Photos
+ Cursor c = resolver[0].query(
+ LocalData.Photo.CONTENT_URI,
+ LocalData.Photo.QUERY_PROJECTION,
+ MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH,
+ LocalData.Photo.QUERY_ORDER);
+ if (c != null && c.moveToFirst()) {
+ // build up the list.
+ while (true) {
+ LocalData data = LocalData.Photo.buildFromCursor(c);
+ if (data != null) {
+ l.add(data);
+ } else {
+ Log.e(TAG, "Error loading data:"
+ + c.getString(LocalData.Photo.COL_DATA));
+ }
+ if (c.isLast()) {
+ break;
+ }
+ c.moveToNext();
+ }
+ }
+ if (c != null) {
+ c.close();
+ }
+
+ c = resolver[0].query(
+ LocalData.Video.CONTENT_URI,
+ LocalData.Video.QUERY_PROJECTION,
+ MediaStore.Video.Media.DATA + " like ? ", CAMERA_PATH,
+ LocalData.Video.QUERY_ORDER);
+ if (c != null && c.moveToFirst()) {
+ // build up the list.
+ c.moveToFirst();
+ while (true) {
+ LocalData data = LocalData.Video.buildFromCursor(c);
+ if (data != null) {
+ l.add(data);
+ } else {
+ Log.e(TAG, "Error loading data:"
+ + c.getString(LocalData.Video.COL_DATA));
+ }
+ if (!c.isLast()) {
+ c.moveToNext();
+ } else {
+ break;
+ }
+ }
+ }
+ if (c != null) {
+ c.close();
+ }
+
+ if (l.size() == 0) return null;
+
+ Collections.sort(l, new LocalData.NewestFirstComparator());
+ return l;
+ }
+
+ @Override
+ protected void onPostExecute(List<LocalData> l) {
+ replaceData(l);
+ }
+ }
+
+ private class DeletionTask extends AsyncTask<LocalData, Void, Void> {
+ Context mContext;
+
+ DeletionTask(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ protected Void doInBackground(LocalData... data) {
+ for (int i = 0; i < data.length; i++) {
+ if (!data[i].isDataActionSupported(LocalData.ACTION_DELETE)) {
+ Log.v(TAG, "Deletion is not supported:" + data[i]);
+ continue;
+ }
+ data[i].delete(mContext);
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/camera/data/CameraPreviewData.java b/src/com/android/camera/data/CameraPreviewData.java
new file mode 100644
index 000000000..8f8e2138d
--- /dev/null
+++ b/src/com/android/camera/data/CameraPreviewData.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2013 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.camera.data;
+
+import android.view.View;
+
+import com.android.camera.ui.FilmStripView.ImageData;
+
+/**
+ * A class implementing {@link LocalData} to represent a camera preview.
+ */
+public class CameraPreviewData extends LocalData.LocalViewData {
+
+ private boolean mPreviewLocked;
+
+ /**
+ * Constructor.
+ *
+ * @param v The {@link android.view.View} for camera preview.
+ * @param width The width of the camera preview.
+ * @param height The height of the camera preview.
+ */
+ public CameraPreviewData(View v, int width, int height) {
+ super(v, width, height, -1, -1);
+ mPreviewLocked = true;
+ }
+
+ @Override
+ public int getType() {
+ return ImageData.TYPE_CAMERA_PREVIEW;
+ }
+
+ @Override
+ public boolean canSwipeInFullScreen() {
+ return !mPreviewLocked;
+ }
+
+ /**
+ * Locks the camera preview. When the camera preview is locked, swipe
+ * to film strip is not allowed. One case is when the video recording
+ * is in progress.
+ *
+ * @param lock {@code true} if the preview should be locked. {@code false}
+ * otherwise.
+ */
+ public void lockPreview(boolean lock) {
+ mPreviewLocked = lock;
+ }
+}
diff --git a/src/com/android/camera/data/FixedFirstDataAdapter.java b/src/com/android/camera/data/FixedFirstDataAdapter.java
new file mode 100644
index 000000000..34ba0a1a0
--- /dev/null
+++ b/src/com/android/camera/data/FixedFirstDataAdapter.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2013 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.camera.data;
+
+import android.content.Context;
+import android.net.Uri;
+import android.view.View;
+
+import com.android.camera.ui.FilmStripView;
+import com.android.camera.ui.FilmStripView.DataAdapter;
+import com.android.camera.ui.FilmStripView.ImageData;
+
+/**
+ * A {@link LocalDataAdapter} which puts a {@link LocalData} fixed at the first
+ * position. It's done by combining a {@link LocalData} and another
+ * {@link LocalDataAdapter}.
+ */
+public class FixedFirstDataAdapter extends AbstractLocalDataAdapterWrapper
+ implements DataAdapter.Listener {
+
+ private final LocalData mFirstData;
+ private Listener mListener;
+
+ /**
+ * Constructor.
+ *
+ * @param wrappedAdapter The {@link LocalDataAdapter} to be wrapped.
+ * @param firstData The {@link LocalData} to be placed at the first
+ * position.
+ */
+ public FixedFirstDataAdapter(
+ LocalDataAdapter wrappedAdapter,
+ LocalData firstData) {
+ super(wrappedAdapter);
+ if (firstData == null) {
+ throw new AssertionError("data is null");
+ }
+ mFirstData = firstData;
+ }
+
+ @Override
+ public void removeData(Context context, int dataID) {
+ if (dataID > 0) {
+ mAdapter.removeData(context, dataID - 1);
+ }
+ }
+
+ @Override
+ public int findDataByContentUri(Uri uri) {
+ int pos = mAdapter.findDataByContentUri(uri);
+ if (pos != -1) {
+ return pos + 1;
+ }
+ return -1;
+ }
+
+ @Override
+ public int getTotalNumber() {
+ return (mAdapter.getTotalNumber() + 1);
+ }
+
+ @Override
+ public View getView(Context context, int dataID) {
+ if (dataID == 0) {
+ return mFirstData.getView(
+ context, mSuggestedWidth, mSuggestedHeight, null);
+ }
+ return mAdapter.getView(context, dataID - 1);
+ }
+
+ @Override
+ public ImageData getImageData(int dataID) {
+ if (dataID == 0) {
+ return mFirstData;
+ }
+ return mAdapter.getImageData(dataID - 1);
+ }
+
+ @Override
+ public void onDataFullScreen(int dataID, boolean fullScreen) {
+ if (dataID == 0) {
+ mFirstData.onFullScreen(fullScreen);
+ } else {
+ mAdapter.onDataFullScreen(dataID - 1, fullScreen);
+ }
+ }
+
+ @Override
+ public void onDataCentered(int dataID, boolean centered) {
+ if (dataID != 0) {
+ mAdapter.onDataCentered(dataID, centered);
+ } else {
+ // TODO: notify the data
+ }
+ }
+
+ @Override
+ public void setListener(Listener listener) {
+ mListener = listener;
+ mAdapter.setListener((listener == null) ? null : this);
+ }
+
+ @Override
+ public boolean canSwipeInFullScreen(int dataID) {
+ if (dataID == 0) {
+ return mFirstData.canSwipeInFullScreen();
+ }
+ return mAdapter.canSwipeInFullScreen(dataID - 1);
+ }
+
+ @Override
+ public void onDataLoaded() {
+ mListener.onDataLoaded();
+ }
+
+ @Override
+ public void onDataUpdated(final UpdateReporter reporter) {
+ mListener.onDataUpdated(new UpdateReporter() {
+ @Override
+ public boolean isDataRemoved(int dataID) {
+ return reporter.isDataRemoved(dataID + 1);
+ }
+
+ @Override
+ public boolean isDataUpdated(int dataID) {
+ return reporter.isDataUpdated(dataID + 1);
+ }
+ });
+ }
+
+ @Override
+ public void onDataInserted(int dataID, ImageData data) {
+ mListener.onDataInserted(dataID + 1, data);
+ }
+
+ @Override
+ public void onDataRemoved(int dataID, ImageData data) {
+ mListener.onDataRemoved(dataID + 1, data);
+ }
+}
diff --git a/src/com/android/camera/data/FixedLastDataAdapter.java b/src/com/android/camera/data/FixedLastDataAdapter.java
new file mode 100644
index 000000000..16c047d1a
--- /dev/null
+++ b/src/com/android/camera/data/FixedLastDataAdapter.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2013 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.camera.data;
+
+import android.content.Context;
+import android.net.Uri;
+import android.view.View;
+
+import com.android.camera.ui.FilmStripView;
+
+/**
+ * A {@link LocalDataAdapter} which puts a {@link LocalData} fixed at the last
+ * position. It's done by combining a {@link LocalData} and another
+ * {@link LocalDataAdapter}.
+ */
+public class FixedLastDataAdapter extends AbstractLocalDataAdapterWrapper {
+
+ private final LocalData mLastData;
+
+ /**
+ * Constructor.
+ *
+ * @param wrappedAdapter The {@link LocalDataAdapter} to be wrapped.
+ * @param lastData The {@link LocalData} to be placed at the last position.
+ */
+ public FixedLastDataAdapter(
+ LocalDataAdapter wrappedAdapter,
+ LocalData lastData) {
+ super(wrappedAdapter);
+ if (lastData == null) {
+ throw new AssertionError("data is null");
+ }
+ mLastData = lastData;
+ }
+
+ @Override
+ public void removeData(Context context, int dataID) {
+ if (dataID < mAdapter.getTotalNumber()) {
+ mAdapter.removeData(context, dataID);
+ }
+ }
+
+ @Override
+ public int findDataByContentUri(Uri uri) {
+ return mAdapter.findDataByContentUri(uri);
+ }
+
+ @Override
+ public int getTotalNumber() {
+ return mAdapter.getTotalNumber() + 1;
+ }
+
+ @Override
+ public View getView(Context context, int dataID) {
+ int totalNumber = mAdapter.getTotalNumber();
+
+ if (dataID < totalNumber) {
+ return mAdapter.getView(context, dataID);
+ } else if (dataID == totalNumber) {
+ return mLastData.getView(context,
+ mSuggestedWidth, mSuggestedHeight, null);
+ }
+
+ return null;
+ }
+
+ @Override
+ public FilmStripView.ImageData getImageData(int dataID) {
+ int totalNumber = mAdapter.getTotalNumber();
+
+ if (dataID < totalNumber) {
+ return mAdapter.getImageData(dataID);
+ } else if (dataID == totalNumber) {
+ return mLastData;
+ }
+ return null;
+ }
+
+ @Override
+ public void onDataFullScreen(int dataID, boolean fullScreen) {
+ int totalNumber = mAdapter.getTotalNumber();
+
+ if (dataID < totalNumber) {
+ mAdapter.onDataFullScreen(dataID, fullScreen);
+ } else if (dataID == totalNumber) {
+ mLastData.onFullScreen(fullScreen);
+ }
+ }
+
+ @Override
+ public void onDataCentered(int dataID, boolean centered) {
+ int totalNumber = mAdapter.getTotalNumber();
+
+ if (dataID < totalNumber) {
+ mAdapter.onDataCentered(dataID, centered);
+ } else if (dataID == totalNumber) {
+ // TODO: notify the data
+ }
+ }
+
+ @Override
+ public boolean canSwipeInFullScreen(int dataID) {
+ int totalNumber = mAdapter.getTotalNumber();
+
+ if (dataID < totalNumber) {
+ return mAdapter.canSwipeInFullScreen(dataID);
+ } else if (dataID == totalNumber) {
+ return mLastData.canSwipeInFullScreen();
+ }
+ return false;
+ }
+}
+
diff --git a/src/com/android/camera/data/LocalData.java b/src/com/android/camera/data/LocalData.java
new file mode 100644
index 000000000..efccfe332
--- /dev/null
+++ b/src/com/android/camera/data/LocalData.java
@@ -0,0 +1,726 @@
+/*
+ * Copyright (C) 2013 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.camera.data;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video.VideoColumns;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.camera.Util;
+import com.android.camera.data.PanoramaMetadataLoader.PanoramaMetadataCallback;
+import com.android.camera.ui.FilmStripView;
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper;
+
+import java.io.File;
+import java.util.Comparator;
+import java.util.Date;
+
+/**
+ * An abstract interface that represents the local media data. Also implements
+ * Comparable interface so we can sort in DataAdapter.
+ */
+public interface LocalData extends FilmStripView.ImageData {
+ static final String TAG = "CAM_LocalData";
+
+ public static final int ACTION_NONE = 0;
+ public static final int ACTION_PLAY = 1;
+ public static final int ACTION_DELETE = (1 << 1);
+
+ View getView(Context c, int width, int height, Drawable placeHolder);
+ long getDateTaken();
+ long getDateModified();
+ String getTitle();
+ boolean isDataActionSupported(int action);
+ boolean delete(Context c);
+ void onFullScreen(boolean fullScreen);
+ boolean canSwipeInFullScreen();
+ String getPath();
+
+ static class NewestFirstComparator implements Comparator<LocalData> {
+
+ /** Compare taken/modified date of LocalData in descent order to make
+ newer data in the front.
+ The negative numbers here are always considered "bigger" than
+ positive ones. Thus, if any one of the numbers is negative, the logic
+ is reversed. */
+ private static int compareDate(long v1, long v2) {
+ if (v1 >= 0 && v2 >= 0) {
+ return ((v1 < v2) ? 1 : ((v1 > v2) ? -1 : 0));
+ }
+ return ((v2 < v1) ? 1 : ((v2 > v1) ? -1 : 0));
+ }
+
+ @Override
+ public int compare(LocalData d1, LocalData d2) {
+ int cmp = compareDate(d1.getDateTaken(), d2.getDateTaken());
+ if (cmp == 0) {
+ cmp = compareDate(d1.getDateModified(), d2.getDateModified());
+ }
+ if (cmp == 0) {
+ cmp = d1.getTitle().compareTo(d2.getTitle());
+ }
+ return cmp;
+ }
+ }
+
+ // Implementations below.
+
+ /**
+<<<<<<< HEAD
+ * A base class for all the local media files. The bitmap is loaded in
+ * background thread. Subclasses should implement their own background
+ * loading thread by subclassing BitmapLoadTask and overriding
+ * doInBackground() to return a bitmap.
+=======
+ * A base class for all the local media files. The bitmap is loaded in background
+ * thread. Subclasses should implement their own background loading thread by
+ * sub-classing BitmapLoadTask and overriding doInBackground() to return a bitmap.
+>>>>>>> Add LocalDataAdapter and wrappers.
+ */
+ abstract static class LocalMediaData implements LocalData {
+ protected long id;
+ protected String title;
+ protected String mimeType;
+ protected long dateTaken;
+ protected long dateModified;
+ protected String path;
+ // width and height should be adjusted according to orientation.
+ protected int width;
+ protected int height;
+
+ /** The panorama metadata information of this media data. */
+ private PanoramaMetadata mPanoramaMetadata;
+
+ /** Used to load photo sphere metadata from image files. */
+ private PanoramaMetadataLoader mPanoramaMetadataLoader = null;
+
+ // true if this data has a corresponding visible view.
+ protected Boolean mUsing = false;
+
+ @Override
+ public long getDateTaken() {
+ return dateTaken;
+ }
+
+ @Override
+ public long getDateModified() {
+ return dateModified;
+ }
+
+ @Override
+ public String getTitle() {
+ return new String(title);
+ }
+
+ @Override
+ public int getWidth() {
+ return width;
+ }
+
+ @Override
+ public int getHeight() {
+ return height;
+ }
+
+ @Override
+ public String getPath() {
+ return path;
+ }
+
+ @Override
+ public boolean isUIActionSupported(int action) {
+ return false;
+ }
+
+ @Override
+ public boolean isDataActionSupported(int action) {
+ return false;
+ }
+
+ @Override
+ public boolean delete(Context ctx) {
+ File f = new File(path);
+ return f.delete();
+ }
+
+ @Override
+ public void viewPhotoSphere(PanoramaViewHelper helper) {
+ helper.showPanorama(getContentUri());
+ }
+
+ @Override
+ public void isPhotoSphere(Context context, final PanoramaSupportCallback callback) {
+ // If we already have metadata, use it.
+ if (mPanoramaMetadata != null) {
+ callback.panoramaInfoAvailable(mPanoramaMetadata.mUsePanoramaViewer,
+ mPanoramaMetadata.mIsPanorama360);
+ }
+
+ // Otherwise prepare a loader, if we don't have one already.
+ if (mPanoramaMetadataLoader == null) {
+ mPanoramaMetadataLoader = new PanoramaMetadataLoader(getContentUri());
+ }
+
+ // Load the metadata asynchronously.
+ mPanoramaMetadataLoader.getPanoramaMetadata(context, new PanoramaMetadataCallback() {
+ @Override
+ public void onPanoramaMetadataLoaded(PanoramaMetadata metadata) {
+ // Store the metadata and remove the loader to free up space.
+ mPanoramaMetadata = metadata;
+ mPanoramaMetadataLoader = null;
+ callback.panoramaInfoAvailable(metadata.mUsePanoramaViewer,
+ metadata.mIsPanorama360);
+ }
+ });
+ }
+
+ @Override
+ public void onFullScreen(boolean fullScreen) {
+ // do nothing.
+ }
+
+ @Override
+ public boolean canSwipeInFullScreen() {
+ return true;
+ }
+
+ protected ImageView fillImageView(Context ctx, ImageView v,
+ int decodeWidth, int decodeHeight, Drawable placeHolder) {
+ v.setScaleType(ImageView.ScaleType.FIT_XY);
+ v.setImageDrawable(placeHolder);
+
+ BitmapLoadTask task = getBitmapLoadTask(v, decodeWidth, decodeHeight);
+ task.execute();
+ return v;
+ }
+
+ @Override
+ public View getView(Context ctx,
+ int decodeWidth, int decodeHeight, Drawable placeHolder) {
+ return fillImageView(ctx, new ImageView(ctx),
+ decodeWidth, decodeHeight, placeHolder);
+ }
+
+ @Override
+ public void prepare() {
+ synchronized (mUsing) {
+ mUsing = true;
+ }
+ }
+
+ @Override
+ public void recycle() {
+ synchronized (mUsing) {
+ mUsing = false;
+ }
+ }
+
+ protected boolean isUsing() {
+ synchronized (mUsing) {
+ return mUsing;
+ }
+ }
+
+ /**
+ * Returns the content URI of this data item.
+ */
+ private Uri getContentUri() {
+ Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+ return baseUri.buildUpon().appendPath(String.valueOf(id)).build();
+ }
+
+ @Override
+ public abstract int getType();
+
+ protected abstract BitmapLoadTask getBitmapLoadTask(
+ ImageView v, int decodeWidth, int decodeHeight);
+
+ /**
+ * An AsyncTask class that loads the bitmap in the background thread.
+ * Sub-classes should implement their own "protected Bitmap doInBackground(Void... )"
+ */
+ protected abstract class BitmapLoadTask extends AsyncTask<Void, Void, Bitmap> {
+ protected ImageView mView;
+
+ protected BitmapLoadTask(ImageView v) {
+ mView = v;
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (!isUsing()) return;
+ if (bitmap == null) {
+ Log.e(TAG, "Failed decoding bitmap for file:" + path);
+ return;
+ }
+ BitmapDrawable d = new BitmapDrawable(bitmap);
+ mView.setScaleType(ImageView.ScaleType.FIT_XY);
+ mView.setImageDrawable(d);
+ }
+ }
+ }
+
+ static class Photo extends LocalMediaData {
+ public static final int COL_ID = 0;
+ public static final int COL_TITLE = 1;
+ public static final int COL_MIME_TYPE = 2;
+ public static final int COL_DATE_TAKEN = 3;
+ public static final int COL_DATE_MODIFIED = 4;
+ public static final int COL_DATA = 5;
+ public static final int COL_ORIENTATION = 6;
+ public static final int COL_WIDTH = 7;
+ public static final int COL_HEIGHT = 8;
+
+ static final Uri CONTENT_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+
+ static final String QUERY_ORDER = ImageColumns.DATE_TAKEN + " DESC, "
+ + ImageColumns._ID + " DESC";
+ /**
+ * These values should be kept in sync with column IDs (COL_*) above.
+ */
+ static final String[] QUERY_PROJECTION = {
+ ImageColumns._ID, // 0, int
+ ImageColumns.TITLE, // 1, string
+ ImageColumns.MIME_TYPE, // 2, string
+ ImageColumns.DATE_TAKEN, // 3, int
+ ImageColumns.DATE_MODIFIED, // 4, int
+ ImageColumns.DATA, // 5, string
+ ImageColumns.ORIENTATION, // 6, int, 0, 90, 180, 270
+ ImageColumns.WIDTH, // 7, int
+ ImageColumns.HEIGHT, // 8, int
+ };
+
+ private static final int mSupportedUIActions =
+ FilmStripView.ImageData.ACTION_DEMOTE
+ | FilmStripView.ImageData.ACTION_PROMOTE;
+ private static final int mSupportedDataActions =
+ LocalData.ACTION_DELETE;
+
+ /** 32K buffer. */
+ private static final byte[] DECODE_TEMP_STORAGE = new byte[32 * 1024];
+
+ /** from MediaStore, can only be 0, 90, 180, 270 */
+ public int orientation;
+
+ static Photo buildFromCursor(Cursor c) {
+ Photo d = new Photo();
+ d.id = c.getLong(COL_ID);
+ d.title = c.getString(COL_TITLE);
+ d.mimeType = c.getString(COL_MIME_TYPE);
+ d.dateTaken = c.getLong(COL_DATE_TAKEN);
+ d.dateModified = c.getLong(COL_DATE_MODIFIED);
+ d.path = c.getString(COL_DATA);
+ d.orientation = c.getInt(COL_ORIENTATION);
+ d.width = c.getInt(COL_WIDTH);
+ d.height = c.getInt(COL_HEIGHT);
+ if (d.width <= 0 || d.height <= 0) {
+ Log.w(TAG, "Warning! zero dimension for "
+ + d.path + ":" + d.width + "x" + d.height);
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(d.path, opts);
+ if (opts.outWidth != -1 && opts.outHeight != -1) {
+ d.width = opts.outWidth;
+ d.height = opts.outHeight;
+ } else {
+ Log.w(TAG, "Warning! dimension decode failed for " + d.path);
+ Bitmap b = BitmapFactory.decodeFile(d.path);
+ if (b == null) {
+ return null;
+ }
+ d.width = b.getWidth();
+ d.height = b.getHeight();
+ }
+ }
+ if (d.orientation == 90 || d.orientation == 270) {
+ int b = d.width;
+ d.width = d.height;
+ d.height = b;
+ }
+ return d;
+ }
+
+ @Override
+ public String toString() {
+ return "Photo:" + ",data=" + path + ",mimeType=" + mimeType
+ + "," + width + "x" + height + ",orientation=" + orientation
+ + ",date=" + new Date(dateTaken);
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_PHOTO;
+ }
+
+ @Override
+ public boolean isUIActionSupported(int action) {
+ return ((action & mSupportedUIActions) == action);
+ }
+
+ @Override
+ public boolean isDataActionSupported(int action) {
+ return ((action & mSupportedDataActions) == action);
+ }
+
+ @Override
+ public boolean delete(Context c) {
+ ContentResolver cr = c.getContentResolver();
+ cr.delete(CONTENT_URI, ImageColumns._ID + "=" + id, null);
+ return super.delete(c);
+ }
+
+ @Override
+ protected BitmapLoadTask getBitmapLoadTask(
+ ImageView v, int decodeWidth, int decodeHeight) {
+ return new PhotoBitmapLoadTask(v, decodeWidth, decodeHeight);
+ }
+
+ private final class PhotoBitmapLoadTask extends BitmapLoadTask {
+ private int mDecodeWidth;
+ private int mDecodeHeight;
+
+ public PhotoBitmapLoadTask(ImageView v, int decodeWidth, int decodeHeight) {
+ super(v);
+ mDecodeWidth = decodeWidth;
+ mDecodeHeight = decodeHeight;
+ }
+
+ @Override
+ protected Bitmap doInBackground(Void... v) {
+ BitmapFactory.Options opts = null;
+ Bitmap b;
+ int sample = 1;
+ while (mDecodeWidth * sample < width
+ || mDecodeHeight * sample < height) {
+ sample *= 2;
+ }
+ opts = new BitmapFactory.Options();
+ opts.inSampleSize = sample;
+ opts.inTempStorage = DECODE_TEMP_STORAGE;
+ if (isCancelled() || !isUsing()) {
+ return null;
+ }
+ b = BitmapFactory.decodeFile(path, opts);
+ if (orientation != 0) {
+ if (isCancelled() || !isUsing()) {
+ return null;
+ }
+ Matrix m = new Matrix();
+ m.setRotate(orientation);
+ b = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false);
+ }
+ return b;
+ }
+ }
+ }
+
+ static class Video extends LocalMediaData {
+ public static final int COL_ID = 0;
+ public static final int COL_TITLE = 1;
+ public static final int COL_MIME_TYPE = 2;
+ public static final int COL_DATE_TAKEN = 3;
+ public static final int COL_DATE_MODIFIED = 4;
+ public static final int COL_DATA = 5;
+ public static final int COL_WIDTH = 6;
+ public static final int COL_HEIGHT = 7;
+ public static final int COL_RESOLUTION = 8;
+
+ static final Uri CONTENT_URI = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+
+ private static final int mSupportedUIActions =
+ FilmStripView.ImageData.ACTION_DEMOTE
+ | FilmStripView.ImageData.ACTION_PROMOTE;
+ private static final int mSupportedDataActions =
+ LocalData.ACTION_DELETE
+ | LocalData.ACTION_PLAY;
+
+ static final String QUERY_ORDER = VideoColumns.DATE_TAKEN + " DESC, "
+ + VideoColumns._ID + " DESC";
+ /**
+ * These values should be kept in sync with column IDs (COL_*) above.
+ */
+ static final String[] QUERY_PROJECTION = {
+ VideoColumns._ID, // 0, int
+ VideoColumns.TITLE, // 1, string
+ VideoColumns.MIME_TYPE, // 2, string
+ VideoColumns.DATE_TAKEN, // 3, int
+ VideoColumns.DATE_MODIFIED, // 4, int
+ VideoColumns.DATA, // 5, string
+ VideoColumns.WIDTH, // 6, int
+ VideoColumns.HEIGHT, // 7, int
+ VideoColumns.RESOLUTION // 8, string
+ };
+
+ private Uri mPlayUri;
+
+ static Video buildFromCursor(Cursor c) {
+ Video d = new Video();
+ d.id = c.getLong(COL_ID);
+ d.title = c.getString(COL_TITLE);
+ d.mimeType = c.getString(COL_MIME_TYPE);
+ d.dateTaken = c.getLong(COL_DATE_TAKEN);
+ d.dateModified = c.getLong(COL_DATE_MODIFIED);
+ d.path = c.getString(COL_DATA);
+ d.width = c.getInt(COL_WIDTH);
+ d.height = c.getInt(COL_HEIGHT);
+ d.mPlayUri = CONTENT_URI.buildUpon()
+ .appendPath(String.valueOf(d.id)).build();
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+ retriever.setDataSource(d.path);
+ String rotation = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+ if (d.width == 0 || d.height == 0) {
+ d.width = Integer.parseInt(retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
+ d.height = Integer.parseInt(retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
+ }
+ retriever.release();
+ if (rotation != null
+ && (rotation.equals("90") || rotation.equals("270"))) {
+ int b = d.width;
+ d.width = d.height;
+ d.height = b;
+ }
+ return d;
+ }
+
+ @Override
+ public String toString() {
+ return "Video:" + ",data=" + path + ",mimeType=" + mimeType
+ + "," + width + "x" + height + ",date=" + new Date(dateTaken);
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_PHOTO;
+ }
+
+ @Override
+ public boolean isUIActionSupported(int action) {
+ return ((action & mSupportedUIActions) == action);
+ }
+
+ @Override
+ public boolean isDataActionSupported(int action) {
+ return ((action & mSupportedDataActions) == action);
+ }
+
+ @Override
+ public boolean delete(Context ctx) {
+ ContentResolver cr = ctx.getContentResolver();
+ cr.delete(CONTENT_URI, VideoColumns._ID + "=" + id, null);
+ return super.delete(ctx);
+ }
+
+ @Override
+ public View getView(final Context ctx,
+ int decodeWidth, int decodeHeight, Drawable placeHolder) {
+
+ // ImageView for the bitmap.
+ ImageView iv = new ImageView(ctx);
+ iv.setLayoutParams(new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
+ fillImageView(ctx, iv, decodeWidth, decodeHeight, placeHolder);
+
+ // ImageView for the play icon.
+ ImageView icon = new ImageView(ctx);
+ icon.setImageResource(R.drawable.ic_control_play);
+ icon.setScaleType(ImageView.ScaleType.CENTER);
+ icon.setLayoutParams(new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
+ icon.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Util.playVideo(ctx, mPlayUri, title);
+ }
+ });
+
+ FrameLayout f = new FrameLayout(ctx);
+ f.addView(iv);
+ f.addView(icon);
+ return f;
+ }
+
+ @Override
+ protected BitmapLoadTask getBitmapLoadTask(
+ ImageView v, int decodeWidth, int decodeHeight) {
+ return new VideoBitmapLoadTask(v);
+ }
+
+ private final class VideoBitmapLoadTask extends BitmapLoadTask {
+
+ public VideoBitmapLoadTask(ImageView v) {
+ super(v);
+ }
+
+ @Override
+ protected Bitmap doInBackground(Void... v) {
+ if (isCancelled() || !isUsing()) {
+ return null;
+ }
+ android.media.MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+ retriever.setDataSource(path);
+ byte[] data = retriever.getEmbeddedPicture();
+ Bitmap bitmap = null;
+ if (isCancelled() || !isUsing()) {
+ retriever.release();
+ return null;
+ }
+ if (data != null) {
+ bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+ }
+ if (bitmap == null) {
+ bitmap = retriever.getFrameAtTime();
+ }
+ retriever.release();
+ return bitmap;
+ }
+ }
+ }
+
+ /**
+ * A LocalData that does nothing but only shows a view.
+ */
+ public static class LocalViewData implements LocalData {
+ private int mWidth;
+ private int mHeight;
+ private View mView;
+ private long mDateTaken;
+ private long mDateModified;
+
+ public LocalViewData(View v,
+ int width, int height,
+ int dateTaken, int dateModified) {
+ mView = v;
+ mWidth = width;
+ mHeight = height;
+ mDateTaken = dateTaken;
+ mDateModified = dateModified;
+ }
+
+ @Override
+ public long getDateTaken() {
+ return mDateTaken;
+ }
+
+ @Override
+ public long getDateModified() {
+ return mDateModified;
+ }
+
+ @Override
+ public String getTitle() {
+ return "";
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public int getType() {
+ return FilmStripView.ImageData.TYPE_PHOTO;
+ }
+
+ @Override
+ public String getPath() {
+ return "";
+ }
+
+ @Override
+ public boolean isUIActionSupported(int action) {
+ return false;
+ }
+
+ @Override
+ public boolean isDataActionSupported(int action) {
+ return false;
+ }
+
+ @Override
+ public boolean delete(Context c) {
+ return false;
+ }
+
+ @Override
+ public View getView(Context c, int width, int height, Drawable placeHolder) {
+ return mView;
+ }
+
+ @Override
+ public void prepare() {
+ // do nothing.
+ }
+
+ @Override
+ public void recycle() {
+ // do nothing.
+ }
+
+ @Override
+ public void isPhotoSphere(Context context, PanoramaSupportCallback callback) {
+ // Not a photo sphere panorama.
+ callback.panoramaInfoAvailable(false, false);
+ }
+
+ @Override
+ public void viewPhotoSphere(PanoramaViewHelper helper) {
+ // do nothing.
+ }
+
+ @Override
+ public void onFullScreen(boolean fullScreen) {
+ // do nothing.
+ }
+
+ @Override
+ public boolean canSwipeInFullScreen() {
+ return true;
+ }
+ }
+}
+
diff --git a/src/com/android/camera/data/LocalDataAdapter.java b/src/com/android/camera/data/LocalDataAdapter.java
new file mode 100644
index 000000000..3b4f07dea
--- /dev/null
+++ b/src/com/android/camera/data/LocalDataAdapter.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2013 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.camera.data;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+
+import static com.android.camera.ui.FilmStripView.DataAdapter;
+
+/**
+ * An interface which extends {@link DataAdapter} and defines operations on
+ * the data in the local camera folder.
+ */
+public interface LocalDataAdapter extends DataAdapter {
+
+ /**
+ * Request for loading the local data.
+ *
+ * @param resolver {@link ContentResolver} used for data loading.
+ */
+ public void requestLoad(ContentResolver resolver);
+
+ /**
+ * Remove the data in the local camera folder.
+ *
+ * @param context {@link Context} used to remove the data.
+ * @param dataID ID of data to be deleted.
+ */
+ public void removeData(Context context, int dataID);
+
+ /**
+ * Add new local video data.
+ *
+ * @param resolver {@link ContentResolver} used to add the data.
+ * @param uri {@link Uri} of the video.
+ */
+ public void addNewVideo(ContentResolver resolver, Uri uri);
+
+ /**
+ * Adds new local photo data.
+ *
+ * @param resolver {@link ContentResolver} used to add the data.
+ * @param uri {@link Uri} of the photo.
+ */
+ public void addNewPhoto(ContentResolver resolver, Uri uri);
+
+ /**
+ * Finds the {@link LocalData} of the specified content Uri.
+ *
+ * @param Uri The content Uri of the {@link LocalData}.
+ * @return The index of the data. {@code -1} if not found.
+ */
+ public int findDataByContentUri(Uri uri);
+
+ /**
+ * Clears all the data currently loaded.
+ */
+ public void flush();
+
+ /**
+ * Executes the deletion task. Delete the data waiting in the deletion queue.
+ *
+ * @param context The {@link Context} from the caller.
+ * @return {@code true} if task has been executed, {@code false}
+ * otherwise.
+ */
+ public boolean executeDeletion(Context context);
+
+ /**
+ * Undo a deletion. If there is any data waiting to be deleted in the queue,
+ * move it out of the deletion queue.
+ *
+ * @return {@code true} if there are items in the queue, {@code false} otherwise.
+ */
+ public boolean undoDataRemoval();
+}
diff --git a/src/com/android/camera/data/PanoramaMetadataLoader.java b/src/com/android/camera/data/PanoramaMetadataLoader.java
new file mode 100644
index 000000000..21b5f8a3d
--- /dev/null
+++ b/src/com/android/camera/data/PanoramaMetadataLoader.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013 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.camera.data;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
+
+import java.util.ArrayList;
+
+/**
+ * This class breaks out the off-thread panorama support.
+ */
+public class PanoramaMetadataLoader {
+ /**
+ * Classes implementing this interface can get information about loaded
+ * photo sphere metadata.
+ */
+ public static interface PanoramaMetadataCallback {
+ /**
+ * Called with the loaded metadata or <code>null</code>.
+ */
+ public void onPanoramaMetadataLoaded(PanoramaMetadata metadata);
+ }
+
+ private PanoramaMetadata mPanoramaMetadata;
+ private ArrayList<PanoramaMetadataCallback> mCallbacksWaiting;
+ private Uri mMediaUri;
+
+ /**
+ * Instantiated the meta data loader for the image resource with the given
+ * URI.
+ */
+ public PanoramaMetadataLoader(Uri uri) {
+ mMediaUri = uri;
+ }
+
+ /**
+ * Asynchronously extract and return panorama metadata from the item with
+ * the given URI.
+ * <p>
+ * NOTE: This call is backed by a cache to speed up successive calls, which
+ * will return immediately. Use {@link #clearCachedValues()} is called.
+ */
+ public synchronized void getPanoramaMetadata(final Context context,
+ PanoramaMetadataCallback callback) {
+ if (mPanoramaMetadata != null) {
+ // Return the cached data right away, no need to fetch it again.
+ callback.onPanoramaMetadataLoaded(mPanoramaMetadata);
+ } else {
+ if (mCallbacksWaiting == null) {
+ mCallbacksWaiting = new ArrayList<PanoramaMetadataCallback>();
+
+ // TODO: Don't create a new thread each time, use a pool or
+ // single instance.
+ (new Thread() {
+ @Override
+ public void run() {
+ onLoadingDone(LightCycleHelper.getPanoramaMetadata(context,
+ mMediaUri));
+ }
+ }).start();
+ }
+ mCallbacksWaiting.add(callback);
+ }
+ }
+
+ /**
+ * Clear cached value and stop all running loading threads.
+ */
+ public synchronized void clearCachedValues() {
+ if (mPanoramaMetadata != null) {
+ mPanoramaMetadata = null;
+ }
+
+ // TODO: Cancel running loading thread if active.
+ }
+
+ private synchronized void onLoadingDone(PanoramaMetadata metadata) {
+ mPanoramaMetadata = metadata;
+ if (mPanoramaMetadata == null) {
+ // Error getting panorama data from file. Treat as not panorama.
+ mPanoramaMetadata = LightCycleHelper.NOT_PANORAMA;
+ }
+ for (PanoramaMetadataCallback cb : mCallbacksWaiting) {
+ cb.onPanoramaMetadataLoaded(mPanoramaMetadata);
+ }
+ mCallbacksWaiting = null;
+ }
+}
diff --git a/src/com/android/camera/drawable/TextDrawable.java b/src/com/android/camera/drawable/TextDrawable.java
new file mode 100644
index 000000000..60d8719c4
--- /dev/null
+++ b/src/com/android/camera/drawable/TextDrawable.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 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.camera.drawable;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+
+
+public class TextDrawable extends Drawable {
+
+ private static final int DEFAULT_COLOR = Color.WHITE;
+ private static final int DEFAULT_TEXTSIZE = 15;
+
+ private Paint mPaint;
+ private CharSequence mText;
+ private int mIntrinsicWidth;
+ private int mIntrinsicHeight;
+ private boolean mUseDropShadow;
+
+ public TextDrawable(Resources res) {
+ this(res, "");
+ }
+
+ public TextDrawable(Resources res, CharSequence text) {
+ mText = text;
+ updatePaint();
+ float textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
+ DEFAULT_TEXTSIZE, res.getDisplayMetrics());
+ mPaint.setTextSize(textSize);
+ mIntrinsicWidth = (int) (mPaint.measureText(mText, 0, mText.length()) + .5);
+ mIntrinsicHeight = mPaint.getFontMetricsInt(null);
+ }
+
+ private void updatePaint() {
+ if (mPaint == null) {
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ }
+ mPaint.setColor(DEFAULT_COLOR);
+ mPaint.setTextAlign(Align.CENTER);
+ if (mUseDropShadow) {
+ mPaint.setTypeface(Typeface.DEFAULT_BOLD);
+ mPaint.setShadowLayer(10, 0, 0, 0xff000000);
+ } else {
+ mPaint.setTypeface(Typeface.DEFAULT);
+ mPaint.setShadowLayer(0, 0, 0, 0);
+ }
+ }
+
+ public void setText(CharSequence txt) {
+ mText = txt;
+ if (txt == null) {
+ mIntrinsicWidth = 0;
+ mIntrinsicHeight = 0;
+ } else {
+ mIntrinsicWidth = (int) (mPaint.measureText(mText, 0, mText.length()) + .5);
+ mIntrinsicHeight = mPaint.getFontMetricsInt(null);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mText != null) {
+ Rect bounds = getBounds();
+ canvas.drawText(mText, 0, mText.length(),
+ bounds.centerX(), bounds.centerY(), mPaint);
+ }
+ }
+
+ public void setDropShadow(boolean shadow) {
+ mUseDropShadow = shadow;
+ updatePaint();
+ }
+
+ @Override
+ public int getOpacity() {
+ return mPaint.getAlpha();
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mIntrinsicWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mIntrinsicHeight;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter filter) {
+ mPaint.setColorFilter(filter);
+ }
+
+}
diff --git a/src/com/android/camera/ui/AbstractSettingPopup.java b/src/com/android/camera/ui/AbstractSettingPopup.java
new file mode 100644
index 000000000..783b6c771
--- /dev/null
+++ b/src/com/android/camera/ui/AbstractSettingPopup.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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+// A popup window that shows one or more camera settings.
+abstract public class AbstractSettingPopup extends RotateLayout {
+ protected ViewGroup mSettingList;
+ protected TextView mTitle;
+
+ public AbstractSettingPopup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mTitle = (TextView) findViewById(R.id.title);
+ mSettingList = (ViewGroup) findViewById(R.id.settingList);
+ }
+
+ abstract public void reloadPreference();
+}
diff --git a/src/com/android/camera/ui/CameraControls.java b/src/com/android/camera/ui/CameraControls.java
new file mode 100644
index 000000000..7fa6890a7
--- /dev/null
+++ b/src/com/android/camera/ui/CameraControls.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2013 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.camera.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.camera.Util;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+public class CameraControls extends RotatableLayout {
+
+ private static final String TAG = "CAM_Controls";
+
+ private View mBackgroundView;
+ private View mShutter;
+ private View mSwitcher;
+ private View mMenu;
+ private View mIndicators;
+ private View mPreview;
+
+ public CameraControls(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CameraControls(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onFinishInflate() {
+ super.onFinishInflate();
+ mBackgroundView = findViewById(R.id.blocker);
+ mSwitcher = findViewById(R.id.camera_switcher);
+ mShutter = findViewById(R.id.shutter_button);
+ mMenu = findViewById(R.id.menu);
+ mIndicators = findViewById(R.id.on_screen_indicators);
+ mPreview = findViewById(R.id.preview_thumb);
+ }
+
+ @Override
+ public void onLayout(boolean changed, int l, int t, int r, int b) {
+ int orientation = getResources().getConfiguration().orientation;
+ int size = getResources().getDimensionPixelSize(R.dimen.camera_controls_size);
+ int rotation = getUnifiedRotation();
+ adjustBackground();
+ // As l,t,r,b are positions relative to parents, we need to convert them
+ // to child's coordinates
+ r = r - l;
+ b = b - t;
+ l = 0;
+ t = 0;
+ for (int i = 0; i < getChildCount(); i++) {
+ View v = getChildAt(i);
+ v.layout(l, t, r, b);
+ }
+ Rect shutter = new Rect();
+ topRight(mPreview, l, t, r, b);
+ if (size > 0) {
+ // restrict controls to size
+ switch (rotation) {
+ case 0:
+ case 180:
+ l = (l + r - size) / 2;
+ r = l + size;
+ break;
+ case 90:
+ case 270:
+ t = (t + b - size) / 2;
+ b = t + size;
+ break;
+ }
+ }
+ center(mShutter, l, t, r, b, orientation, rotation, shutter);
+ center(mBackgroundView, l, t, r, b, orientation, rotation, new Rect());
+ toLeft(mSwitcher, shutter, rotation);
+ toRight(mMenu, shutter, rotation);
+ toRight(mIndicators, shutter, rotation);
+ View retake = findViewById(R.id.btn_retake);
+ if (retake != null) {
+ center(retake, shutter, rotation);
+ View cancel = findViewById(R.id.btn_cancel);
+ toLeft(cancel, shutter, rotation);
+ View done = findViewById(R.id.btn_done);
+ toRight(done, shutter, rotation);
+ }
+ }
+
+ private void center(View v, int l, int t, int r, int b, int orientation, int rotation, Rect result) {
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams();
+ int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin;
+ int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin;
+ switch (rotation) {
+ case 0:
+ // phone portrait; controls bottom
+ result.left = (r + l) / 2 - tw / 2 + lp.leftMargin;
+ result.right = (r + l) / 2 + tw / 2 - lp.rightMargin;
+ result.bottom = b - lp.bottomMargin;
+ result.top = b - th + lp.topMargin;
+ break;
+ case 90:
+ // phone landscape: controls right
+ result.right = r - lp.rightMargin;
+ result.left = r - tw + lp.leftMargin;
+ result.top = (b + t) / 2 - th / 2 + lp.topMargin;
+ result.bottom = (b + t) / 2 + th / 2 - lp.bottomMargin;
+ break;
+ case 180:
+ // phone upside down: controls top
+ result.left = (r + l) / 2 - tw / 2 + lp.leftMargin;
+ result.right = (r + l) / 2 + tw / 2 - lp.rightMargin;
+ result.top = t + lp.topMargin;
+ result.bottom = t + th - lp.bottomMargin;
+ break;
+ case 270:
+ // reverse landscape: controls left
+ result.left = l + lp.leftMargin;
+ result.right = l + tw - lp.rightMargin;
+ result.top = (b + t) / 2 - th / 2 + lp.topMargin;
+ result.bottom = (b + t) / 2 + th / 2 - lp.bottomMargin;
+ break;
+ }
+ v.layout(result.left, result.top, result.right, result.bottom);
+ }
+
+ private void center(View v, Rect other, int rotation) {
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams();
+ int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin;
+ int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin;
+ int cx = (other.left + other.right) / 2;
+ int cy = (other.top + other.bottom) / 2;
+ v.layout(cx - tw / 2 + lp.leftMargin,
+ cy - th / 2 + lp.topMargin,
+ cx + tw / 2 - lp.rightMargin,
+ cy + th / 2 - lp.bottomMargin);
+ }
+
+ private void toLeft(View v, Rect other, int rotation) {
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams();
+ int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin;
+ int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin;
+ int cx = (other.left + other.right) / 2;
+ int cy = (other.top + other.bottom) / 2;
+ int l = 0, r = 0, t = 0, b = 0;
+ switch (rotation) {
+ case 0:
+ // portrait, to left of anchor at bottom
+ l = other.left - tw + lp.leftMargin;
+ r = other.left - lp.rightMargin;
+ t = cy - th / 2 + lp.topMargin;
+ b = cy + th / 2 - lp.bottomMargin;
+ break;
+ case 90:
+ // phone landscape: below anchor on right
+ l = cx - tw / 2 + lp.leftMargin;
+ r = cx + tw / 2 - lp.rightMargin;
+ t = other.bottom + lp.topMargin;
+ b = other.bottom + th - lp.bottomMargin;
+ break;
+ case 180:
+ // phone upside down: right of anchor at top
+ l = other.right + lp.leftMargin;
+ r = other.right + tw - lp.rightMargin;
+ t = cy - th / 2 + lp.topMargin;
+ b = cy + th / 2 - lp.bottomMargin;
+ break;
+ case 270:
+ // reverse landscape: above anchor on left
+ l = cx - tw / 2 + lp.leftMargin;
+ r = cx + tw / 2 - lp.rightMargin;
+ t = other.top - th + lp.topMargin;
+ b = other.top - lp.bottomMargin;
+ break;
+ }
+ v.layout(l, t, r, b);
+ }
+
+ private void toRight(View v, Rect other, int rotation) {
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams();
+ int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin;
+ int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin;
+ int cx = (other.left + other.right) / 2;
+ int cy = (other.top + other.bottom) / 2;
+ int l = 0, r = 0, t = 0, b = 0;
+ switch (rotation) {
+ case 0:
+ l = other.right + lp.leftMargin;
+ r = other.right + tw - lp.rightMargin;
+ t = cy - th / 2 + lp.topMargin;
+ b = cy + th / 2 - lp.bottomMargin;
+ break;
+ case 90:
+ l = cx - tw / 2 + lp.leftMargin;
+ r = cx + tw / 2 - lp.rightMargin;
+ t = other.top - th + lp.topMargin;
+ b = other.top - lp.bottomMargin;
+ break;
+ case 180:
+ l = other.left - tw + lp.leftMargin;
+ r = other.left - lp.rightMargin;
+ t = cy - th / 2 + lp.topMargin;
+ b = cy + th / 2 - lp.bottomMargin;
+ break;
+ case 270:
+ l = cx - tw / 2 + lp.leftMargin;
+ r = cx + tw / 2 - lp.rightMargin;
+ t = other.bottom + lp.topMargin;
+ b = other.bottom + th - lp.bottomMargin;
+ break;
+ }
+ v.layout(l, t, r, b);
+ }
+
+ private void topRight(View v, int l, int t, int r, int b) {
+ // layout using the specific margins; the rotation code messes up the others
+ int mt = getContext().getResources().getDimensionPixelSize(R.dimen.capture_margin_top);
+ int mr = getContext().getResources().getDimensionPixelSize(R.dimen.capture_margin_right);
+ v.layout(r - v.getMeasuredWidth() - mr, t + mt, r - mr, t + mt + v.getMeasuredHeight());
+ }
+
+ private void adjustBackground() {
+ int rotation = getUnifiedRotation();
+ // remove current drawable and reset rotation
+ mBackgroundView.setBackgroundDrawable(null);
+ mBackgroundView.setRotationX(0);
+ mBackgroundView.setRotationY(0);
+ // if the switcher background is top aligned we need to flip the background
+ // drawable vertically; if left aligned, flip horizontally
+ switch (rotation) {
+ case 180:
+ mBackgroundView.setRotationX(180);
+ break;
+ case 270:
+ mBackgroundView.setRotationY(180);
+ break;
+ default:
+ break;
+ }
+ mBackgroundView.setBackgroundResource(R.drawable.switcher_bg);
+ }
+
+}
diff --git a/src/com/android/camera/ui/CameraRootView.java b/src/com/android/camera/ui/CameraRootView.java
new file mode 100644
index 000000000..adda70697
--- /dev/null
+++ b/src/com/android/camera/ui/CameraRootView.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2013 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.camera.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.camera.Util;
+import com.android.gallery3d.common.ApiHelper;
+
+public class CameraRootView extends FrameLayout {
+
+ private int mTopMargin = 0;
+ private int mBottomMargin = 0;
+ private int mLeftMargin = 0;
+ private int mRightMargin = 0;
+ private Rect mCurrentInsets;
+ private int mOffset = 0;
+ private Object mDisplayListener;
+ private MyDisplayListener mListener;
+ public interface MyDisplayListener {
+ public void onDisplayChanged();
+ }
+
+ public CameraRootView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initDisplayListener();
+ setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+
+ @Override
+ protected boolean fitSystemWindows(Rect insets) {
+ super.fitSystemWindows(insets);
+ mCurrentInsets = insets;
+ // insets include status bar, navigation bar, etc
+ // In this case, we are only concerned with the size of nav bar
+ if (mOffset > 0) return true;
+
+ if (insets.bottom > 0) {
+ mOffset = insets.bottom;
+ } else if (insets.right > 0) {
+ mOffset = insets.right;
+ }
+ return true;
+ }
+
+ public void initDisplayListener() {
+ if (ApiHelper.HAS_DISPLAY_LISTENER) {
+ mDisplayListener = new DisplayListener() {
+
+ @Override
+ public void onDisplayAdded(int arg0) {}
+
+ @Override
+ public void onDisplayChanged(int arg0) {
+ mListener.onDisplayChanged();
+ }
+
+ @Override
+ public void onDisplayRemoved(int arg0) {}
+ };
+ }
+ }
+
+ public void setDisplayChangeListener(MyDisplayListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (ApiHelper.HAS_DISPLAY_LISTENER) {
+ ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE))
+ .registerDisplayListener((DisplayListener) mDisplayListener, null);
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow () {
+ super.onDetachedFromWindow();
+ if (ApiHelper.HAS_DISPLAY_LISTENER) {
+ ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE))
+ .unregisterDisplayListener((DisplayListener) mDisplayListener);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int rotation = Util.getDisplayRotation((Activity) getContext());
+ // all the layout code assumes camera device orientation to be portrait
+ // adjust rotation for landscape
+ int orientation = getResources().getConfiguration().orientation;
+ int camOrientation = (rotation % 180 == 0) ? Configuration.ORIENTATION_PORTRAIT
+ : Configuration.ORIENTATION_LANDSCAPE;
+ if (camOrientation != orientation) {
+ rotation = (rotation + 90) % 360;
+ }
+ // calculate margins
+ mLeftMargin = 0;
+ mRightMargin = 0;
+ mBottomMargin = 0;
+ mTopMargin = 0;
+ switch (rotation) {
+ case 0:
+ mBottomMargin += mOffset;
+ break;
+ case 90:
+ mRightMargin += mOffset;
+ break;
+ case 180:
+ mTopMargin += mOffset;
+ break;
+ case 270:
+ mLeftMargin += mOffset;
+ break;
+ }
+ if (mCurrentInsets != null) {
+ if (mCurrentInsets.right > 0) {
+ // navigation bar on the right
+ mRightMargin = mRightMargin > 0 ? mRightMargin : mCurrentInsets.right;
+ } else {
+ // navigation bar on the bottom
+ mBottomMargin = mBottomMargin > 0 ? mBottomMargin : mCurrentInsets.bottom;
+ }
+ }
+ // make sure all the children are resized
+ super.onMeasure(widthMeasureSpec - mLeftMargin - mRightMargin,
+ heightMeasureSpec - mTopMargin - mBottomMargin);
+ setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public void onLayout(boolean changed, int l, int t, int r, int b) {
+ r -= l;
+ b -= t;
+ l = 0;
+ t = 0;
+ int orientation = getResources().getConfiguration().orientation;
+ // Lay out children
+ for (int i = 0; i < getChildCount(); i++) {
+ View v = getChildAt(i);
+ if (v instanceof CameraControls) {
+ // Lay out camera controls to center on the short side of the screen
+ // so that they stay in place during rotation
+ int width = v.getMeasuredWidth();
+ int height = v.getMeasuredHeight();
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ int left = (l + r - width) / 2;
+ v.layout(left, t + mTopMargin, left + width, b - mBottomMargin);
+ } else {
+ int top = (t + b - height) / 2;
+ v.layout(l + mLeftMargin, top, r - mRightMargin, top + height);
+ }
+ } else {
+ v.layout(l + mLeftMargin, t + mTopMargin, r - mRightMargin, b - mBottomMargin);
+ }
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/CameraSwitcher.java b/src/com/android/camera/ui/CameraSwitcher.java
new file mode 100644
index 000000000..6e4321571
--- /dev/null
+++ b/src/com/android/camera/ui/CameraSwitcher.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.LinearLayout;
+
+import com.android.camera.Util;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.UsageStatistics;
+
+public class CameraSwitcher extends RotateImageView
+ implements OnClickListener, OnTouchListener {
+
+ private static final String TAG = "CAM_Switcher";
+ private static final int SWITCHER_POPUP_ANIM_DURATION = 200;
+
+ public static final int PHOTO_MODULE_INDEX = 0;
+ public static final int VIDEO_MODULE_INDEX = 1;
+ public static final int LIGHTCYCLE_MODULE_INDEX = 2;
+ public static final int REFOCUS_MODULE_INDEX = 3;
+ private static final int[] DRAW_IDS = {
+ R.drawable.ic_switch_camera,
+ R.drawable.ic_switch_video,
+ R.drawable.ic_switch_photosphere,
+ R.drawable.ic_switch_refocus
+ };
+ public interface CameraSwitchListener {
+ public void onCameraSelected(int i);
+ public void onShowSwitcherPopup();
+ }
+
+ private CameraSwitchListener mListener;
+ private int mCurrentIndex;
+ private int[] mModuleIds;
+ private int[] mDrawIds;
+ private int mItemSize;
+ private View mPopup;
+ private View mParent;
+ private boolean mShowingPopup;
+ private boolean mNeedsAnimationSetup;
+ private Drawable mIndicator;
+
+ private float mTranslationX = 0;
+ private float mTranslationY = 0;
+
+ private AnimatorListener mHideAnimationListener;
+ private AnimatorListener mShowAnimationListener;
+
+ public CameraSwitcher(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public CameraSwitcher(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ private void init(Context context) {
+ mItemSize = context.getResources().getDimensionPixelSize(R.dimen.switcher_size);
+ setOnClickListener(this);
+ mIndicator = context.getResources().getDrawable(R.drawable.ic_switcher_menu_indicator);
+ initializeDrawables(context);
+ }
+
+ public void initializeDrawables(Context context) {
+ int totaldrawid = (LightCycleHelper.hasLightCycleCapture(context)
+ ? DRAW_IDS.length : DRAW_IDS.length - 1);
+
+ int[] drawids = new int[totaldrawid];
+ int[] moduleids = new int[totaldrawid];
+ int ix = 0;
+ for (int i = 0; i < DRAW_IDS.length; i++) {
+ if (i == LIGHTCYCLE_MODULE_INDEX && !LightCycleHelper.hasLightCycleCapture(context)) {
+ continue; // not enabled, so don't add to UI
+ }
+ moduleids[ix] = i;
+ drawids[ix++] = DRAW_IDS[i];
+ }
+ setIds(moduleids, drawids);
+ }
+
+ public void setIds(int[] moduleids, int[] drawids) {
+ mDrawIds = drawids;
+ mModuleIds = moduleids;
+ }
+
+ public void setCurrentIndex(int i) {
+ mCurrentIndex = i;
+ setImageResource(mDrawIds[i]);
+ }
+
+ public void setSwitchListener(CameraSwitchListener l) {
+ mListener = l;
+ }
+
+ @Override
+ public void onClick(View v) {
+ showSwitcher();
+ mListener.onShowSwitcherPopup();
+ }
+
+ private void onCameraSelected(int ix) {
+ hidePopup();
+ if ((ix != mCurrentIndex) && (mListener != null)) {
+ UsageStatistics.onEvent("CameraModeSwitch", null, null);
+ UsageStatistics.setPendingTransitionCause(
+ UsageStatistics.TRANSITION_MENU_TAP);
+ setCurrentIndex(ix);
+ mListener.onCameraSelected(mModuleIds[ix]);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ mIndicator.setBounds(getDrawable().getBounds());
+ mIndicator.draw(canvas);
+ }
+
+ private void initPopup() {
+ mParent = LayoutInflater.from(getContext()).inflate(R.layout.switcher_popup,
+ (ViewGroup) getParent());
+ LinearLayout content = (LinearLayout) mParent.findViewById(R.id.content);
+ mPopup = content;
+ // Set the gravity of the popup, so that it shows up at the right position
+ // on screen
+ LayoutParams lp = ((LayoutParams) mPopup.getLayoutParams());
+ lp.gravity = ((LayoutParams) mParent.findViewById(R.id.camera_switcher)
+ .getLayoutParams()).gravity;
+ mPopup.setLayoutParams(lp);
+
+ mPopup.setVisibility(View.INVISIBLE);
+ mNeedsAnimationSetup = true;
+ for (int i = mDrawIds.length - 1; i >= 0; i--) {
+ RotateImageView item = new RotateImageView(getContext());
+ item.setImageResource(mDrawIds[i]);
+ item.setBackgroundResource(R.drawable.bg_pressed);
+ final int index = i;
+ item.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (showsPopup()) onCameraSelected(index);
+ }
+ });
+ switch (mDrawIds[i]) {
+ case R.drawable.ic_switch_camera:
+ item.setContentDescription(getContext().getResources().getString(
+ R.string.accessibility_switch_to_camera));
+ break;
+ case R.drawable.ic_switch_video:
+ item.setContentDescription(getContext().getResources().getString(
+ R.string.accessibility_switch_to_video));
+ break;
+ case R.drawable.ic_switch_photosphere:
+ item.setContentDescription(getContext().getResources().getString(
+ R.string.accessibility_switch_to_new_panorama));
+ break;
+ case R.drawable.ic_switch_refocus:
+ item.setContentDescription(getContext().getResources().getString(
+ R.string.accessibility_switch_to_refocus));
+ break;
+ default:
+ break;
+ }
+ content.addView(item, new LinearLayout.LayoutParams(mItemSize, mItemSize));
+ }
+ mPopup.measure(MeasureSpec.makeMeasureSpec(mParent.getWidth(), MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(mParent.getHeight(), MeasureSpec.AT_MOST));
+ }
+
+ public boolean showsPopup() {
+ return mShowingPopup;
+ }
+
+ public boolean isInsidePopup(MotionEvent evt) {
+ if (!showsPopup()) return false;
+ int topLeft[] = new int[2];
+ mPopup.getLocationOnScreen(topLeft);
+ int left = topLeft[0];
+ int top = topLeft[1];
+ int bottom = top + mPopup.getHeight();
+ int right = left + mPopup.getWidth();
+ return evt.getX() >= left && evt.getX() < right
+ && evt.getY() >= top && evt.getY() < bottom;
+ }
+
+ private void hidePopup() {
+ mShowingPopup = false;
+ setVisibility(View.VISIBLE);
+ if (mPopup != null && !animateHidePopup()) {
+ mPopup.setVisibility(View.INVISIBLE);
+ }
+ mParent.setOnTouchListener(null);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration config) {
+ if (showsPopup()) {
+ ((ViewGroup) mParent).removeView(mPopup);
+ mPopup = null;
+ initPopup();
+ mPopup.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void showSwitcher() {
+ mShowingPopup = true;
+ if (mPopup == null) {
+ initPopup();
+ }
+ layoutPopup();
+ mPopup.setVisibility(View.VISIBLE);
+ if (!animateShowPopup()) {
+ setVisibility(View.INVISIBLE);
+ }
+ mParent.setOnTouchListener(this);
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ closePopup();
+ return true;
+ }
+
+ public void closePopup() {
+ if (showsPopup()) {
+ hidePopup();
+ }
+ }
+
+ @Override
+ public void setOrientation(int degree, boolean animate) {
+ super.setOrientation(degree, animate);
+ ViewGroup content = (ViewGroup) mPopup;
+ if (content == null) return;
+ for (int i = 0; i < content.getChildCount(); i++) {
+ RotateImageView iv = (RotateImageView) content.getChildAt(i);
+ iv.setOrientation(degree, animate);
+ }
+ }
+
+ private void layoutPopup() {
+ int orientation = Util.getDisplayRotation((Activity) getContext());
+ int w = mPopup.getMeasuredWidth();
+ int h = mPopup.getMeasuredHeight();
+ if (orientation == 0) {
+ mPopup.layout(getRight() - w, getBottom() - h, getRight(), getBottom());
+ mTranslationX = 0;
+ mTranslationY = h / 3;
+ } else if (orientation == 90) {
+ mTranslationX = w / 3;
+ mTranslationY = - h / 3;
+ mPopup.layout(getRight() - w, getTop(), getRight(), getTop() + h);
+ } else if (orientation == 180) {
+ mTranslationX = - w / 3;
+ mTranslationY = - h / 3;
+ mPopup.layout(getLeft(), getTop(), getLeft() + w, getTop() + h);
+ } else {
+ mTranslationX = - w / 3;
+ mTranslationY = h - getHeight();
+ mPopup.layout(getLeft(), getBottom() - h, getLeft() + w, getBottom());
+ }
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (mPopup != null) {
+ layoutPopup();
+ }
+ }
+
+ private void popupAnimationSetup() {
+ if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) {
+ return;
+ }
+ layoutPopup();
+ mPopup.setScaleX(0.3f);
+ mPopup.setScaleY(0.3f);
+ mPopup.setTranslationX(mTranslationX);
+ mPopup.setTranslationY(mTranslationY);
+ mNeedsAnimationSetup = false;
+ }
+
+ private boolean animateHidePopup() {
+ if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) {
+ return false;
+ }
+ if (mHideAnimationListener == null) {
+ mHideAnimationListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Verify that we weren't canceled
+ if (!showsPopup() && mPopup != null) {
+ mPopup.setVisibility(View.INVISIBLE);
+ ((ViewGroup) mParent).removeView(mPopup);
+ mPopup = null;
+ }
+ }
+ };
+ }
+ mPopup.animate()
+ .alpha(0f)
+ .scaleX(0.3f).scaleY(0.3f)
+ .translationX(mTranslationX)
+ .translationY(mTranslationY)
+ .setDuration(SWITCHER_POPUP_ANIM_DURATION)
+ .setListener(mHideAnimationListener);
+ animate().alpha(1f).setDuration(SWITCHER_POPUP_ANIM_DURATION)
+ .setListener(null);
+ return true;
+ }
+
+ private boolean animateShowPopup() {
+ if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) {
+ return false;
+ }
+ if (mNeedsAnimationSetup) {
+ popupAnimationSetup();
+ }
+ if (mShowAnimationListener == null) {
+ mShowAnimationListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Verify that we weren't canceled
+ if (showsPopup()) {
+ setVisibility(View.INVISIBLE);
+ // request layout to make sure popup is laid out correctly on ICS
+ mPopup.requestLayout();
+ }
+ }
+ };
+ }
+ mPopup.animate()
+ .alpha(1f)
+ .scaleX(1f).scaleY(1f)
+ .translationX(0)
+ .translationY(0)
+ .setDuration(SWITCHER_POPUP_ANIM_DURATION)
+ .setListener(null);
+ animate().alpha(0f).setDuration(SWITCHER_POPUP_ANIM_DURATION)
+ .setListener(mShowAnimationListener);
+ return true;
+ }
+}
diff --git a/src/com/android/camera/ui/CheckedLinearLayout.java b/src/com/android/camera/ui/CheckedLinearLayout.java
new file mode 100644
index 000000000..4e7750499
--- /dev/null
+++ b/src/com/android/camera/ui/CheckedLinearLayout.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.LinearLayout;
+
+public class CheckedLinearLayout extends LinearLayout implements Checkable {
+ private static final int[] CHECKED_STATE_SET = {
+ android.R.attr.state_checked
+ };
+ private boolean mChecked;
+
+ public CheckedLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ if (mChecked != checked) {
+ mChecked = checked;
+ refreshDrawableState();
+ }
+ }
+
+ @Override
+ public void toggle() {
+ setChecked(!mChecked);
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (mChecked) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+}
diff --git a/src/com/android/camera/ui/CountDownView.java b/src/com/android/camera/ui/CountDownView.java
new file mode 100644
index 000000000..907d33508
--- /dev/null
+++ b/src/com/android/camera/ui/CountDownView.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import java.util.Locale;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.SoundPool;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+public class CountDownView extends FrameLayout {
+
+ private static final String TAG = "CAM_CountDownView";
+ private static final int SET_TIMER_TEXT = 1;
+ private TextView mRemainingSecondsView;
+ private int mRemainingSecs = 0;
+ private OnCountDownFinishedListener mListener;
+ private Animation mCountDownAnim;
+ private SoundPool mSoundPool;
+ private int mBeepTwice;
+ private int mBeepOnce;
+ private boolean mPlaySound;
+ private final Handler mHandler = new MainHandler();
+
+ public CountDownView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mCountDownAnim = AnimationUtils.loadAnimation(context, R.anim.count_down_exit);
+ // Load the beeps
+ mSoundPool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0);
+ mBeepOnce = mSoundPool.load(context, R.raw.beep_once, 1);
+ mBeepTwice = mSoundPool.load(context, R.raw.beep_twice, 1);
+ }
+
+ public boolean isCountingDown() {
+ return mRemainingSecs > 0;
+ };
+
+ public interface OnCountDownFinishedListener {
+ public void onCountDownFinished();
+ }
+
+ private void remainingSecondsChanged(int newVal) {
+ mRemainingSecs = newVal;
+ if (newVal == 0) {
+ // Countdown has finished
+ setVisibility(View.INVISIBLE);
+ mListener.onCountDownFinished();
+ } else {
+ Locale locale = getResources().getConfiguration().locale;
+ String localizedValue = String.format(locale, "%d", newVal);
+ mRemainingSecondsView.setText(localizedValue);
+ // Fade-out animation
+ mCountDownAnim.reset();
+ mRemainingSecondsView.clearAnimation();
+ mRemainingSecondsView.startAnimation(mCountDownAnim);
+
+ // Play sound effect for the last 3 seconds of the countdown
+ if (mPlaySound) {
+ if (newVal == 1) {
+ mSoundPool.play(mBeepTwice, 1.0f, 1.0f, 0, 0, 1.0f);
+ } else if (newVal <= 3) {
+ mSoundPool.play(mBeepOnce, 1.0f, 1.0f, 0, 0, 1.0f);
+ }
+ }
+ // Schedule the next remainingSecondsChanged() call in 1 second
+ mHandler.sendEmptyMessageDelayed(SET_TIMER_TEXT, 1000);
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mRemainingSecondsView = (TextView) findViewById(R.id.remaining_seconds);
+ }
+
+ public void setCountDownFinishedListener(OnCountDownFinishedListener listener) {
+ mListener = listener;
+ }
+
+ public void startCountDown(int sec, boolean playSound) {
+ if (sec <= 0) {
+ Log.w(TAG, "Invalid input for countdown timer: " + sec + " seconds");
+ return;
+ }
+ setVisibility(View.VISIBLE);
+ mPlaySound = playSound;
+ remainingSecondsChanged(sec);
+ }
+
+ public void cancelCountDown() {
+ if (mRemainingSecs > 0) {
+ mRemainingSecs = 0;
+ mHandler.removeMessages(SET_TIMER_TEXT);
+ setVisibility(View.INVISIBLE);
+ }
+ }
+
+ private class MainHandler extends Handler {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == SET_TIMER_TEXT) {
+ remainingSecondsChanged(mRemainingSecs -1);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/camera/ui/CountdownTimerPopup.java b/src/com/android/camera/ui/CountdownTimerPopup.java
new file mode 100644
index 000000000..7c3572b55
--- /dev/null
+++ b/src/com/android/camera/ui/CountdownTimerPopup.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.NumberPicker;
+import android.widget.NumberPicker.OnValueChangeListener;
+
+import com.android.camera.ListPreference;
+import com.android.gallery3d.R;
+
+import java.util.Locale;
+
+/**
+ * This is a popup window that allows users to specify a countdown timer
+ */
+
+public class CountdownTimerPopup extends AbstractSettingPopup {
+ private static final String TAG = "TimerSettingPopup";
+ private NumberPicker mNumberSpinner;
+ private String[] mDurations;
+ private ListPreference mTimer;
+ private ListPreference mBeep;
+ private Listener mListener;
+ private Button mConfirmButton;
+ private View mPickerTitle;
+ private CheckBox mTimerSound;
+ private View mSoundTitle;
+
+ static public interface Listener {
+ public void onListPrefChanged(ListPreference pref);
+ }
+
+ public void setSettingChangedListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public CountdownTimerPopup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void initialize(ListPreference timer, ListPreference beep) {
+ mTimer = timer;
+ mBeep = beep;
+ // Set title.
+ mTitle.setText(mTimer.getTitle());
+
+ // Duration
+ CharSequence[] entries = mTimer.getEntryValues();
+ mDurations = new String[entries.length];
+ Locale locale = getResources().getConfiguration().locale;
+ mDurations[0] = getResources().getString(R.string.setting_off); // Off
+ for (int i = 1; i < entries.length; i++)
+ mDurations[i] = String.format(locale, "%d", Integer.parseInt(entries[i].toString()));
+ int durationCount = mDurations.length;
+ mNumberSpinner = (NumberPicker) findViewById(R.id.duration);
+ mNumberSpinner.setMinValue(0);
+ mNumberSpinner.setMaxValue(durationCount - 1);
+ mNumberSpinner.setDisplayedValues(mDurations);
+ mNumberSpinner.setWrapSelectorWheel(false);
+ mNumberSpinner.setOnValueChangedListener(new OnValueChangeListener() {
+ @Override
+ public void onValueChange(NumberPicker picker, int oldValue, int newValue) {
+ setTimeSelectionEnabled(newValue != 0);
+ }
+ });
+ mConfirmButton = (Button) findViewById(R.id.timer_set_button);
+ mPickerTitle = findViewById(R.id.set_time_interval_title);
+
+ // Disable focus on the spinners to prevent keyboard from coming up
+ mNumberSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
+
+ mConfirmButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ updateInputState();
+ }
+ });
+ mTimerSound = (CheckBox) findViewById(R.id.sound_check_box);
+ mSoundTitle = findViewById(R.id.beep_title);
+ }
+
+ private void restoreSetting() {
+ int index = mTimer.findIndexOfValue(mTimer.getValue());
+ if (index == -1) {
+ Log.e(TAG, "Invalid preference value.");
+ mTimer.print();
+ throw new IllegalArgumentException();
+ } else {
+ setTimeSelectionEnabled(index != 0);
+ mNumberSpinner.setValue(index);
+ }
+ boolean checked = mBeep.findIndexOfValue(mBeep.getValue()) != 0;
+ mTimerSound.setChecked(checked);
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ if (visibility == View.VISIBLE) {
+ if (getVisibility() != View.VISIBLE) {
+ // Set the number pickers and on/off switch to be consistent
+ // with the preference
+ restoreSetting();
+ }
+ }
+ super.setVisibility(visibility);
+ }
+
+ protected void setTimeSelectionEnabled(boolean enabled) {
+ mPickerTitle.setVisibility(enabled ? VISIBLE : INVISIBLE);
+ mTimerSound.setEnabled(enabled);
+ mSoundTitle.setEnabled(enabled);
+ }
+
+ @Override
+ public void reloadPreference() {
+ }
+
+ private void updateInputState() {
+ mTimer.setValueIndex(mNumberSpinner.getValue());
+ mBeep.setValueIndex(mTimerSound.isChecked() ? 1 : 0);
+ if (mListener != null) {
+ mListener.onListPrefChanged(mTimer);
+ mListener.onListPrefChanged(mBeep);
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/EffectSettingPopup.java b/src/com/android/camera/ui/EffectSettingPopup.java
new file mode 100644
index 000000000..568781a01
--- /dev/null
+++ b/src/com/android/camera/ui/EffectSettingPopup.java
@@ -0,0 +1,214 @@
+/*
+ * 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.camera.ui;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.GridView;
+import android.widget.SimpleAdapter;
+
+import com.android.camera.IconListPreference;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+// A popup window that shows video effect setting. It has two grid view.
+// One shows the goofy face effects. The other shows the background replacer
+// effects.
+public class EffectSettingPopup extends AbstractSettingPopup implements
+ AdapterView.OnItemClickListener, View.OnClickListener {
+ private static final String TAG = "EffectSettingPopup";
+ private String mNoEffect;
+ private IconListPreference mPreference;
+ private Listener mListener;
+ private View mClearEffects;
+ private GridView mSillyFacesGrid;
+ private GridView mBackgroundGrid;
+
+ // Data for silly face items. (text, image, and preference value)
+ ArrayList<HashMap<String, Object>> mSillyFacesItem =
+ new ArrayList<HashMap<String, Object>>();
+
+ // Data for background replacer items. (text, image, and preference value)
+ ArrayList<HashMap<String, Object>> mBackgroundItem =
+ new ArrayList<HashMap<String, Object>>();
+
+
+ static public interface Listener {
+ public void onSettingChanged();
+ }
+
+ public EffectSettingPopup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mNoEffect = context.getString(R.string.pref_video_effect_default);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mClearEffects = findViewById(R.id.clear_effects);
+ mClearEffects.setOnClickListener(this);
+ mSillyFacesGrid = (GridView) findViewById(R.id.effect_silly_faces);
+ mBackgroundGrid = (GridView) findViewById(R.id.effect_background);
+ }
+
+ public void initialize(IconListPreference preference) {
+ mPreference = preference;
+ Context context = getContext();
+ CharSequence[] entries = mPreference.getEntries();
+ CharSequence[] entryValues = mPreference.getEntryValues();
+ int[] iconIds = mPreference.getImageIds();
+ if (iconIds == null) {
+ iconIds = mPreference.getLargeIconIds();
+ }
+
+ // Set title.
+ mTitle.setText(mPreference.getTitle());
+
+ for(int i = 0; i < entries.length; ++i) {
+ String value = entryValues[i].toString();
+ if (value.equals(mNoEffect)) continue; // no effect, skip it.
+ HashMap<String, Object> map = new HashMap<String, Object>();
+ map.put("value", value);
+ map.put("text", entries[i].toString());
+ if (iconIds != null) map.put("image", iconIds[i]);
+ if (value.startsWith("goofy_face")) {
+ mSillyFacesItem.add(map);
+ } else if (value.startsWith("backdropper")) {
+ mBackgroundItem.add(map);
+ }
+ }
+
+ boolean hasSillyFaces = mSillyFacesItem.size() > 0;
+ boolean hasBackground = mBackgroundItem.size() > 0;
+
+ // Initialize goofy face if it is supported.
+ if (hasSillyFaces) {
+ findViewById(R.id.effect_silly_faces_title).setVisibility(View.VISIBLE);
+ findViewById(R.id.effect_silly_faces_title_separator).setVisibility(View.VISIBLE);
+ mSillyFacesGrid.setVisibility(View.VISIBLE);
+ SimpleAdapter sillyFacesItemAdapter = new SimpleAdapter(context,
+ mSillyFacesItem, R.layout.effect_setting_item,
+ new String[] {"text", "image"},
+ new int[] {R.id.text, R.id.image});
+ mSillyFacesGrid.setAdapter(sillyFacesItemAdapter);
+ mSillyFacesGrid.setOnItemClickListener(this);
+ }
+
+ if (hasSillyFaces && hasBackground) {
+ findViewById(R.id.effect_background_separator).setVisibility(View.VISIBLE);
+ }
+
+ // Initialize background replacer if it is supported.
+ if (hasBackground) {
+ findViewById(R.id.effect_background_title).setVisibility(View.VISIBLE);
+ findViewById(R.id.effect_background_title_separator).setVisibility(View.VISIBLE);
+ mBackgroundGrid.setVisibility(View.VISIBLE);
+ SimpleAdapter backgroundItemAdapter = new SimpleAdapter(context,
+ mBackgroundItem, R.layout.effect_setting_item,
+ new String[] {"text", "image"},
+ new int[] {R.id.text, R.id.image});
+ mBackgroundGrid.setAdapter(backgroundItemAdapter);
+ mBackgroundGrid.setOnItemClickListener(this);
+ }
+
+ reloadPreference();
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ if (visibility == View.VISIBLE) {
+ if (getVisibility() != View.VISIBLE) {
+ // Do not show or hide "Clear effects" button when the popup
+ // is already visible. Otherwise it looks strange.
+ boolean noEffect = mPreference.getValue().equals(mNoEffect);
+ mClearEffects.setVisibility(noEffect ? View.GONE : View.VISIBLE);
+ }
+ reloadPreference();
+ }
+ super.setVisibility(visibility);
+ }
+
+ // The value of the preference may have changed. Update the UI.
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ @Override
+ public void reloadPreference() {
+ mBackgroundGrid.setItemChecked(mBackgroundGrid.getCheckedItemPosition(), false);
+ mSillyFacesGrid.setItemChecked(mSillyFacesGrid.getCheckedItemPosition(), false);
+
+ String value = mPreference.getValue();
+ if (value.equals(mNoEffect)) return;
+
+ for (int i = 0; i < mSillyFacesItem.size(); i++) {
+ if (value.equals(mSillyFacesItem.get(i).get("value"))) {
+ mSillyFacesGrid.setItemChecked(i, true);
+ return;
+ }
+ }
+
+ for (int i = 0; i < mBackgroundItem.size(); i++) {
+ if (value.equals(mBackgroundItem.get(i).get("value"))) {
+ mBackgroundGrid.setItemChecked(i, true);
+ return;
+ }
+ }
+
+ Log.e(TAG, "Invalid preference value: " + value);
+ mPreference.print();
+ }
+
+ public void setSettingChangedListener(Listener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view,
+ int index, long id) {
+ String value;
+ if (parent == mSillyFacesGrid) {
+ value = (String) mSillyFacesItem.get(index).get("value");
+ } else if (parent == mBackgroundGrid) {
+ value = (String) mBackgroundItem.get(index).get("value");
+ } else {
+ return;
+ }
+
+ // Tapping the selected effect will deselect it (clear effects).
+ if (value.equals(mPreference.getValue())) {
+ mPreference.setValue(mNoEffect);
+ } else {
+ mPreference.setValue(value);
+ }
+ reloadPreference();
+ if (mListener != null) mListener.onSettingChanged();
+ }
+
+ @Override
+ public void onClick(View v) {
+ // Clear the effect.
+ mPreference.setValue(mNoEffect);
+ reloadPreference();
+ if (mListener != null) mListener.onSettingChanged();
+ }
+}
diff --git a/src/com/android/camera/ui/ExpandedGridView.java b/src/com/android/camera/ui/ExpandedGridView.java
new file mode 100644
index 000000000..13cf58f34
--- /dev/null
+++ b/src/com/android/camera/ui/ExpandedGridView.java
@@ -0,0 +1,36 @@
+/*
+ * 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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.GridView;
+
+public class ExpandedGridView extends GridView {
+ public ExpandedGridView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // If UNSPECIFIED is passed to GridView, it will show only one row.
+ // Here GridView is put in a ScrollView, so pass it a very big size with
+ // AT_MOST to show all the rows.
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(65536, MeasureSpec.AT_MOST);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
diff --git a/src/com/android/camera/ui/FaceView.java b/src/com/android/camera/ui/FaceView.java
new file mode 100644
index 000000000..7d66dc079
--- /dev/null
+++ b/src/com/android/camera/ui/FaceView.java
@@ -0,0 +1,226 @@
+/*
+ * 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.camera.ui;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.RectF;
+import android.hardware.Camera.Face;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+import com.android.camera.PhotoUI;
+import com.android.camera.Util;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+@TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+public class FaceView extends View
+ implements FocusIndicator, Rotatable,
+ PhotoUI.SurfaceTextureSizeChangedListener {
+ private static final String TAG = "CAM FaceView";
+ private final boolean LOGV = false;
+ // The value for android.hardware.Camera.setDisplayOrientation.
+ private int mDisplayOrientation;
+ // The orientation compensation for the face indicator to make it look
+ // correctly in all device orientations. Ex: if the value is 90, the
+ // indicator should be rotated 90 degrees counter-clockwise.
+ private int mOrientation;
+ private boolean mMirror;
+ private boolean mPause;
+ private Matrix mMatrix = new Matrix();
+ private RectF mRect = new RectF();
+ // As face detection can be flaky, we add a layer of filtering on top of it
+ // to avoid rapid changes in state (eg, flickering between has faces and
+ // not having faces)
+ private Face[] mFaces;
+ private Face[] mPendingFaces;
+ private int mColor;
+ private final int mFocusingColor;
+ private final int mFocusedColor;
+ private final int mFailColor;
+ private Paint mPaint;
+ private volatile boolean mBlocked;
+
+ private int mUncroppedWidth;
+ private int mUncroppedHeight;
+ private static final int MSG_SWITCH_FACES = 1;
+ private static final int SWITCH_DELAY = 70;
+ private boolean mStateSwitchPending = false;
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_SWITCH_FACES:
+ mStateSwitchPending = false;
+ mFaces = mPendingFaces;
+ invalidate();
+ break;
+ }
+ }
+ };
+
+ public FaceView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ Resources res = getResources();
+ mFocusingColor = res.getColor(R.color.face_detect_start);
+ mFocusedColor = res.getColor(R.color.face_detect_success);
+ mFailColor = res.getColor(R.color.face_detect_fail);
+ mColor = mFocusingColor;
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setStyle(Style.STROKE);
+ mPaint.setStrokeWidth(res.getDimension(R.dimen.face_circle_stroke));
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(int uncroppedWidth, int uncroppedHeight) {
+ mUncroppedWidth = uncroppedWidth;
+ mUncroppedHeight = uncroppedHeight;
+ }
+
+ public void setFaces(Face[] faces) {
+ if (LOGV) Log.v(TAG, "Num of faces=" + faces.length);
+ if (mPause) return;
+ if (mFaces != null) {
+ if ((faces.length > 0 && mFaces.length == 0)
+ || (faces.length == 0 && mFaces.length > 0)) {
+ mPendingFaces = faces;
+ if (!mStateSwitchPending) {
+ mStateSwitchPending = true;
+ mHandler.sendEmptyMessageDelayed(MSG_SWITCH_FACES, SWITCH_DELAY);
+ }
+ return;
+ }
+ }
+ if (mStateSwitchPending) {
+ mStateSwitchPending = false;
+ mHandler.removeMessages(MSG_SWITCH_FACES);
+ }
+ mFaces = faces;
+ invalidate();
+ }
+
+ public void setDisplayOrientation(int orientation) {
+ mDisplayOrientation = orientation;
+ if (LOGV) Log.v(TAG, "mDisplayOrientation=" + orientation);
+ }
+
+ @Override
+ public void setOrientation(int orientation, boolean animation) {
+ mOrientation = orientation;
+ invalidate();
+ }
+
+ public void setMirror(boolean mirror) {
+ mMirror = mirror;
+ if (LOGV) Log.v(TAG, "mMirror=" + mirror);
+ }
+
+ public boolean faceExists() {
+ return (mFaces != null && mFaces.length > 0);
+ }
+
+ @Override
+ public void showStart() {
+ mColor = mFocusingColor;
+ invalidate();
+ }
+
+ // Ignore the parameter. No autofocus animation for face detection.
+ @Override
+ public void showSuccess(boolean timeout) {
+ mColor = mFocusedColor;
+ invalidate();
+ }
+
+ // Ignore the parameter. No autofocus animation for face detection.
+ @Override
+ public void showFail(boolean timeout) {
+ mColor = mFailColor;
+ invalidate();
+ }
+
+ @Override
+ public void clear() {
+ // Face indicator is displayed during preview. Do not clear the
+ // drawable.
+ mColor = mFocusingColor;
+ mFaces = null;
+ invalidate();
+ }
+
+ public void pause() {
+ mPause = true;
+ }
+
+ public void resume() {
+ mPause = false;
+ }
+
+ public void setBlockDraw(boolean block) {
+ mBlocked = block;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (!mBlocked && (mFaces != null) && (mFaces.length > 0)) {
+ int rw, rh;
+ rw = mUncroppedWidth;
+ rh = mUncroppedHeight;
+ // Prepare the matrix.
+ if (((rh > rw) && ((mDisplayOrientation == 0) || (mDisplayOrientation == 180)))
+ || ((rw > rh) && ((mDisplayOrientation == 90) || (mDisplayOrientation == 270)))) {
+ int temp = rw;
+ rw = rh;
+ rh = temp;
+ }
+ Util.prepareMatrix(mMatrix, mMirror, mDisplayOrientation, rw, rh);
+ int dx = (getWidth() - rw) / 2;
+ int dy = (getHeight() - rh) / 2;
+
+ // Focus indicator is directional. Rotate the matrix and the canvas
+ // so it looks correctly in all orientations.
+ canvas.save();
+ mMatrix.postRotate(mOrientation); // postRotate is clockwise
+ canvas.rotate(-mOrientation); // rotate is counter-clockwise (for canvas)
+ for (int i = 0; i < mFaces.length; i++) {
+ // Filter out false positives.
+ if (mFaces[i].score < 50) continue;
+
+ // Transform the coordinates.
+ mRect.set(mFaces[i].rect);
+ if (LOGV) Util.dumpRect(mRect, "Original rect");
+ mMatrix.mapRect(mRect);
+ if (LOGV) Util.dumpRect(mRect, "Transformed rect");
+ mPaint.setColor(mColor);
+ mRect.offset(dx, dy);
+ canvas.drawOval(mRect, mPaint);
+ }
+ canvas.restore();
+ }
+ super.onDraw(canvas);
+ }
+}
diff --git a/src/com/android/camera/ui/FilmStripGestureRecognizer.java b/src/com/android/camera/ui/FilmStripGestureRecognizer.java
new file mode 100644
index 000000000..f870b5829
--- /dev/null
+++ b/src/com/android/camera/ui/FilmStripGestureRecognizer.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2013 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.camera.ui;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+// This class aggregates three gesture detectors: GestureDetector,
+// ScaleGestureDetector.
+public class FilmStripGestureRecognizer {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilmStripGestureRecognizer";
+
+ public interface Listener {
+ boolean onSingleTapUp(float x, float y);
+ boolean onDoubleTap(float x, float y);
+ boolean onScroll(float x, float y, float dx, float dy);
+ boolean onFling(float velocityX, float velocityY);
+ boolean onScaleBegin(float focusX, float focusY);
+ boolean onScale(float focusX, float focusY, float scale);
+ boolean onDown(float x, float y);
+ boolean onUp(float x, float y);
+ void onScaleEnd();
+ }
+
+ private final GestureDetector mGestureDetector;
+ private final ScaleGestureDetector mScaleDetector;
+ private final Listener mListener;
+
+ public FilmStripGestureRecognizer(Context context, Listener listener) {
+ mListener = listener;
+ mGestureDetector = new GestureDetector(context, new MyGestureListener(),
+ null, true /* ignoreMultitouch */);
+ mScaleDetector = new ScaleGestureDetector(
+ context, new MyScaleListener());
+ }
+
+ public void onTouchEvent(MotionEvent event) {
+ mGestureDetector.onTouchEvent(event);
+ mScaleDetector.onTouchEvent(event);
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ mListener.onUp(event.getX(), event.getY());
+ }
+ }
+
+ private class MyGestureListener
+ extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return mListener.onSingleTapUp(e.getX(), e.getY());
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ return mListener.onDoubleTap(e.getX(), e.getY());
+ }
+
+ @Override
+ public boolean onScroll(
+ MotionEvent e1, MotionEvent e2, float dx, float dy) {
+ return mListener.onScroll(e2.getX(), e2.getY(), dx, dy);
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ return mListener.onFling(velocityX, velocityY);
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ mListener.onDown(e.getX(), e.getY());
+ return super.onDown(e);
+ }
+ }
+
+ private class MyScaleListener
+ extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ return mListener.onScaleBegin(
+ detector.getFocusX(), detector.getFocusY());
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ return mListener.onScale(detector.getFocusX(),
+ detector.getFocusY(), detector.getScaleFactor());
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mListener.onScaleEnd();
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java
new file mode 100644
index 000000000..8a1a85a55
--- /dev/null
+++ b/src/com/android/camera/ui/FilmStripView.java
@@ -0,0 +1,1720 @@
+/*
+ * Copyright (C) 2013 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.camera.ui;
+
+import android.animation.Animator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.Scroller;
+
+import com.android.camera.ui.FilmStripView.ImageData.PanoramaSupportCallback;
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper;
+
+public class FilmStripView extends ViewGroup {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilmStripView";
+
+ private static final int BUFFER_SIZE = 5;
+ private static final int DURATION_GEOMETRY_ADJUST = 200;
+ private static final float FILM_STRIP_SCALE = 0.6f;
+ private static final float FULLSCREEN_SCALE = 1f;
+ // Only check for intercepting touch events within first 500ms
+ private static final int SWIPE_TIME_OUT = 500;
+
+ private Context mContext;
+ private FilmStripGestureRecognizer mGestureRecognizer;
+ private DataAdapter mDataAdapter;
+ private int mViewGap;
+ private final Rect mDrawArea = new Rect();
+
+ private final int mCurrentInfo = (BUFFER_SIZE - 1) / 2;
+ private float mScale;
+ private MyController mController;
+ private int mCenterX = -1;
+ private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE];
+
+ private Listener mListener;
+
+ private MotionEvent mDown;
+ private boolean mCheckToIntercept = true;
+ private View mCameraView;
+ private int mSlop;
+ private TimeInterpolator mViewAnimInterpolator;
+
+ private ImageButton mViewPhotoSphereButton;
+ private PanoramaViewHelper mPanoramaViewHelper;
+ private long mLastItemId = -1;
+
+ // This is used to resolve the misalignment problem when the device
+ // orientation is changed. If the current item is in fullscreen, it might
+ // be shifted because mCenterX is not adjusted with the orientation.
+ // Set this to true when onSizeChanged is called to make sure we adjust
+ // mCenterX accordingly.
+ private boolean mAnchorPending;
+
+ /**
+ * Common interface for all images in the filmstrip.
+ */
+ public interface ImageData {
+ /**
+ * Interface that is used to tell the caller whether an image is a photo
+ * sphere.
+ */
+ public static interface PanoramaSupportCallback {
+ /**
+ * Called then photo sphere info has been loaded.
+ *
+ * @param isPanorama whether the image is a valid photo sphere
+ * @param isPanorama360 whether the photo sphere is a full 360
+ * degree horizontal panorama
+ */
+ void panoramaInfoAvailable(boolean isPanorama,
+ boolean isPanorama360);
+ }
+
+ // Image data types.
+ public static final int TYPE_NONE = 0;
+ public static final int TYPE_CAMERA_PREVIEW = 1;
+ public static final int TYPE_PHOTO = 2;
+ public static final int TYPE_VIDEO = 3;
+
+ // Actions allowed to be performed on the image data.
+ // The actions are defined bit-wise so we can use bit operations like
+ // | and &.
+ public static final int ACTION_NONE = 0;
+ public static final int ACTION_PROMOTE = 1;
+ public static final int ACTION_DEMOTE = (1 << 1);
+
+ /**
+ * SIZE_FULL can be returned by {@link ImageData#getWidth()} and
+ * {@link ImageData#getHeight()}.
+ * When SIZE_FULL is returned for width/height, it means the the
+ * width or height will be disregarded when deciding the view size
+ * of this ImageData, just use full screen size.
+ */
+ public static final int SIZE_FULL = -2;
+
+ /**
+ * Returns the width of the image. The final layout of the view returned
+ * by {@link DataAdapter#getView(android.content.Context, int)} will
+ * preserve the aspect ratio of
+ * {@link com.android.camera.ui.FilmStripView.ImageData#getWidth()} and
+ * {@link com.android.camera.ui.FilmStripView.ImageData#getHeight()}.
+ */
+ public int getWidth();
+
+
+ /**
+ * Returns the width of the image. The final layout of the view returned
+ * by {@link DataAdapter#getView(android.content.Context, int)} will
+ * preserve the aspect ratio of
+ * {@link com.android.camera.ui.FilmStripView.ImageData#getWidth()} and
+ * {@link com.android.camera.ui.FilmStripView.ImageData#getHeight()}.
+ */
+ public int getHeight();
+
+ /** Returns the image data type */
+ public int getType();
+
+ /**
+ * Checks if the UI action is supported.
+ *
+ * @param action The UI actions to check.
+ * @return {@code false} if at least one of the actions is not
+ * supported. {@code true} otherwise.
+ */
+ public boolean isUIActionSupported(int action);
+
+ /**
+ * Gives the data a hint when its view is going to be displayed.
+ * {@code FilmStripView} should always call this function before
+ * showing its corresponding view every time.
+ */
+ public void prepare();
+
+ /**
+ * Gives the data a hint when its view is going to be removed from the
+ * view hierarchy. {@code FilmStripView} should always call this
+ * function after its corresponding view is removed from the view
+ * hierarchy.
+ */
+ public void recycle();
+
+ /**
+ * Asynchronously checks if the image is a photo sphere. Notified the
+ * callback when the results are available.
+ */
+ public void isPhotoSphere(Context context, PanoramaSupportCallback callback);
+
+ /**
+ * If the item is a valid photo sphere panorama, this method will launch
+ * the viewer.
+ */
+ public void viewPhotoSphere(PanoramaViewHelper helper);
+ }
+
+ /**
+ * An interfaces which defines the interactions between the
+ * {@link ImageData} and the {@link FilmStripView}.
+ */
+ public interface DataAdapter {
+ /**
+ * An interface which defines the update report used to return to
+ * the {@link com.android.camera.ui.FilmStripView.Listener}.
+ */
+ public interface UpdateReporter {
+ /** Checks if the data of dataID is removed. */
+ public boolean isDataRemoved(int dataID);
+
+ /** Checks if the data of dataID is updated. */
+ public boolean isDataUpdated(int dataID);
+ }
+
+ /**
+ * An interface which defines the listener for UI actions over
+ * {@link ImageData}.
+ */
+ public interface Listener {
+ // Called when the whole data loading is done. No any assumption
+ // on previous data.
+ public void onDataLoaded();
+
+ // Only some of the data is changed. The listener should check
+ // if any thing needs to be updated.
+ public void onDataUpdated(UpdateReporter reporter);
+
+ public void onDataInserted(int dataID, ImageData data);
+
+ public void onDataRemoved(int dataID, ImageData data);
+ }
+
+ /** Returns the total number of image data */
+ public int getTotalNumber();
+
+ /**
+ * Returns the view to visually present the image data.
+ *
+ * @param context The {@link Context} to create the view.
+ * @param dataID The ID of the image data to be presented.
+ * @return The view representing the image data. Null if
+ * unavailable or the {@code dataID} is out of range.
+ */
+ public View getView(Context context, int dataID);
+
+ /**
+ * Returns the {@link ImageData} specified by the ID.
+ *
+ * @param dataID The ID of the {@link ImageData}.
+ * @return The specified {@link ImageData}. Null if not available.
+ */
+ public ImageData getImageData(int dataID);
+
+ /**
+ * Suggests the data adapter the maximum possible size of the layout
+ * so the {@link DataAdapter} can optimize the view returned for the
+ * {@link ImageData}.
+ *
+ * @param w Maximum width.
+ * @param h Maximum height.
+ */
+ public void suggestViewSizeBound(int w, int h);
+
+ /**
+ * Sets the listener for FilmStripView UI actions over the ImageData.
+ *
+ * @param listener The listener to use.
+ */
+ public void setListener(Listener listener);
+
+ /**
+ * The callback when the item enters/leaves full-screen.
+ * TODO: Call this function actually.
+ *
+ * @param dataID The ID of the image data.
+ * @param fullScreen {@code true} if the data is entering full-screen.
+ * {@code false} otherwise.
+ */
+ public void onDataFullScreen(int dataID, boolean fullScreen);
+
+ /**
+ * The callback when the item is centered/off-centered.
+ * TODO: Calls this function actually.
+ *
+ * @param dataID The ID of the image data.
+ * @param centered {@code true} if the data is centered.
+ * {@code false} otherwise.
+ */
+ public void onDataCentered(int dataID, boolean centered);
+
+ /**
+ * Returns {@code true} if the view of the data can be moved by swipe
+ * gesture when in full-screen.
+ *
+ * @param dataID The ID of the data.
+ * @return {@code true} if the view can be moved,
+ * {@code false} otherwise.
+ */
+ public boolean canSwipeInFullScreen(int dataID);
+ }
+
+ /**
+ * An interface which defines the FilmStripView UI action listener.
+ */
+ public interface Listener {
+ /**
+ * Callback when the data is promoted.
+ *
+ * @param dataID The ID of the promoted data.
+ */
+ public void onDataPromoted(int dataID);
+
+ /**
+ * Callback when the data is demoted.
+ *
+ * @param dataID The ID of the demoted data.
+ */
+ public void onDataDemoted(int dataID);
+
+ public void onDataFullScreenChange(int dataID, boolean full);
+
+ /**
+ * Callback when entering/leaving camera mode.
+ *
+ * @param toCamera {@code true} if entering camera mode. Otherwise,
+ * {@code false}
+ */
+ public void onSwitchMode(boolean toCamera);
+ }
+
+ /**
+ * An interface which defines the controller of {@link FilmStripView}.
+ */
+ public interface Controller {
+ public boolean isScalling();
+
+ public void scroll(float deltaX);
+
+ public void fling(float velocity);
+
+ public void scrollTo(int position, int duration, boolean interruptible);
+
+ public boolean stopScrolling();
+
+ public boolean isScrolling();
+
+ public void lockAtCurrentView();
+
+ public void unlockPosition();
+
+ public void gotoCameraFullScreen();
+
+ public void gotoFilmStrip();
+
+ public void gotoFullScreen();
+ }
+
+ /**
+ * A helper class to tract and calculate the view coordination.
+ */
+ private static class ViewInfo {
+ private int mDataID;
+ /** The position of the left of the view in the whole filmstrip. */
+ private int mLeftPosition;
+ private View mView;
+ private RectF mViewArea;
+
+ public ViewInfo(int id, View v) {
+ v.setPivotX(0f);
+ v.setPivotY(0f);
+ mDataID = id;
+ mView = v;
+ mLeftPosition = -1;
+ mViewArea = new RectF();
+ }
+
+ public int getID() {
+ return mDataID;
+ }
+
+ public void setID(int id) {
+ mDataID = id;
+ }
+
+ public void setLeftPosition(int pos) {
+ mLeftPosition = pos;
+ }
+
+ public int getLeftPosition() {
+ return mLeftPosition;
+ }
+
+ public float getTranslationY(float scale) {
+ return mView.getTranslationY() / scale;
+ }
+
+ public float getTranslationX(float scale) {
+ return mView.getTranslationX();
+ }
+
+ public void setTranslationY(float transY, float scale) {
+ mView.setTranslationY(transY * scale);
+ }
+
+ public void setTranslationX(float transX, float scale) {
+ mView.setTranslationX(transX * scale);
+ }
+
+ public void translateXBy(float transX, float scale) {
+ mView.setTranslationX(mView.getTranslationX() + transX * scale);
+ }
+
+ public int getCenterX() {
+ return mLeftPosition + mView.getWidth() / 2;
+ }
+
+ public View getView() {
+ return mView;
+ }
+
+ private void layoutAt(int left, int top) {
+ mView.layout(left, top, left + mView.getMeasuredWidth(),
+ top + mView.getMeasuredHeight());
+ }
+
+ public void layoutIn(Rect drawArea, int refCenter, float scale) {
+ // drawArea is where to layout in.
+ // refCenter is the absolute horizontal position of the center of
+ // drawArea.
+ int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter) * scale);
+ int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale);
+ layoutAt(left, top);
+ mView.setScaleX(scale);
+ mView.setScaleY(scale);
+
+ // update mViewArea for touch detection.
+ int l = mView.getLeft();
+ int t = mView.getTop();
+ mViewArea.set(l, t,
+ l + mView.getWidth() * scale,
+ t + mView.getHeight() * scale);
+ }
+
+ public boolean areaContains(float x, float y) {
+ return mViewArea.contains(x, y);
+ }
+ }
+
+ /** Constructor. */
+ public FilmStripView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ /** Constructor. */
+ public FilmStripView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ /** Constructor. */
+ public FilmStripView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context);
+ }
+
+ private void init(Context context) {
+ // This is for positioning camera controller at the same place in
+ // different orientations.
+ setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
+
+ setWillNotDraw(false);
+ mContext = context;
+ mScale = 1.0f;
+ mController = new MyController(context);
+ mViewAnimInterpolator = new DecelerateInterpolator();
+ mGestureRecognizer =
+ new FilmStripGestureRecognizer(context, new MyGestureReceiver());
+ mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop);
+ }
+
+ /**
+ * Returns the controller.
+ *
+ * @return The {@code Controller}.
+ */
+ public Controller getController() {
+ return mController;
+ }
+
+ public void setListener(Listener l) {
+ mListener = l;
+ }
+
+ public void setViewGap(int viewGap) {
+ mViewGap = viewGap;
+ }
+
+ /**
+ * Sets the helper that's to be used to open photo sphere panoramas.
+ */
+ public void setPanoramaViewHelper(PanoramaViewHelper helper) {
+ mPanoramaViewHelper = helper;
+ }
+
+ public float getScale() {
+ return mScale;
+ }
+
+ public boolean isAnchoredTo(int id) {
+ if (mViewInfo[mCurrentInfo].getID() == id
+ && mViewInfo[mCurrentInfo].getCenterX() == mCenterX) {
+ return true;
+ }
+ return false;
+ }
+
+ public int getCurrentType() {
+ if (mDataAdapter == null) {
+ return ImageData.TYPE_NONE;
+ }
+ ViewInfo curr = mViewInfo[mCurrentInfo];
+ if (curr == null) {
+ return ImageData.TYPE_NONE;
+ }
+ return mDataAdapter.getImageData(curr.getID()).getType();
+ }
+
+ @Override
+ public void onDraw(Canvas c) {
+ if (mController.hasNewGeometry()) {
+ layoutChildren();
+ }
+ }
+
+ /** Returns [width, height] preserving image aspect ratio. */
+ private int[] calculateChildDimension(
+ int imageWidth, int imageHeight,
+ int boundWidth, int boundHeight) {
+
+ if (imageWidth == ImageData.SIZE_FULL
+ || imageHeight == ImageData.SIZE_FULL) {
+ imageWidth = boundWidth;
+ imageHeight = boundHeight;
+ }
+
+ int[] ret = new int[2];
+ ret[0] = boundWidth;
+ ret[1] = boundHeight;
+
+ if (imageWidth * ret[1] > ret[0] * imageHeight) {
+ ret[1] = imageHeight * ret[0] / imageWidth;
+ } else {
+ ret[0] = imageWidth * ret[1] / imageHeight;
+ }
+
+ return ret;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int boundWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int boundHeight = MeasureSpec.getSize(heightMeasureSpec);
+ if (boundWidth == 0 || boundHeight == 0) {
+ // Either width or height is unknown, can't measure children yet.
+ return;
+ }
+
+ if (mDataAdapter != null) {
+ mDataAdapter.suggestViewSizeBound(boundWidth / 2, boundHeight / 2);
+ }
+
+ for (ViewInfo info : mViewInfo) {
+ if (info == null) continue;
+
+ int id = info.getID();
+ int[] dim = calculateChildDimension(
+ mDataAdapter.getImageData(id).getWidth(),
+ mDataAdapter.getImageData(id).getHeight(),
+ boundWidth, boundHeight);
+
+ info.getView().measure(
+ MeasureSpec.makeMeasureSpec(
+ dim[0], MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(
+ dim[1], MeasureSpec.EXACTLY));
+ }
+ }
+
+ @Override
+ protected boolean fitSystemWindows(Rect insets) {
+ if (mViewPhotoSphereButton != null) {
+ // Set the position of the "View Photo Sphere" button.
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mViewPhotoSphereButton
+ .getLayoutParams();
+ params.bottomMargin = insets.bottom;
+ mViewPhotoSphereButton.setLayoutParams(params);
+ }
+
+ return super.fitSystemWindows(insets);
+ }
+
+ private int findTheNearestView(int pointX) {
+
+ int nearest = 0;
+ // Find the first non-null ViewInfo.
+ while (nearest < BUFFER_SIZE
+ && (mViewInfo[nearest] == null || mViewInfo[nearest].getLeftPosition() == -1)) {
+ nearest++;
+ }
+ // No existing available ViewInfo
+ if (nearest == BUFFER_SIZE) {
+ return -1;
+ }
+ int min = Math.abs(pointX - mViewInfo[nearest].getCenterX());
+
+ for (int infoID = nearest + 1; infoID < BUFFER_SIZE && mViewInfo[infoID] != null; infoID++) {
+ // Not measured yet.
+ if (mViewInfo[infoID].getLeftPosition() == -1)
+ continue;
+
+ int c = mViewInfo[infoID].getCenterX();
+ int dist = Math.abs(pointX - c);
+ if (dist < min) {
+ min = dist;
+ nearest = infoID;
+ }
+ }
+ return nearest;
+ }
+
+ private ViewInfo buildInfoFromData(int dataID) {
+ ImageData data = mDataAdapter.getImageData(dataID);
+ if (data == null) {
+ return null;
+ }
+ data.prepare();
+ View v = mDataAdapter.getView(mContext, dataID);
+ if (v == null) {
+ return null;
+ }
+ ViewInfo info = new ViewInfo(dataID, v);
+ v = info.getView();
+ if (v != mCameraView) {
+ addView(info.getView());
+ } else {
+ v.setVisibility(View.VISIBLE);
+ }
+ return info;
+ }
+
+ private void removeInfo(int infoID) {
+ if (infoID >= mViewInfo.length || mViewInfo[infoID] == null) {
+ return;
+ }
+
+ ImageData data = mDataAdapter.getImageData(mViewInfo[infoID].getID());
+ checkForRemoval(data, mViewInfo[infoID].getView());
+ mViewInfo[infoID] = null;
+ }
+
+ /**
+ * We try to keep the one closest to the center of the screen at position
+ * mCurrentInfo.
+ */
+ private void stepIfNeeded() {
+ if (!inFilmStrip() && !inFullScreen()) {
+ // The good timing to step to the next view is when everything is
+ // not in
+ // transition.
+ return;
+ }
+ int nearest = findTheNearestView(mCenterX);
+ // no change made.
+ if (nearest == -1 || nearest == mCurrentInfo)
+ return;
+
+ int adjust = nearest - mCurrentInfo;
+ if (adjust > 0) {
+ for (int k = 0; k < adjust; k++) {
+ removeInfo(k);
+ }
+ for (int k = 0; k + adjust < BUFFER_SIZE; k++) {
+ mViewInfo[k] = mViewInfo[k + adjust];
+ }
+ for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) {
+ mViewInfo[k] = null;
+ if (mViewInfo[k - 1] != null) {
+ mViewInfo[k] = buildInfoFromData(mViewInfo[k - 1].getID() + 1);
+ }
+ }
+ } else {
+ for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) {
+ removeInfo(k);
+ }
+ for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) {
+ mViewInfo[k] = mViewInfo[k + adjust];
+ }
+ for (int k = -1 - adjust; k >= 0; k--) {
+ mViewInfo[k] = null;
+ if (mViewInfo[k + 1] != null) {
+ mViewInfo[k] = buildInfoFromData(mViewInfo[k + 1].getID() - 1);
+ }
+ }
+ }
+ }
+
+ /** Don't go beyond the bound. */
+ private void clampCenterX() {
+ ViewInfo curr = mViewInfo[mCurrentInfo];
+ if (curr == null) {
+ return;
+ }
+
+ if (curr.getID() == 0 && mCenterX < curr.getCenterX()) {
+ mCenterX = curr.getCenterX();
+ if (mController.isScrolling()) {
+ mController.stopScrolling();
+ }
+ if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW
+ && !mController.isScalling()
+ && mScale != FULLSCREEN_SCALE) {
+ mController.gotoFullScreen();
+ }
+ }
+ if (curr.getID() == mDataAdapter.getTotalNumber() - 1
+ && mCenterX > curr.getCenterX()) {
+ mCenterX = curr.getCenterX();
+ if (!mController.isScrolling()) {
+ mController.stopScrolling();
+ }
+ }
+ }
+
+ private void adjustChildZOrder() {
+ for (int i = BUFFER_SIZE - 1; i >= 0; i--) {
+ if (mViewInfo[i] == null)
+ continue;
+ bringChildToFront(mViewInfo[i].getView());
+ }
+ }
+
+ /**
+ * If the current photo is a photo sphere, this will launch the Photo Sphere
+ * panorama viewer.
+ */
+ private void showPhotoSphere() {
+ ViewInfo curr = mViewInfo[mCurrentInfo];
+ if (curr != null) {
+ mDataAdapter.getImageData(curr.getID()).viewPhotoSphere(mPanoramaViewHelper);
+ }
+ }
+
+ /**
+ * @return The ID of the current item, or -1.
+ */
+ private int getCurrentId() {
+ ViewInfo current = mViewInfo[mCurrentInfo];
+ if (current == null) {
+ return -1;
+ }
+ return current.getID();
+ }
+
+ /**
+ * Updates the visibility of the View Photo Sphere button.
+ */
+ private void updatePhotoSphereViewButton() {
+ if (mViewPhotoSphereButton == null) {
+ mViewPhotoSphereButton = (ImageButton) ((View) getParent())
+ .findViewById(R.id.filmstrip_bottom_control_panorama);
+ mViewPhotoSphereButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ showPhotoSphere();
+ }
+ });
+ }
+ final int requestId = getCurrentId();
+
+ // Check if the item has changed since the last time we updated the
+ // visibility status. Only then check of the current image is a photo
+ // sphere.
+ if (requestId == mLastItemId || requestId < 0) {
+ return;
+ }
+
+ ImageData data = mDataAdapter.getImageData(requestId);
+ data.isPhotoSphere(mContext, new PanoramaSupportCallback() {
+ @Override
+ public void panoramaInfoAvailable(final boolean isPanorama,
+ boolean isPanorama360) {
+ // Make sure the returned data is for the current image.
+ if (requestId == getCurrentId()) {
+ mViewPhotoSphereButton.post(new Runnable() {
+ @Override
+ public void run() {
+ mViewPhotoSphereButton.setVisibility(isPanorama ? View.VISIBLE
+ : View.GONE);
+ }
+ });
+ }
+ }
+ });
+ }
+
+ private void layoutChildren() {
+ if (mAnchorPending) {
+ mCenterX = mViewInfo[mCurrentInfo].getCenterX();
+ mAnchorPending = false;
+ }
+
+ if (mController.hasNewGeometry()) {
+ mCenterX = mController.getNewPosition();
+ mScale = mController.getNewScale();
+ }
+
+ clampCenterX();
+
+ mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterX, mScale);
+
+ int currentViewLeft = mViewInfo[mCurrentInfo].getLeftPosition();
+ int currentViewCenter = mViewInfo[mCurrentInfo].getCenterX();
+ int fullScreenWidth = mDrawArea.width() + mViewGap;
+ float scaleFraction = mViewAnimInterpolator.getInterpolation(
+ (mScale - FILM_STRIP_SCALE) / (FULLSCREEN_SCALE - FILM_STRIP_SCALE));
+
+ // images on the left
+ for (int infoID = mCurrentInfo - 1; infoID >= 0; infoID--) {
+ ViewInfo curr = mViewInfo[infoID];
+ if (curr == null) {
+ continue;
+ }
+
+ ViewInfo next = mViewInfo[infoID + 1];
+ int myLeft =
+ next.getLeftPosition() - curr.getView().getMeasuredWidth() - mViewGap;
+ curr.setLeftPosition(myLeft);
+ curr.layoutIn(mDrawArea, mCenterX, mScale);
+ curr.getView().setAlpha(1f);
+ int infoDiff = mCurrentInfo - infoID;
+ curr.setTranslationX(
+ (currentViewCenter
+ - fullScreenWidth * infoDiff - curr.getCenterX()) * scaleFraction,
+ mScale);
+ }
+
+ // images on the right
+ for (int infoID = mCurrentInfo + 1; infoID < BUFFER_SIZE; infoID++) {
+ ViewInfo curr = mViewInfo[infoID];
+ if (curr == null) {
+ continue;
+ }
+
+ ViewInfo prev = mViewInfo[infoID - 1];
+ int myLeft =
+ prev.getLeftPosition() + prev.getView().getMeasuredWidth() + mViewGap;
+ curr.setLeftPosition(myLeft);
+ curr.layoutIn(mDrawArea, mCenterX, mScale);
+ if (infoID == mCurrentInfo + 1) {
+ curr.getView().setAlpha(1f - scaleFraction);
+ } else {
+ if (scaleFraction == 0f) {
+ curr.getView().setAlpha(1f);
+ } else {
+ curr.getView().setAlpha(0f);
+ }
+ }
+ curr.setTranslationX((currentViewLeft - myLeft) * scaleFraction, mScale);
+ }
+
+ stepIfNeeded();
+ adjustChildZOrder();
+ invalidate();
+ updatePhotoSphereViewButton();
+ mLastItemId = getCurrentId();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ if (mViewInfo[mCurrentInfo] == null) {
+ return;
+ }
+
+ mDrawArea.left = l;
+ mDrawArea.top = t;
+ mDrawArea.right = r;
+ mDrawArea.bottom = b;
+
+ layoutChildren();
+ }
+
+ // Keeps the view in the view hierarchy if it's camera preview.
+ // Remove from the hierarchy otherwise.
+ private void checkForRemoval(ImageData data, View v) {
+ if (data.getType() != ImageData.TYPE_CAMERA_PREVIEW) {
+ removeView(v);
+ data.recycle();
+ } else {
+ v.setVisibility(View.INVISIBLE);
+ if (mCameraView != null && mCameraView != v) {
+ removeView(mCameraView);
+ }
+ mCameraView = v;
+ }
+ }
+
+ private void slideViewBack(View v) {
+ v.animate().translationX(0)
+ .alpha(1f)
+ .setDuration(DURATION_GEOMETRY_ADJUST)
+ .setInterpolator(mViewAnimInterpolator)
+ .start();
+ }
+
+ private void updateRemoval(int dataID, final ImageData data) {
+ int removedInfo = findInfoByDataID(dataID);
+
+ // adjust the data id to be consistent
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] == null || mViewInfo[i].getID() <= dataID) {
+ continue;
+ }
+ mViewInfo[i].setID(mViewInfo[i].getID() - 1);
+ }
+ if (removedInfo == -1) {
+ return;
+ }
+
+ final View removedView = mViewInfo[removedInfo].getView();
+ final int offsetX = removedView.getMeasuredWidth() + mViewGap;
+
+ for (int i = removedInfo + 1; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] != null) {
+ mViewInfo[i].setLeftPosition(mViewInfo[i].getLeftPosition() - offsetX);
+ }
+ }
+
+ if (removedInfo >= mCurrentInfo
+ && mViewInfo[removedInfo].getID() < mDataAdapter.getTotalNumber()) {
+ // Fill the removed info by left shift when the current one or
+ // anyone on the right is removed, and there's more data on the
+ // right available.
+ for (int i = removedInfo; i < BUFFER_SIZE - 1; i++) {
+ mViewInfo[i] = mViewInfo[i + 1];
+ }
+
+ // pull data out from the DataAdapter for the last one.
+ int curr = BUFFER_SIZE - 1;
+ int prev = curr - 1;
+ if (mViewInfo[prev] != null) {
+ mViewInfo[curr] = buildInfoFromData(mViewInfo[prev].getID() + 1);
+ }
+
+ // Translate the views to their original places.
+ for (int i = removedInfo; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] != null) {
+ mViewInfo[i].setTranslationX(offsetX, mScale);
+ }
+ }
+
+ // The end of the filmstrip might have been changed.
+ // The mCenterX might be out of the bound.
+ ViewInfo currInfo = mViewInfo[mCurrentInfo];
+ if (currInfo.getID() == mDataAdapter.getTotalNumber() - 1
+ && mCenterX > currInfo.getCenterX()) {
+ int adjustDiff = currInfo.getCenterX() - mCenterX;
+ mCenterX = currInfo.getCenterX();
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] != null) {
+ mViewInfo[i].translateXBy(adjustDiff, mScale);
+ }
+ }
+ }
+ } else {
+ // fill the removed place by right shift
+ mCenterX -= offsetX;
+
+ for (int i = removedInfo; i > 0; i--) {
+ mViewInfo[i] = mViewInfo[i - 1];
+ }
+
+ // pull data out from the DataAdapter for the first one.
+ int curr = 0;
+ int next = curr + 1;
+ if (mViewInfo[next] != null) {
+ mViewInfo[curr] = buildInfoFromData(mViewInfo[next].getID() - 1);
+ }
+
+ // Translate the views to their original places.
+ for (int i = removedInfo; i >= 0; i--) {
+ if (mViewInfo[i] != null) {
+ mViewInfo[i].setTranslationX(-offsetX, mScale);
+ }
+ }
+ }
+
+ // Now, slide every one back.
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] != null
+ && mViewInfo[i].getTranslationX(mScale) != 0f) {
+ slideViewBack(mViewInfo[i].getView());
+ }
+ }
+
+ int transY = getHeight() / 8;
+ if (removedView.getTranslationY() < 0) {
+ transY = -transY;
+ }
+ removedView.animate()
+ .alpha(0f)
+ .translationYBy(transY)
+ .setInterpolator(mViewAnimInterpolator)
+ .setDuration(DURATION_GEOMETRY_ADJUST)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ checkForRemoval(data, removedView);
+ }
+ })
+ .start();
+ layoutChildren();
+ }
+
+ // returns -1 on failure.
+ private int findInfoByDataID(int dataID) {
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] != null
+ && mViewInfo[i].getID() == dataID) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void updateInsertion(int dataID) {
+ int insertedInfo = findInfoByDataID(dataID);
+ if (insertedInfo == -1) {
+ // Not in the current info buffers. Check if it's inserted
+ // at the end.
+ if (dataID == mDataAdapter.getTotalNumber() - 1) {
+ int prev = findInfoByDataID(dataID - 1);
+ if (prev >= 0 && prev < BUFFER_SIZE - 1) {
+ // The previous data is in the buffer and we still
+ // have room for the inserted data.
+ insertedInfo = prev + 1;
+ }
+ }
+ }
+
+ // adjust the data id to be consistent
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] == null || mViewInfo[i].getID() < dataID) {
+ continue;
+ }
+ mViewInfo[i].setID(mViewInfo[i].getID() + 1);
+ }
+ if (insertedInfo == -1) {
+ return;
+ }
+
+ final ImageData data = mDataAdapter.getImageData(dataID);
+ int[] dim = calculateChildDimension(
+ data.getWidth(), data.getHeight(),
+ getMeasuredWidth(), getMeasuredHeight());
+ final int offsetX = dim[0] + mViewGap;
+ ViewInfo viewInfo = buildInfoFromData(dataID);
+
+ if (insertedInfo >= mCurrentInfo) {
+ if (insertedInfo == mCurrentInfo) {
+ viewInfo.setLeftPosition(mViewInfo[mCurrentInfo].getLeftPosition());
+ }
+ // Shift right to make rooms for newly inserted item.
+ removeInfo(BUFFER_SIZE - 1);
+ for (int i = BUFFER_SIZE - 1; i > insertedInfo; i--) {
+ mViewInfo[i] = mViewInfo[i - 1];
+ if (mViewInfo[i] != null) {
+ mViewInfo[i].setTranslationX(-offsetX, mScale);
+ slideViewBack(mViewInfo[i].getView());
+ }
+ }
+ } else {
+ // Shift left. Put the inserted data on the left instead of the
+ // found position.
+ --insertedInfo;
+ if (insertedInfo < 0) {
+ return;
+ }
+ removeInfo(0);
+ for (int i = 1; i <= insertedInfo; i++) {
+ if (mViewInfo[i] != null) {
+ mViewInfo[i].setTranslationX(offsetX, mScale);
+ slideViewBack(mViewInfo[i].getView());
+ mViewInfo[i - 1] = mViewInfo[i];
+ }
+ }
+ }
+
+ mViewInfo[insertedInfo] = viewInfo;
+ View insertedView = mViewInfo[insertedInfo].getView();
+ insertedView.setAlpha(0f);
+ insertedView.setTranslationY(getHeight() / 8);
+ insertedView.animate()
+ .alpha(1f)
+ .translationY(0f)
+ .setInterpolator(mViewAnimInterpolator)
+ .setDuration(DURATION_GEOMETRY_ADJUST)
+ .start();
+ invalidate();
+ }
+
+ public void setDataAdapter(DataAdapter adapter) {
+ mDataAdapter = adapter;
+ mDataAdapter.suggestViewSizeBound(getMeasuredWidth(), getMeasuredHeight());
+ mDataAdapter.setListener(new DataAdapter.Listener() {
+ @Override
+ public void onDataLoaded() {
+ reload();
+ }
+
+ @Override
+ public void onDataUpdated(DataAdapter.UpdateReporter reporter) {
+ update(reporter);
+ }
+
+ @Override
+ public void onDataInserted(int dataID, ImageData data) {
+ if (mViewInfo[mCurrentInfo] == null) {
+ // empty now, simply do a reload.
+ reload();
+ return;
+ }
+ updateInsertion(dataID);
+ }
+
+ @Override
+ public void onDataRemoved(int dataID, ImageData data) {
+ updateRemoval(dataID, data);
+ }
+ });
+ }
+
+ public boolean inFilmStrip() {
+ return (mScale == FILM_STRIP_SCALE);
+ }
+
+ public boolean inFullScreen() {
+ return (mScale == FULLSCREEN_SCALE);
+ }
+
+ public boolean inCameraFullscreen() {
+ return isAnchoredTo(0) && inFullScreen()
+ && (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (inFilmStrip()) {
+ return true;
+ }
+
+ if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mCheckToIntercept = true;
+ mDown = MotionEvent.obtain(ev);
+ ViewInfo viewInfo = mViewInfo[mCurrentInfo];
+ // Do not intercept touch if swipe is not enabled
+ if (viewInfo != null && !mDataAdapter.canSwipeInFullScreen(viewInfo.getID())) {
+ mCheckToIntercept = false;
+ }
+ return false;
+ } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
+ // Do not intercept touch once child is in zoom mode
+ mCheckToIntercept = false;
+ return false;
+ } else {
+ if (!mCheckToIntercept) {
+ return false;
+ }
+ if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT) {
+ return false;
+ }
+ int deltaX = (int) (ev.getX() - mDown.getX());
+ int deltaY = (int) (ev.getY() - mDown.getY());
+ if (ev.getActionMasked() == MotionEvent.ACTION_MOVE
+ && deltaX < mSlop * (-1)) {
+ // intercept left swipe
+ if (Math.abs(deltaX) >= Math.abs(deltaY) * 2) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ mGestureRecognizer.onTouchEvent(ev);
+ return true;
+ }
+
+ private void updateViewInfo(int infoID) {
+ ViewInfo info = mViewInfo[infoID];
+ removeView(info.getView());
+ mViewInfo[infoID] = buildInfoFromData(info.getID());
+ }
+
+ /** Some of the data is changed. */
+ private void update(DataAdapter.UpdateReporter reporter) {
+ // No data yet.
+ if (mViewInfo[mCurrentInfo] == null) {
+ reload();
+ return;
+ }
+
+ // Check the current one.
+ ViewInfo curr = mViewInfo[mCurrentInfo];
+ int dataID = curr.getID();
+ if (reporter.isDataRemoved(dataID)) {
+ mCenterX = -1;
+ reload();
+ return;
+ }
+ if (reporter.isDataUpdated(dataID)) {
+ updateViewInfo(mCurrentInfo);
+ }
+
+ // Check left
+ for (int i = mCurrentInfo - 1; i >= 0; i--) {
+ curr = mViewInfo[i];
+ if (curr != null) {
+ dataID = curr.getID();
+ if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) {
+ updateViewInfo(i);
+ }
+ } else {
+ ViewInfo next = mViewInfo[i + 1];
+ if (next != null) {
+ mViewInfo[i] = buildInfoFromData(next.getID() - 1);
+ }
+ }
+ }
+
+ // Check right
+ for (int i = mCurrentInfo + 1; i < BUFFER_SIZE; i++) {
+ curr = mViewInfo[i];
+ if (curr != null) {
+ dataID = curr.getID();
+ if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) {
+ updateViewInfo(i);
+ }
+ } else {
+ ViewInfo prev = mViewInfo[i - 1];
+ if (prev != null) {
+ mViewInfo[i] = buildInfoFromData(prev.getID() + 1);
+ }
+ }
+ }
+ }
+
+ /**
+ * The whole data might be totally different. Flush all and load from the
+ * start.
+ */
+ private void reload() {
+ removeAllViews();
+ int dataNumber = mDataAdapter.getTotalNumber();
+ if (dataNumber == 0) {
+ return;
+ }
+
+ mViewInfo[mCurrentInfo] = buildInfoFromData(0);
+ mViewInfo[mCurrentInfo].setLeftPosition(0);
+ if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW) {
+ // we are in camera mode by default.
+ mController.lockAtCurrentView();
+ }
+ for (int i = 1; mCurrentInfo + i < BUFFER_SIZE || mCurrentInfo - i >= 0; i++) {
+ int infoID = mCurrentInfo + i;
+ if (infoID < BUFFER_SIZE && mViewInfo[infoID - 1] != null) {
+ mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID - 1].getID() + 1);
+ }
+ infoID = mCurrentInfo - i;
+ if (infoID >= 0 && mViewInfo[infoID + 1] != null) {
+ mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID + 1].getID() - 1);
+ }
+ }
+ layoutChildren();
+ }
+
+ private void promoteData(int infoID, int dataID) {
+ if (mListener != null) {
+ mListener.onDataPromoted(dataID);
+ }
+ }
+
+ private void demoteData(int infoID, int dataID) {
+ if (mListener != null) {
+ mListener.onDataDemoted(dataID);
+ }
+ }
+
+ /**
+ * MyController controls all the geometry animations. It passively tells the
+ * geometry information on demand.
+ */
+ private class MyController implements
+ Controller,
+ ValueAnimator.AnimatorUpdateListener,
+ Animator.AnimatorListener {
+
+ private ValueAnimator mScaleAnimator;
+ private boolean mHasNewScale;
+ private float mNewScale;
+
+ private Scroller mScroller;
+ private boolean mHasNewPosition;
+ private DecelerateInterpolator mDecelerateInterpolator;
+
+ private boolean mCanStopScroll;
+
+ private boolean mIsPositionLocked;
+ private int mLockedViewInfo;
+
+ MyController(Context context) {
+ mScroller = new Scroller(context);
+ mHasNewPosition = false;
+ mScaleAnimator = new ValueAnimator();
+ mScaleAnimator.addUpdateListener(MyController.this);
+ mScaleAnimator.addListener(MyController.this);
+ mDecelerateInterpolator = new DecelerateInterpolator();
+ mCanStopScroll = true;
+ mHasNewScale = false;
+ }
+
+ @Override
+ public boolean isScrolling() {
+ return !mScroller.isFinished();
+ }
+
+ @Override
+ public boolean isScalling() {
+ return mScaleAnimator.isRunning();
+ }
+
+ boolean hasNewGeometry() {
+ mHasNewPosition = mScroller.computeScrollOffset();
+ if (!mHasNewPosition) {
+ mCanStopScroll = true;
+ }
+ // If the position is locked, then we always return true to force
+ // the position value to use the locked value.
+ return (mHasNewPosition || mHasNewScale || mIsPositionLocked);
+ }
+
+ /**
+ * Always call {@link #hasNewGeometry()} before getting the new scale
+ * value.
+ */
+ float getNewScale() {
+ if (!mHasNewScale) {
+ return mScale;
+ }
+ mHasNewScale = false;
+ return mNewScale;
+ }
+
+ /**
+ * Always call {@link #hasNewGeometry()} before getting the new position
+ * value.
+ */
+ int getNewPosition() {
+ if (mIsPositionLocked) {
+ if (mViewInfo[mLockedViewInfo] == null)
+ return mCenterX;
+ return mViewInfo[mLockedViewInfo].getCenterX();
+ }
+ if (!mHasNewPosition)
+ return mCenterX;
+ return mScroller.getCurrX();
+ }
+
+ @Override
+ public void lockAtCurrentView() {
+ mIsPositionLocked = true;
+ mLockedViewInfo = mCurrentInfo;
+ }
+
+ @Override
+ public void unlockPosition() {
+ if (mIsPositionLocked) {
+ // only when the position is previously locked we set the
+ // current position to make it consistent.
+ if (mViewInfo[mLockedViewInfo] != null) {
+ mCenterX = mViewInfo[mLockedViewInfo].getCenterX();
+ }
+ mIsPositionLocked = false;
+ }
+ }
+
+ private int estimateMinX(int dataID, int leftPos, int viewWidth) {
+ return leftPos - (dataID + 100) * (viewWidth + mViewGap);
+ }
+
+ private int estimateMaxX(int dataID, int leftPos, int viewWidth) {
+ return leftPos
+ + (mDataAdapter.getTotalNumber() - dataID + 100)
+ * (viewWidth + mViewGap);
+ }
+
+ @Override
+ public void scroll(float deltaX) {
+ if (mController.isScrolling()) {
+ return;
+ }
+ mCenterX += deltaX;
+ }
+
+ @Override
+ public void fling(float velocityX) {
+ if (!stopScrolling() || mIsPositionLocked) {
+ return;
+ }
+ ViewInfo info = mViewInfo[mCurrentInfo];
+ if (info == null) {
+ return;
+ }
+
+ float scaledVelocityX = velocityX / mScale;
+ if (inCameraFullscreen() && scaledVelocityX < 0) {
+ // Swipe left in camera preview.
+ gotoFilmStrip();
+ }
+
+ int w = getWidth();
+ // Estimation of possible length on the left. To ensure the
+ // velocity doesn't become too slow eventually, we add a huge number
+ // to the estimated maximum.
+ int minX = estimateMinX(info.getID(), info.getLeftPosition(), w);
+ // Estimation of possible length on the right. Likewise, exaggerate
+ // the possible maximum too.
+ int maxX = estimateMaxX(info.getID(), info.getLeftPosition(), w);
+ mScroller.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0);
+
+ layoutChildren();
+ }
+
+ @Override
+ public boolean stopScrolling() {
+ if (!mCanStopScroll)
+ return false;
+ mScroller.forceFinished(true);
+ mHasNewPosition = false;
+ return true;
+ }
+
+ private void stopScale() {
+ mScaleAnimator.cancel();
+ mHasNewScale = false;
+ }
+
+ @Override
+ public void scrollTo(int position, int duration, boolean interruptible) {
+ if (!stopScrolling() || mIsPositionLocked)
+ return;
+ mCanStopScroll = interruptible;
+ stopScrolling();
+ mScroller.startScroll(mCenterX, 0, position - mCenterX,
+ 0, duration);
+ invalidate();
+ }
+
+ private void scaleTo(float scale, int duration) {
+ stopScale();
+ mScaleAnimator.setDuration(duration);
+ mScaleAnimator.setFloatValues(mScale, scale);
+ mScaleAnimator.setInterpolator(mDecelerateInterpolator);
+ mScaleAnimator.start();
+ mHasNewScale = true;
+ layoutChildren();
+ }
+
+ @Override
+ public void gotoFilmStrip() {
+ unlockPosition();
+ scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST);
+ if (mListener != null) {
+ mListener.onSwitchMode(false);
+ }
+ }
+
+ @Override
+ public void gotoFullScreen() {
+ if (mViewInfo[mCurrentInfo] != null) {
+ mController.scrollTo(mViewInfo[mCurrentInfo].getCenterX(),
+ DURATION_GEOMETRY_ADJUST, false);
+ }
+ enterFullScreen();
+ }
+
+ private void enterFullScreen() {
+ if (mListener != null) {
+ // TODO: After full size images snapping to fill the screen at
+ // the end of a scroll/fling is implemented, we should only make
+ // this call when the view on the center of the screen is
+ // camera preview
+ mListener.onSwitchMode(true);
+ }
+ if (inFullScreen()) {
+ return;
+ }
+ scaleTo(1f, DURATION_GEOMETRY_ADJUST);
+ }
+
+ @Override
+ public void gotoCameraFullScreen() {
+ if (mDataAdapter.getImageData(0).getType()
+ != ImageData.TYPE_CAMERA_PREVIEW) {
+ return;
+ }
+ gotoFullScreen();
+ scrollTo(
+ estimateMinX(mViewInfo[mCurrentInfo].getID(),
+ mViewInfo[mCurrentInfo].getLeftPosition(),
+ getWidth()),
+ DURATION_GEOMETRY_ADJUST, false);
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ mHasNewScale = true;
+ mNewScale = (Float) animation.getAnimatedValue();
+ layoutChildren();
+ }
+
+ @Override
+ public void onAnimationStart(Animator anim) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator anim) {
+ ViewInfo info = mViewInfo[mCurrentInfo];
+ if (info != null && mCenterX == info.getCenterX()) {
+ if (inFullScreen()) {
+ lockAtCurrentView();
+ } else if (inFilmStrip()) {
+ unlockPosition();
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator anim) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator anim) {
+ }
+ }
+
+ private class MyGestureReceiver implements FilmStripGestureRecognizer.Listener {
+ // Indicating the current trend of scaling is up (>1) or down (<1).
+ private float mScaleTrend;
+
+ @Override
+ public boolean onSingleTapUp(float x, float y) {
+ if (inFilmStrip()) {
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] == null) {
+ continue;
+ }
+
+ if (mViewInfo[i].areaContains(x, y)) {
+ mController.scrollTo(mViewInfo[i].getCenterX(),
+ DURATION_GEOMETRY_ADJUST, false);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onDoubleTap(float x, float y) {
+ if (inFilmStrip()) {
+ ViewInfo centerInfo = mViewInfo[mCurrentInfo];
+ if (centerInfo != null && centerInfo.areaContains(x, y)) {
+ mController.gotoFullScreen();
+ return true;
+ }
+ } else if (inFullScreen()) {
+ mController.gotoFilmStrip();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onDown(float x, float y) {
+ if (mController.isScrolling()) {
+ mController.stopScrolling();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onUp(float x, float y) {
+ float halfH = getHeight() / 2;
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ if (mViewInfo[i] == null) {
+ continue;
+ }
+ float transY = mViewInfo[i].getTranslationY(mScale);
+ if (transY == 0) {
+ continue;
+ }
+ int id = mViewInfo[i].getID();
+
+ if (mDataAdapter.getImageData(id)
+ .isUIActionSupported(ImageData.ACTION_DEMOTE)
+ && transY > halfH) {
+ demoteData(i, id);
+ } else if (mDataAdapter.getImageData(id)
+ .isUIActionSupported(ImageData.ACTION_PROMOTE)
+ && transY < -halfH) {
+ promoteData(i, id);
+ } else {
+ // put the view back.
+ mViewInfo[i].getView().animate()
+ .translationY(0f)
+ .alpha(1f)
+ .setDuration(DURATION_GEOMETRY_ADJUST)
+ .start();
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onScroll(float x, float y, float dx, float dy) {
+ int deltaX = (int) (dx / mScale);
+ if (inFilmStrip()) {
+ if (Math.abs(dx) > Math.abs(dy)) {
+ if (deltaX > 0 && inCameraFullscreen()) {
+ mController.gotoFilmStrip();
+ }
+ mController.scroll(deltaX);
+ } else {
+ // Vertical part. Promote or demote.
+ // int scaledDeltaY = (int) (dy * mScale);
+ int hit = 0;
+ Rect hitRect = new Rect();
+ for (; hit < BUFFER_SIZE; hit++) {
+ if (mViewInfo[hit] == null) {
+ continue;
+ }
+ mViewInfo[hit].getView().getHitRect(hitRect);
+ if (hitRect.contains((int) x, (int) y)) {
+ break;
+ }
+ }
+ if (hit == BUFFER_SIZE) {
+ return false;
+ }
+
+ ImageData data = mDataAdapter.getImageData(mViewInfo[hit].getID());
+ float transY = mViewInfo[hit].getTranslationY(mScale) - dy / mScale;
+ if (!data.isUIActionSupported(ImageData.ACTION_DEMOTE) && transY > 0f) {
+ transY = 0f;
+ }
+ if (!data.isUIActionSupported(ImageData.ACTION_PROMOTE) && transY < 0f) {
+ transY = 0f;
+ }
+ mViewInfo[hit].setTranslationY(transY, mScale);
+ }
+ } else if (inFullScreen()) {
+ if (deltaX > 0 && inCameraFullscreen()) {
+ mController.gotoFilmStrip();
+ }
+ mController.scroll(deltaX);
+ }
+ layoutChildren();
+
+ return true;
+ }
+
+ @Override
+ public boolean onFling(float velocityX, float velocityY) {
+ if (Math.abs(velocityX) > Math.abs(velocityY)) {
+ mController.fling(velocityX);
+ } else {
+ // ignore vertical fling.
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onScaleBegin(float focusX, float focusY) {
+ if (inCameraFullscreen()) {
+ return false;
+ }
+ mScaleTrend = 1f;
+ return true;
+ }
+
+ @Override
+ public boolean onScale(float focusX, float focusY, float scale) {
+ if (inCameraFullscreen()) {
+ return false;
+ }
+
+ mScaleTrend = mScaleTrend * 0.3f + scale * 0.7f;
+ mScale *= scale;
+ if (mScale <= FILM_STRIP_SCALE) {
+ mScale = FILM_STRIP_SCALE;
+ }
+ if (mScale >= FULLSCREEN_SCALE) {
+ mScale = FULLSCREEN_SCALE;
+ }
+ layoutChildren();
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd() {
+ if (mScaleTrend >= 1f) {
+ mController.gotoFullScreen();
+ } else {
+ mController.gotoFilmStrip();
+ }
+ mScaleTrend = 1f;
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/FocusIndicator.java b/src/com/android/camera/ui/FocusIndicator.java
new file mode 100644
index 000000000..e06057041
--- /dev/null
+++ b/src/com/android/camera/ui/FocusIndicator.java
@@ -0,0 +1,24 @@
+/*
+ * 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.camera.ui;
+
+public interface FocusIndicator {
+ public void showStart();
+ public void showSuccess(boolean timeout);
+ public void showFail(boolean timeout);
+ public void clear();
+}
diff --git a/src/com/android/camera/ui/InLineSettingCheckBox.java b/src/com/android/camera/ui/InLineSettingCheckBox.java
new file mode 100644
index 000000000..c1aa5a91c
--- /dev/null
+++ b/src/com/android/camera/ui/InLineSettingCheckBox.java
@@ -0,0 +1,83 @@
+/*
+ * 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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+
+
+import com.android.camera.ListPreference;
+import com.android.gallery3d.R;
+
+/* A check box setting control which turns on/off the setting. */
+public class InLineSettingCheckBox extends InLineSettingItem {
+ private CheckBox mCheckBox;
+
+ OnCheckedChangeListener mCheckedChangeListener = new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean desiredState) {
+ changeIndex(desiredState ? 1 : 0);
+ }
+ };
+
+ public InLineSettingCheckBox(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mCheckBox = (CheckBox) findViewById(R.id.setting_check_box);
+ mCheckBox.setOnCheckedChangeListener(mCheckedChangeListener);
+ }
+
+ @Override
+ public void initialize(ListPreference preference) {
+ super.initialize(preference);
+ // Add content descriptions for the increment and decrement buttons.
+ mCheckBox.setContentDescription(getContext().getResources().getString(
+ R.string.accessibility_check_box, mPreference.getTitle()));
+ }
+
+ @Override
+ protected void updateView() {
+ mCheckBox.setOnCheckedChangeListener(null);
+ if (mOverrideValue == null) {
+ mCheckBox.setChecked(mIndex == 1);
+ } else {
+ int index = mPreference.findIndexOfValue(mOverrideValue);
+ mCheckBox.setChecked(index == 1);
+ }
+ mCheckBox.setOnCheckedChangeListener(mCheckedChangeListener);
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ event.getText().add(mPreference.getTitle());
+ return true;
+ }
+
+ @Override
+ public void setEnabled(boolean enable) {
+ if (mTitle != null) mTitle.setEnabled(enable);
+ if (mCheckBox != null) mCheckBox.setEnabled(enable);
+ }
+}
diff --git a/src/com/android/camera/ui/InLineSettingItem.java b/src/com/android/camera/ui/InLineSettingItem.java
new file mode 100644
index 000000000..839a77fd0
--- /dev/null
+++ b/src/com/android/camera/ui/InLineSettingItem.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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.camera.ListPreference;
+import com.android.gallery3d.R;
+
+/**
+ * A one-line camera setting could be one of three types: knob, switch or restore
+ * preference button. The setting includes a title for showing the preference
+ * title which is initialized in the SimpleAdapter. A knob also includes
+ * (ex: Picture size), a previous button, the current value (ex: 5MP),
+ * and a next button. A switch, i.e. the preference RecordLocationPreference,
+ * has only two values on and off which will be controlled in a switch button.
+ * Other setting popup window includes several InLineSettingItem items with
+ * different types if possible.
+ */
+public abstract class InLineSettingItem extends LinearLayout {
+ private Listener mListener;
+ protected ListPreference mPreference;
+ protected int mIndex;
+ // Scene mode can override the original preference value.
+ protected String mOverrideValue;
+ protected TextView mTitle;
+
+ static public interface Listener {
+ public void onSettingChanged(ListPreference pref);
+ }
+
+ public InLineSettingItem(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ protected void setTitle(ListPreference preference) {
+ mTitle = ((TextView) findViewById(R.id.title));
+ mTitle.setText(preference.getTitle());
+ }
+
+ public void initialize(ListPreference preference) {
+ setTitle(preference);
+ if (preference == null) return;
+ mPreference = preference;
+ reloadPreference();
+ }
+
+ protected abstract void updateView();
+
+ protected boolean changeIndex(int index) {
+ if (index >= mPreference.getEntryValues().length || index < 0) return false;
+ mIndex = index;
+ mPreference.setValueIndex(mIndex);
+ if (mListener != null) {
+ mListener.onSettingChanged(mPreference);
+ }
+ updateView();
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+ return true;
+ }
+
+ // The value of the preference may have changed. Update the UI.
+ public void reloadPreference() {
+ mIndex = mPreference.findIndexOfValue(mPreference.getValue());
+ updateView();
+ }
+
+ public void setSettingChangedListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public void overrideSettings(String value) {
+ mOverrideValue = value;
+ updateView();
+ }
+}
diff --git a/src/com/android/camera/ui/InLineSettingMenu.java b/src/com/android/camera/ui/InLineSettingMenu.java
new file mode 100644
index 000000000..8e45c3e38
--- /dev/null
+++ b/src/com/android/camera/ui/InLineSettingMenu.java
@@ -0,0 +1,78 @@
+/*
+ * 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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.TextView;
+
+import com.android.camera.ListPreference;
+import com.android.gallery3d.R;
+
+/* Setting menu item that will bring up a menu when you click on it. */
+public class InLineSettingMenu extends InLineSettingItem {
+ private static final String TAG = "InLineSettingMenu";
+ // The view that shows the current selected setting. Ex: 5MP
+ private TextView mEntry;
+
+ public InLineSettingMenu(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mEntry = (TextView) findViewById(R.id.current_setting);
+ }
+
+ @Override
+ public void initialize(ListPreference preference) {
+ super.initialize(preference);
+ //TODO: add contentDescription
+ }
+
+ @Override
+ protected void updateView() {
+ if (mOverrideValue == null) {
+ mEntry.setText(mPreference.getEntry());
+ } else {
+ int index = mPreference.findIndexOfValue(mOverrideValue);
+ if (index != -1) {
+ mEntry.setText(mPreference.getEntries()[index]);
+ } else {
+ // Avoid the crash if camera driver has bugs.
+ Log.e(TAG, "Fail to find override value=" + mOverrideValue);
+ mPreference.print();
+ }
+ }
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ event.getText().add(mPreference.getTitle() + mPreference.getEntry());
+ return true;
+ }
+
+ @Override
+ public void setEnabled(boolean enable) {
+ super.setEnabled(enable);
+ if (mTitle != null) mTitle.setEnabled(enable);
+ if (mEntry != null) mEntry.setEnabled(enable);
+ }
+}
diff --git a/src/com/android/camera/ui/LayoutChangeHelper.java b/src/com/android/camera/ui/LayoutChangeHelper.java
new file mode 100644
index 000000000..ef4eb6a7a
--- /dev/null
+++ b/src/com/android/camera/ui/LayoutChangeHelper.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.view.View;
+
+public class LayoutChangeHelper implements LayoutChangeNotifier {
+ private LayoutChangeNotifier.Listener mListener;
+ private boolean mFirstTimeLayout;
+ private View mView;
+
+ public LayoutChangeHelper(View v) {
+ mView = v;
+ mFirstTimeLayout = true;
+ }
+
+ @Override
+ public void setOnLayoutChangeListener(LayoutChangeNotifier.Listener listener) {
+ mListener = listener;
+ }
+
+ public void onLayout(boolean changed, int l, int t, int r, int b) {
+ if (mListener == null) return;
+ if (mFirstTimeLayout || changed) {
+ mFirstTimeLayout = false;
+ mListener.onLayoutChange(mView, l, t, r, b);
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/LayoutChangeNotifier.java b/src/com/android/camera/ui/LayoutChangeNotifier.java
new file mode 100644
index 000000000..6261d34f6
--- /dev/null
+++ b/src/com/android/camera/ui/LayoutChangeNotifier.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.view.View;
+
+public interface LayoutChangeNotifier {
+ public interface Listener {
+ // Invoked only when the layout has changed or it is the first layout.
+ public void onLayoutChange(View v, int l, int t, int r, int b);
+ }
+
+ public void setOnLayoutChangeListener(LayoutChangeNotifier.Listener listener);
+}
diff --git a/src/com/android/camera/ui/LayoutNotifyView.java b/src/com/android/camera/ui/LayoutNotifyView.java
new file mode 100644
index 000000000..6e118fc3a
--- /dev/null
+++ b/src/com/android/camera/ui/LayoutNotifyView.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+/*
+ * Customized view to support onLayoutChange() at or before API 10.
+ */
+public class LayoutNotifyView extends View implements LayoutChangeNotifier {
+ private LayoutChangeHelper mLayoutChangeHelper = new LayoutChangeHelper(this);
+
+ public LayoutNotifyView(Context context) {
+ super(context);
+ }
+
+ public LayoutNotifyView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void setOnLayoutChangeListener(
+ LayoutChangeNotifier.Listener listener) {
+ mLayoutChangeHelper.setOnLayoutChangeListener(listener);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ mLayoutChangeHelper.onLayout(changed, l, t, r, b);
+ }
+}
diff --git a/src/com/android/camera/ui/ListPrefSettingPopup.java b/src/com/android/camera/ui/ListPrefSettingPopup.java
new file mode 100644
index 000000000..cfef73f49
--- /dev/null
+++ b/src/com/android/camera/ui/ListPrefSettingPopup.java
@@ -0,0 +1,127 @@
+/*
+ * 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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.AdapterView;
+import android.widget.ImageView;
+import android.widget.SimpleAdapter;
+
+import com.android.camera.IconListPreference;
+import com.android.camera.ListPreference;
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+// A popup window that shows one camera setting. The title is the name of the
+// setting (ex: white-balance). The entries are the supported values (ex:
+// daylight, incandescent, etc). If initialized with an IconListPreference,
+// the entries will contain both text and icons. Otherwise, entries will be
+// shown in text.
+public class ListPrefSettingPopup extends AbstractSettingPopup implements
+ AdapterView.OnItemClickListener {
+ private static final String TAG = "ListPrefSettingPopup";
+ private ListPreference mPreference;
+ private Listener mListener;
+
+ static public interface Listener {
+ public void onListPrefChanged(ListPreference pref);
+ }
+
+ public ListPrefSettingPopup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ private class ListPrefSettingAdapter extends SimpleAdapter {
+ ListPrefSettingAdapter(Context context, List<? extends Map<String, ?>> data,
+ int resource, String[] from, int[] to) {
+ super(context, data, resource, from, to);
+ }
+
+ @Override
+ public void setViewImage(ImageView v, String value) {
+ if ("".equals(value)) {
+ // Some settings have no icons. Ex: exposure compensation.
+ v.setVisibility(View.GONE);
+ } else {
+ super.setViewImage(v, value);
+ }
+ }
+ }
+
+ public void initialize(ListPreference preference) {
+ mPreference = preference;
+ Context context = getContext();
+ CharSequence[] entries = mPreference.getEntries();
+ int[] iconIds = null;
+ if (preference instanceof IconListPreference) {
+ iconIds = ((IconListPreference) mPreference).getImageIds();
+ if (iconIds == null) {
+ iconIds = ((IconListPreference) mPreference).getLargeIconIds();
+ }
+ }
+ // Set title.
+ mTitle.setText(mPreference.getTitle());
+
+ // Prepare the ListView.
+ ArrayList<HashMap<String, Object>> listItem =
+ new ArrayList<HashMap<String, Object>>();
+ for(int i = 0; i < entries.length; ++i) {
+ HashMap<String, Object> map = new HashMap<String, Object>();
+ map.put("text", entries[i].toString());
+ if (iconIds != null) map.put("image", iconIds[i]);
+ listItem.add(map);
+ }
+ SimpleAdapter listItemAdapter = new ListPrefSettingAdapter(context, listItem,
+ R.layout.setting_item,
+ new String[] {"text", "image"},
+ new int[] {R.id.text, R.id.image});
+ ((ListView) mSettingList).setAdapter(listItemAdapter);
+ ((ListView) mSettingList).setOnItemClickListener(this);
+ reloadPreference();
+ }
+
+ // The value of the preference may have changed. Update the UI.
+ @Override
+ public void reloadPreference() {
+ int index = mPreference.findIndexOfValue(mPreference.getValue());
+ if (index != -1) {
+ ((ListView) mSettingList).setItemChecked(index, true);
+ } else {
+ Log.e(TAG, "Invalid preference value.");
+ mPreference.print();
+ }
+ }
+
+ public void setSettingChangedListener(Listener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view,
+ int index, long id) {
+ mPreference.setValueIndex(index);
+ if (mListener != null) mListener.onListPrefChanged(mPreference);
+ }
+}
diff --git a/src/com/android/camera/ui/MoreSettingPopup.java b/src/com/android/camera/ui/MoreSettingPopup.java
new file mode 100644
index 000000000..5900058df
--- /dev/null
+++ b/src/com/android/camera/ui/MoreSettingPopup.java
@@ -0,0 +1,203 @@
+/*
+ * 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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import com.android.camera.ListPreference;
+import com.android.camera.PreferenceGroup;
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+/* A popup window that contains several camera settings. */
+public class MoreSettingPopup extends AbstractSettingPopup
+ implements InLineSettingItem.Listener,
+ AdapterView.OnItemClickListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MoreSettingPopup";
+
+ private Listener mListener;
+ private ArrayList<ListPreference> mListItem = new ArrayList<ListPreference>();
+
+ // Keep track of which setting items are disabled
+ // e.g. White balance will be disabled when scene mode is set to non-auto
+ private boolean[] mEnabled;
+
+ static public interface Listener {
+ public void onSettingChanged(ListPreference pref);
+ public void onPreferenceClicked(ListPreference pref);
+ }
+
+ private class MoreSettingAdapter extends ArrayAdapter<ListPreference> {
+ LayoutInflater mInflater;
+ String mOnString;
+ String mOffString;
+ MoreSettingAdapter() {
+ super(MoreSettingPopup.this.getContext(), 0, mListItem);
+ Context context = getContext();
+ mInflater = LayoutInflater.from(context);
+ mOnString = context.getString(R.string.setting_on);
+ mOffString = context.getString(R.string.setting_off);
+ }
+
+ private int getSettingLayoutId(ListPreference pref) {
+
+ if (isOnOffPreference(pref)) {
+ return R.layout.in_line_setting_check_box;
+ }
+ return R.layout.in_line_setting_menu;
+ }
+
+ private boolean isOnOffPreference(ListPreference pref) {
+ CharSequence[] entries = pref.getEntries();
+ if (entries.length != 2) return false;
+ String str1 = entries[0].toString();
+ String str2 = entries[1].toString();
+ return ((str1.equals(mOnString) && str2.equals(mOffString)) ||
+ (str1.equals(mOffString) && str2.equals(mOnString)));
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView != null) return convertView;
+
+ ListPreference pref = mListItem.get(position);
+
+ int viewLayoutId = getSettingLayoutId(pref);
+ InLineSettingItem view = (InLineSettingItem)
+ mInflater.inflate(viewLayoutId, parent, false);
+
+ view.initialize(pref); // no init for restore one
+ view.setSettingChangedListener(MoreSettingPopup.this);
+ if (position >= 0 && position < mEnabled.length) {
+ view.setEnabled(mEnabled[position]);
+ } else {
+ Log.w(TAG, "Invalid input: enabled list length, " + mEnabled.length
+ + " position " + position);
+ }
+ return view;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ if (position >= 0 && position < mEnabled.length) {
+ return mEnabled[position];
+ }
+ return true;
+ }
+ }
+
+ public void setSettingChangedListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public MoreSettingPopup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void initialize(PreferenceGroup group, String[] keys) {
+ // Prepare the setting items.
+ for (int i = 0; i < keys.length; ++i) {
+ ListPreference pref = group.findPreference(keys[i]);
+ if (pref != null) mListItem.add(pref);
+ }
+
+ ArrayAdapter<ListPreference> mListItemAdapter = new MoreSettingAdapter();
+ ((ListView) mSettingList).setAdapter(mListItemAdapter);
+ ((ListView) mSettingList).setOnItemClickListener(this);
+ ((ListView) mSettingList).setSelector(android.R.color.transparent);
+ // Initialize mEnabled
+ mEnabled = new boolean[mListItem.size()];
+ for (int i = 0; i < mEnabled.length; i++) {
+ mEnabled[i] = true;
+ }
+ }
+
+ // When preferences are disabled, we will display them grayed out. Users
+ // will not be able to change the disabled preferences, but they can still see
+ // the current value of the preferences
+ public void setPreferenceEnabled(String key, boolean enable) {
+ int count = mEnabled == null ? 0 : mEnabled.length;
+ for (int j = 0; j < count; j++) {
+ ListPreference pref = mListItem.get(j);
+ if (pref != null && key.equals(pref.getKey())) {
+ mEnabled[j] = enable;
+ break;
+ }
+ }
+ }
+
+ public void onSettingChanged(ListPreference pref) {
+ if (mListener != null) {
+ mListener.onSettingChanged(pref);
+ }
+ }
+
+ // Scene mode can override other camera settings (ex: flash mode).
+ public void overrideSettings(final String ... keyvalues) {
+ int count = mEnabled == null ? 0 : mEnabled.length;
+ for (int i = 0; i < keyvalues.length; i += 2) {
+ String key = keyvalues[i];
+ String value = keyvalues[i + 1];
+ for (int j = 0; j < count; j++) {
+ ListPreference pref = mListItem.get(j);
+ if (pref != null && key.equals(pref.getKey())) {
+ // Change preference
+ if (value != null) pref.setValue(value);
+ // If the preference is overridden, disable the preference
+ boolean enable = value == null;
+ mEnabled[j] = enable;
+ if (mSettingList.getChildCount() > j) {
+ mSettingList.getChildAt(j).setEnabled(enable);
+ }
+ }
+ }
+ }
+ reloadPreference();
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position,
+ long id) {
+ if (mListener != null) {
+ ListPreference pref = mListItem.get(position);
+ mListener.onPreferenceClicked(pref);
+ }
+ }
+
+ @Override
+ public void reloadPreference() {
+ int count = mSettingList.getChildCount();
+ for (int i = 0; i < count; i++) {
+ ListPreference pref = mListItem.get(i);
+ if (pref != null) {
+ InLineSettingItem settingItem =
+ (InLineSettingItem) mSettingList.getChildAt(i);
+ settingItem.reloadPreference();
+ }
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/OnIndicatorEventListener.java b/src/com/android/camera/ui/OnIndicatorEventListener.java
new file mode 100644
index 000000000..566f5c7a8
--- /dev/null
+++ b/src/com/android/camera/ui/OnIndicatorEventListener.java
@@ -0,0 +1,25 @@
+/*
+ * 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.camera.ui;
+
+public interface OnIndicatorEventListener {
+ public static int EVENT_ENTER_SECOND_LEVEL_INDICATOR_BAR = 0;
+ public static int EVENT_LEAVE_SECOND_LEVEL_INDICATOR_BAR = 1;
+ public static int EVENT_ENTER_ZOOM_CONTROL = 2;
+ public static int EVENT_LEAVE_ZOOM_CONTROL = 3;
+ void onIndicatorEvent(int event);
+}
diff --git a/src/com/android/camera/ui/OverlayRenderer.java b/src/com/android/camera/ui/OverlayRenderer.java
new file mode 100644
index 000000000..417e219aa
--- /dev/null
+++ b/src/com/android/camera/ui/OverlayRenderer.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+
+public abstract class OverlayRenderer implements RenderOverlay.Renderer {
+
+ private static final String TAG = "CAM OverlayRenderer";
+ protected RenderOverlay mOverlay;
+
+ protected int mLeft, mTop, mRight, mBottom;
+
+ protected boolean mVisible;
+
+ public void setVisible(boolean vis) {
+ mVisible = vis;
+ update();
+ }
+
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ // default does not handle touch
+ @Override
+ public boolean handlesTouch() {
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ return false;
+ }
+
+ public abstract void onDraw(Canvas canvas);
+
+ public void draw(Canvas canvas) {
+ if (mVisible) {
+ onDraw(canvas);
+ }
+ }
+
+ @Override
+ public void setOverlay(RenderOverlay overlay) {
+ mOverlay = overlay;
+ }
+
+ @Override
+ public void layout(int left, int top, int right, int bottom) {
+ mLeft = left;
+ mRight = right;
+ mTop = top;
+ mBottom = bottom;
+ }
+
+ protected Context getContext() {
+ if (mOverlay != null) {
+ return mOverlay.getContext();
+ } else {
+ return null;
+ }
+ }
+
+ public int getWidth() {
+ return mRight - mLeft;
+ }
+
+ public int getHeight() {
+ return mBottom - mTop;
+ }
+
+ protected void update() {
+ if (mOverlay != null) {
+ mOverlay.update();
+ }
+ }
+
+}
diff --git a/src/com/android/camera/ui/PieItem.java b/src/com/android/camera/ui/PieItem.java
new file mode 100644
index 000000000..47fe06758
--- /dev/null
+++ b/src/com/android/camera/ui/PieItem.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.drawable.Drawable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Pie menu item
+ */
+public class PieItem {
+
+ public static interface OnClickListener {
+ void onClick(PieItem item);
+ }
+
+ private Drawable mDrawable;
+ private int level;
+
+ private boolean mSelected;
+ private boolean mEnabled;
+ private List<PieItem> mItems;
+ private Path mPath;
+ private OnClickListener mOnClickListener;
+ private float mAlpha;
+ private CharSequence mLabel;
+
+ // Gray out the view when disabled
+ private static final float ENABLED_ALPHA = 1;
+ private static final float DISABLED_ALPHA = (float) 0.3;
+ private boolean mChangeAlphaWhenDisabled = true;
+
+ public PieItem(Drawable drawable, int level) {
+ mDrawable = drawable;
+ this.level = level;
+ if (drawable != null) {
+ setAlpha(1f);
+ }
+ mEnabled = true;
+ }
+
+ public void setLabel(CharSequence txt) {
+ mLabel = txt;
+ }
+
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ public boolean hasItems() {
+ return mItems != null;
+ }
+
+ public List<PieItem> getItems() {
+ return mItems;
+ }
+
+ public void addItem(PieItem item) {
+ if (mItems == null) {
+ mItems = new ArrayList<PieItem>();
+ }
+ mItems.add(item);
+ }
+
+ public void clearItems() {
+ mItems = null;
+ }
+
+ public void setLevel(int level) {
+ this.level = level;
+ }
+
+ public void setPath(Path p) {
+ mPath = p;
+ }
+
+ public Path getPath() {
+ return mPath;
+ }
+
+ public void setChangeAlphaWhenDisabled (boolean enable) {
+ mChangeAlphaWhenDisabled = enable;
+ }
+
+ public void setAlpha(float alpha) {
+ mAlpha = alpha;
+ mDrawable.setAlpha((int) (255 * alpha));
+ }
+
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ if (mChangeAlphaWhenDisabled) {
+ if (mEnabled) {
+ setAlpha(ENABLED_ALPHA);
+ } else {
+ setAlpha(DISABLED_ALPHA);
+ }
+ }
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void setSelected(boolean s) {
+ mSelected = s;
+ }
+
+ public boolean isSelected() {
+ return mSelected;
+ }
+
+ public int getLevel() {
+ return level;
+ }
+
+
+ public void setOnClickListener(OnClickListener listener) {
+ mOnClickListener = listener;
+ }
+
+ public void performClick() {
+ if (mOnClickListener != null) {
+ mOnClickListener.onClick(this);
+ }
+ }
+
+ public int getIntrinsicWidth() {
+ return mDrawable.getIntrinsicWidth();
+ }
+
+ public int getIntrinsicHeight() {
+ return mDrawable.getIntrinsicHeight();
+ }
+
+ public void setBounds(int left, int top, int right, int bottom) {
+ mDrawable.setBounds(left, top, right, bottom);
+ }
+
+ public void draw(Canvas canvas) {
+ mDrawable.draw(canvas);
+ }
+
+ public void setImageResource(Context context, int resId) {
+ Drawable d = context.getResources().getDrawable(resId).mutate();
+ d.setBounds(mDrawable.getBounds());
+ mDrawable = d;
+ setAlpha(mAlpha);
+ }
+
+}
diff --git a/src/com/android/camera/ui/PieMenuButton.java b/src/com/android/camera/ui/PieMenuButton.java
new file mode 100644
index 000000000..0e23226b2
--- /dev/null
+++ b/src/com/android/camera/ui/PieMenuButton.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2013 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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+public class PieMenuButton extends View {
+ private boolean mPressed;
+ private boolean mReadyToClick = false;
+ public PieMenuButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ mPressed = isPressed();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ boolean handled = super.onTouchEvent(event);
+ if (MotionEvent.ACTION_UP == event.getAction() && mPressed) {
+ // Perform a customized click as soon as the ACTION_UP event
+ // is received. The reason for doing this is that Framework
+ // delays the performClick() call after ACTION_UP. But we do not
+ // want the delay because it affects an important state change
+ // for PieRenderer.
+ mReadyToClick = true;
+ performClick();
+ }
+ return handled;
+ }
+
+ @Override
+ public boolean performClick() {
+ if (mReadyToClick) {
+ // We only respond to our customized click which happens right
+ // after ACTION_UP event is received, with no delay.
+ mReadyToClick = false;
+ return super.performClick();
+ }
+ return false;
+ }
+}; \ No newline at end of file
diff --git a/src/com/android/camera/ui/PieRenderer.java b/src/com/android/camera/ui/PieRenderer.java
new file mode 100644
index 000000000..c78107ce9
--- /dev/null
+++ b/src/com/android/camera/ui/PieRenderer.java
@@ -0,0 +1,1091 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.os.Message;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+
+import com.android.camera.drawable.TextDrawable;
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PieRenderer extends OverlayRenderer
+ implements FocusIndicator {
+
+ private static final String TAG = "CAM Pie";
+
+ // Sometimes continuous autofocus starts and stops several times quickly.
+ // These states are used to make sure the animation is run for at least some
+ // time.
+ private volatile int mState;
+ private ScaleAnimation mAnimation = new ScaleAnimation();
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_FOCUSING = 1;
+ private static final int STATE_FINISHING = 2;
+ private static final int STATE_PIE = 8;
+
+ private static final float MATH_PI_2 = (float)(Math.PI / 2);
+
+ private Runnable mDisappear = new Disappear();
+ private Animation.AnimationListener mEndAction = new EndAction();
+ private static final int SCALING_UP_TIME = 600;
+ private static final int SCALING_DOWN_TIME = 100;
+ private static final int DISAPPEAR_TIMEOUT = 200;
+ private static final int DIAL_HORIZONTAL = 157;
+ // fade out timings
+ private static final int PIE_FADE_OUT_DURATION = 600;
+
+ private static final long PIE_FADE_IN_DURATION = 200;
+ private static final long PIE_XFADE_DURATION = 200;
+ private static final long PIE_SELECT_FADE_DURATION = 300;
+ private static final long PIE_OPEN_SUB_DELAY = 400;
+ private static final long PIE_SLICE_DURATION = 80;
+
+ private static final int MSG_OPEN = 0;
+ private static final int MSG_CLOSE = 1;
+ private static final int MSG_OPENSUBMENU = 2;
+
+ protected static float CENTER = (float) Math.PI / 2;
+ protected static float RAD24 = (float)(24 * Math.PI / 180);
+ protected static final float SWEEP_SLICE = 0.14f;
+ protected static final float SWEEP_ARC = 0.23f;
+
+ // geometry
+ private int mRadius;
+ private int mRadiusInc;
+
+ // the detection if touch is inside a slice is offset
+ // inbounds by this amount to allow the selection to show before the
+ // finger covers it
+ private int mTouchOffset;
+
+ private List<PieItem> mOpen;
+
+ private Paint mSelectedPaint;
+ private Paint mSubPaint;
+ private Paint mMenuArcPaint;
+
+ // touch handling
+ private PieItem mCurrentItem;
+
+ private Paint mFocusPaint;
+ private int mSuccessColor;
+ private int mFailColor;
+ private int mCircleSize;
+ private int mFocusX;
+ private int mFocusY;
+ private int mCenterX;
+ private int mCenterY;
+ private int mArcCenterY;
+ private int mSliceCenterY;
+ private int mPieCenterX;
+ private int mPieCenterY;
+ private int mSliceRadius;
+ private int mArcRadius;
+ private int mArcOffset;
+
+ private int mDialAngle;
+ private RectF mCircle;
+ private RectF mDial;
+ private Point mPoint1;
+ private Point mPoint2;
+ private int mStartAnimationAngle;
+ private boolean mFocused;
+ private int mInnerOffset;
+ private int mOuterStroke;
+ private int mInnerStroke;
+ private boolean mTapMode;
+ private boolean mBlockFocus;
+ private int mTouchSlopSquared;
+ private Point mDown;
+ private boolean mOpening;
+ private ValueAnimator mXFade;
+ private ValueAnimator mFadeIn;
+ private ValueAnimator mFadeOut;
+ private ValueAnimator mSlice;
+ private volatile boolean mFocusCancelled;
+ private PointF mPolar = new PointF();
+ private TextDrawable mLabel;
+ private int mDeadZone;
+ private int mAngleZone;
+ private float mCenterAngle;
+
+
+
+ private Handler mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case MSG_OPEN:
+ if (mListener != null) {
+ mListener.onPieOpened(mPieCenterX, mPieCenterY);
+ }
+ break;
+ case MSG_CLOSE:
+ if (mListener != null) {
+ mListener.onPieClosed();
+ }
+ break;
+ case MSG_OPENSUBMENU:
+ onEnterOpen();
+ break;
+ }
+
+ }
+ };
+
+ private PieListener mListener;
+
+ static public interface PieListener {
+ public void onPieOpened(int centerX, int centerY);
+ public void onPieClosed();
+ }
+
+ public void setPieListener(PieListener pl) {
+ mListener = pl;
+ }
+
+ public PieRenderer(Context context) {
+ init(context);
+ }
+
+ private void init(Context ctx) {
+ setVisible(false);
+ mOpen = new ArrayList<PieItem>();
+ mOpen.add(new PieItem(null, 0));
+ Resources res = ctx.getResources();
+ mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start);
+ mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment);
+ mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
+ mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset);
+ mSelectedPaint = new Paint();
+ mSelectedPaint.setColor(Color.argb(255, 51, 181, 229));
+ mSelectedPaint.setAntiAlias(true);
+ mSubPaint = new Paint();
+ mSubPaint.setAntiAlias(true);
+ mSubPaint.setColor(Color.argb(200, 250, 230, 128));
+ mFocusPaint = new Paint();
+ mFocusPaint.setAntiAlias(true);
+ mFocusPaint.setColor(Color.WHITE);
+ mFocusPaint.setStyle(Paint.Style.STROKE);
+ mSuccessColor = Color.GREEN;
+ mFailColor = Color.RED;
+ mCircle = new RectF();
+ mDial = new RectF();
+ mPoint1 = new Point();
+ mPoint2 = new Point();
+ mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset);
+ mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
+ mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
+ mState = STATE_IDLE;
+ mBlockFocus = false;
+ mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop();
+ mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared;
+ mDown = new Point();
+ mMenuArcPaint = new Paint();
+ mMenuArcPaint.setAntiAlias(true);
+ mMenuArcPaint.setColor(Color.argb(140, 255, 255, 255));
+ mMenuArcPaint.setStrokeWidth(10);
+ mMenuArcPaint.setStyle(Paint.Style.STROKE);
+ mSliceRadius = res.getDimensionPixelSize(R.dimen.pie_item_radius);
+ mArcRadius = res.getDimensionPixelSize(R.dimen.pie_arc_radius);
+ mArcOffset = res.getDimensionPixelSize(R.dimen.pie_arc_offset);
+ mLabel = new TextDrawable(res);
+ mLabel.setDropShadow(true);
+ mDeadZone = res.getDimensionPixelSize(R.dimen.pie_deadzone_width);
+ mAngleZone = res.getDimensionPixelSize(R.dimen.pie_anglezone_width);
+ }
+
+ private PieItem getRoot() {
+ return mOpen.get(0);
+ }
+
+ public boolean showsItems() {
+ return mTapMode;
+ }
+
+ public void addItem(PieItem item) {
+ // add the item to the pie itself
+ getRoot().addItem(item);
+ }
+
+ public void clearItems() {
+ getRoot().clearItems();
+ }
+
+ public void showInCenter() {
+ if ((mState == STATE_PIE) && isVisible()) {
+ mTapMode = false;
+ show(false);
+ } else {
+ if (mState != STATE_IDLE) {
+ cancelFocus();
+ }
+ mState = STATE_PIE;
+ resetPieCenter();
+ setCenter(mPieCenterX, mPieCenterY);
+ mTapMode = true;
+ show(true);
+ }
+ }
+
+ public void hide() {
+ show(false);
+ }
+
+ /**
+ * guaranteed has center set
+ * @param show
+ */
+ private void show(boolean show) {
+ if (show) {
+ if (mXFade != null) {
+ mXFade.cancel();
+ }
+ mState = STATE_PIE;
+ // ensure clean state
+ mCurrentItem = null;
+ PieItem root = getRoot();
+ for (PieItem openItem : mOpen) {
+ if (openItem.hasItems()) {
+ for (PieItem item : openItem.getItems()) {
+ item.setSelected(false);
+ }
+ }
+ }
+ mLabel.setText("");
+ mOpen.clear();
+ mOpen.add(root);
+ layoutPie();
+ fadeIn();
+ } else {
+ mState = STATE_IDLE;
+ mTapMode = false;
+ if (mXFade != null) {
+ mXFade.cancel();
+ }
+ if (mLabel != null) {
+ mLabel.setText("");
+ }
+ }
+ setVisible(show);
+ mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
+ }
+
+ public boolean isOpen() {
+ return mState == STATE_PIE && isVisible();
+ }
+
+ private void fadeIn() {
+ mFadeIn = new ValueAnimator();
+ mFadeIn.setFloatValues(0f, 1f);
+ mFadeIn.setDuration(PIE_FADE_IN_DURATION);
+ // linear interpolation
+ mFadeIn.setInterpolator(null);
+ mFadeIn.addListener(new AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mFadeIn = null;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationCancel(Animator arg0) {
+ }
+ });
+ mFadeIn.start();
+ }
+
+ public void setCenter(int x, int y) {
+ mPieCenterX = x;
+ mPieCenterY = y;
+ mSliceCenterY = y + mSliceRadius - mArcOffset;
+ mArcCenterY = y - mArcOffset + mArcRadius;
+ }
+
+ @Override
+ public void layout(int l, int t, int r, int b) {
+ super.layout(l, t, r, b);
+ mCenterX = (r - l) / 2;
+ mCenterY = (b - t) / 2;
+
+ mFocusX = mCenterX;
+ mFocusY = mCenterY;
+ resetPieCenter();
+ setCircle(mFocusX, mFocusY);
+ if (isVisible() && mState == STATE_PIE) {
+ setCenter(mPieCenterX, mPieCenterY);
+ layoutPie();
+ }
+ }
+
+ private void resetPieCenter() {
+ mPieCenterX = mCenterX;
+ mPieCenterY = (int) (getHeight() - 2.5f * mDeadZone);
+ }
+
+ private void layoutPie() {
+ mCenterAngle = getCenterAngle();
+ layoutItems(0, getRoot().getItems());
+ layoutLabel(getLevel());
+ }
+
+ private void layoutLabel(int level) {
+ int x = mPieCenterX - (int) (FloatMath.sin(mCenterAngle - CENTER)
+ * (mArcRadius + (level + 2) * mRadiusInc));
+ int y = mArcCenterY - mArcRadius - (level + 2) * mRadiusInc;
+ int w = mLabel.getIntrinsicWidth();
+ int h = mLabel.getIntrinsicHeight();
+ mLabel.setBounds(x - w/2, y - h/2, x + w/2, y + h/2);
+ }
+
+ private void layoutItems(int level, List<PieItem> items) {
+ int extend = 1;
+ Path path = makeSlice(getDegrees(0) + extend, getDegrees(SWEEP_ARC) - extend,
+ mArcRadius, mArcRadius + mRadiusInc + mRadiusInc / 4,
+ mPieCenterX, mArcCenterY - level * mRadiusInc);
+ final int count = items.size();
+ int pos = 0;
+ for (PieItem item : items) {
+ // shared between items
+ item.setPath(path);
+ float angle = getArcCenter(item, pos, count);
+ int w = item.getIntrinsicWidth();
+ int h = item.getIntrinsicHeight();
+ // move views to outer border
+ int r = mArcRadius + mRadiusInc * 2 / 3;
+ int x = (int) (r * Math.cos(angle));
+ int y = mArcCenterY - (level * mRadiusInc) - (int) (r * Math.sin(angle)) - h / 2;
+ x = mPieCenterX + x - w / 2;
+ item.setBounds(x, y, x + w, y + h);
+ item.setLevel(level);
+ if (item.hasItems()) {
+ layoutItems(level + 1, item.getItems());
+ }
+ pos++;
+ }
+ }
+
+ private Path makeSlice(float start, float end, int inner, int outer, int cx, int cy) {
+ RectF bb =
+ new RectF(cx - outer, cy - outer, cx + outer,
+ cy + outer);
+ RectF bbi =
+ new RectF(cx - inner, cy - inner, cx + inner,
+ cy + inner);
+ Path path = new Path();
+ path.arcTo(bb, start, end - start, true);
+ path.arcTo(bbi, end, start - end);
+ path.close();
+ return path;
+ }
+
+ private float getArcCenter(PieItem item, int pos, int count) {
+ return getCenter(pos, count, SWEEP_ARC);
+ }
+
+ private float getSliceCenter(PieItem item, int pos, int count) {
+ float center = (getCenterAngle() - CENTER) * 0.5f + CENTER;
+ return center + (count - 1) * SWEEP_SLICE / 2f
+ - pos * SWEEP_SLICE;
+ }
+
+ private float getCenter(int pos, int count, float sweep) {
+ return mCenterAngle + (count - 1) * sweep / 2f - pos * sweep;
+ }
+
+ private float getCenterAngle() {
+ float center = CENTER;
+ if (mPieCenterX < mDeadZone + mAngleZone) {
+ center = CENTER - (mAngleZone - mPieCenterX + mDeadZone) * RAD24
+ / (float) mAngleZone;
+ } else if (mPieCenterX > getWidth() - mDeadZone - mAngleZone) {
+ center = CENTER + (mPieCenterX - (getWidth() - mDeadZone - mAngleZone)) * RAD24
+ / (float) mAngleZone;
+ }
+ return center;
+ }
+
+ /**
+ * converts a
+ * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
+ * @return skia angle
+ */
+ private float getDegrees(double angle) {
+ return (float) (360 - 180 * angle / Math.PI);
+ }
+
+ private void startFadeOut(final PieItem item) {
+ if (mFadeIn != null) {
+ mFadeIn.cancel();
+ }
+ if (mXFade != null) {
+ mXFade.cancel();
+ }
+ mFadeOut = new ValueAnimator();
+ mFadeOut.setFloatValues(1f, 0f);
+ mFadeOut.setDuration(PIE_FADE_OUT_DURATION);
+ mFadeOut.addListener(new AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ item.performClick();
+ mFadeOut = null;
+ deselect();
+ show(false);
+ mOverlay.setAlpha(1);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ }
+
+ });
+ mFadeOut.start();
+ }
+
+ // root does not count
+ private boolean hasOpenItem() {
+ return mOpen.size() > 1;
+ }
+
+ // pop an item of the open item stack
+ private PieItem closeOpenItem() {
+ PieItem item = getOpenItem();
+ mOpen.remove(mOpen.size() -1);
+ return item;
+ }
+
+ private PieItem getOpenItem() {
+ return mOpen.get(mOpen.size() - 1);
+ }
+
+ // return the children either the root or parent of the current open item
+ private PieItem getParent() {
+ return mOpen.get(Math.max(0, mOpen.size() - 2));
+ }
+
+ private int getLevel() {
+ return mOpen.size() - 1;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ float alpha = 1;
+ if (mXFade != null) {
+ alpha = (Float) mXFade.getAnimatedValue();
+ } else if (mFadeIn != null) {
+ alpha = (Float) mFadeIn.getAnimatedValue();
+ } else if (mFadeOut != null) {
+ alpha = (Float) mFadeOut.getAnimatedValue();
+ }
+ int state = canvas.save();
+ if (mFadeIn != null) {
+ float sf = 0.9f + alpha * 0.1f;
+ canvas.scale(sf, sf, mPieCenterX, mPieCenterY);
+ }
+ if (mState != STATE_PIE) {
+ drawFocus(canvas);
+ }
+ if (mState == STATE_FINISHING) {
+ canvas.restoreToCount(state);
+ return;
+ }
+ if (mState != STATE_PIE) return;
+ if (!hasOpenItem() || (mXFade != null)) {
+ // draw base menu
+ drawArc(canvas, getLevel(), getParent());
+ List<PieItem> items = getParent().getItems();
+ final int count = items.size();
+ int pos = 0;
+ for (PieItem item : getParent().getItems()) {
+ drawItem(Math.max(0, mOpen.size() - 2), pos, count, canvas, item, alpha);
+ pos++;
+ }
+ mLabel.draw(canvas);
+ }
+ if (hasOpenItem()) {
+ int level = getLevel();
+ drawArc(canvas, level, getOpenItem());
+ List<PieItem> items = getOpenItem().getItems();
+ final int count = items.size();
+ int pos = 0;
+ for (PieItem inner : items) {
+ if (mFadeOut != null) {
+ drawItem(level, pos, count, canvas, inner, alpha);
+ } else {
+ drawItem(level, pos, count, canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
+ }
+ pos++;
+ }
+ mLabel.draw(canvas);
+ }
+ canvas.restoreToCount(state);
+ }
+
+ private void drawArc(Canvas canvas, int level, PieItem item) {
+ // arc
+ if (mState == STATE_PIE) {
+ final int count = item.getItems().size();
+ float start = mCenterAngle + (count * SWEEP_ARC / 2f);
+ float end = mCenterAngle - (count * SWEEP_ARC / 2f);
+ int cy = mArcCenterY - level * mRadiusInc;
+ canvas.drawArc(new RectF(mPieCenterX - mArcRadius, cy - mArcRadius,
+ mPieCenterX + mArcRadius, cy + mArcRadius),
+ getDegrees(end), getDegrees(start) - getDegrees(end), false, mMenuArcPaint);
+ }
+ }
+
+ private void drawItem(int level, int pos, int count, Canvas canvas, PieItem item, float alpha) {
+ if (mState == STATE_PIE) {
+ if (item.getPath() != null) {
+ int y = mArcCenterY - level * mRadiusInc;
+ if (item.isSelected()) {
+ Paint p = mSelectedPaint;
+ int state = canvas.save();
+ float angle = 0;
+ if (mSlice != null) {
+ angle = (Float) mSlice.getAnimatedValue();
+ } else {
+ angle = getArcCenter(item, pos, count) - SWEEP_ARC / 2f;
+ }
+ angle = getDegrees(angle);
+ canvas.rotate(angle, mPieCenterX, y);
+ if (mFadeOut != null) {
+ p.setAlpha((int)(255 * alpha));
+ }
+ canvas.drawPath(item.getPath(), p);
+ if (mFadeOut != null) {
+ p.setAlpha(255);
+ }
+ canvas.restoreToCount(state);
+ }
+ if (mFadeOut == null) {
+ alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
+ // draw the item view
+ item.setAlpha(alpha);
+ }
+ item.draw(canvas);
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ float x = evt.getX();
+ float y = evt.getY();
+ int action = evt.getActionMasked();
+ getPolar(x, y, !mTapMode, mPolar);
+ if (MotionEvent.ACTION_DOWN == action) {
+ if ((x < mDeadZone) || (x > getWidth() - mDeadZone)) {
+ return false;
+ }
+ mDown.x = (int) evt.getX();
+ mDown.y = (int) evt.getY();
+ mOpening = false;
+ if (mTapMode) {
+ PieItem item = findItem(mPolar);
+ if ((item != null) && (mCurrentItem != item)) {
+ mState = STATE_PIE;
+ onEnter(item);
+ }
+ } else {
+ setCenter((int) x, (int) y);
+ show(true);
+ }
+ return true;
+ } else if (MotionEvent.ACTION_UP == action) {
+ if (isVisible()) {
+ PieItem item = mCurrentItem;
+ if (mTapMode) {
+ item = findItem(mPolar);
+ if (mOpening) {
+ mOpening = false;
+ return true;
+ }
+ }
+ if (item == null) {
+ mTapMode = false;
+ show(false);
+ } else if (!mOpening && !item.hasItems()) {
+ startFadeOut(item);
+ mTapMode = false;
+ } else {
+ mTapMode = true;
+ }
+ return true;
+ }
+ } else if (MotionEvent.ACTION_CANCEL == action) {
+ if (isVisible() || mTapMode) {
+ show(false);
+ }
+ deselect();
+ mHandler.removeMessages(MSG_OPENSUBMENU);
+ return false;
+ } else if (MotionEvent.ACTION_MOVE == action) {
+ if (pulledToCenter(mPolar)) {
+ mHandler.removeMessages(MSG_OPENSUBMENU);
+ if (hasOpenItem()) {
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ closeOpenItem();
+ mCurrentItem = null;
+ } else {
+ deselect();
+ }
+ mLabel.setText("");
+ return false;
+ }
+ PieItem item = findItem(mPolar);
+ boolean moved = hasMoved(evt);
+ if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
+ mHandler.removeMessages(MSG_OPENSUBMENU);
+ // only select if we didn't just open or have moved past slop
+ if (moved) {
+ // switch back to swipe mode
+ mTapMode = false;
+ }
+ onEnterSelect(item);
+ mHandler.sendEmptyMessageDelayed(MSG_OPENSUBMENU, PIE_OPEN_SUB_DELAY);
+ }
+ }
+ return false;
+ }
+
+ private boolean pulledToCenter(PointF polarCoords) {
+ return polarCoords.y < mArcRadius - mRadiusInc;
+ }
+
+ private boolean inside(PointF polar, PieItem item, int pos, int count) {
+ float start = getSliceCenter(item, pos, count) - SWEEP_SLICE / 2f;
+ boolean res = (mArcRadius < polar.y)
+ && (start < polar.x)
+ && (start + SWEEP_SLICE > polar.x)
+ && (!mTapMode || (mArcRadius + mRadiusInc > polar.y));
+ return res;
+ }
+
+ private void getPolar(float x, float y, boolean useOffset, PointF res) {
+ // get angle and radius from x/y
+ res.x = (float) Math.PI / 2;
+ x = x - mPieCenterX;
+ float y1 = mSliceCenterY - getLevel() * mRadiusInc - y;
+ float y2 = mArcCenterY - getLevel() * mRadiusInc - y;
+ res.y = (float) Math.sqrt(x * x + y2 * y2);
+ if (x != 0) {
+ res.x = (float) Math.atan2(y1, x);
+ if (res.x < 0) {
+ res.x = (float) (2 * Math.PI + res.x);
+ }
+ }
+ res.y = res.y + (useOffset ? mTouchOffset : 0);
+ }
+
+ private boolean hasMoved(MotionEvent e) {
+ return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x)
+ + (e.getY() - mDown.y) * (e.getY() - mDown.y);
+ }
+
+ private void onEnterSelect(PieItem item) {
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ if (item != null && item.isEnabled()) {
+ moveSelection(mCurrentItem, item);
+ item.setSelected(true);
+ mCurrentItem = item;
+ mLabel.setText(mCurrentItem.getLabel());
+ layoutLabel(getLevel());
+ } else {
+ mCurrentItem = null;
+ }
+ }
+
+ private void onEnterOpen() {
+ if ((mCurrentItem != null) && (mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
+ openCurrentItem();
+ }
+ }
+
+ /**
+ * enter a slice for a view
+ * updates model only
+ * @param item
+ */
+ private void onEnter(PieItem item) {
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ if (item != null && item.isEnabled()) {
+ item.setSelected(true);
+ mCurrentItem = item;
+ mLabel.setText(mCurrentItem.getLabel());
+ if ((mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
+ openCurrentItem();
+ layoutLabel(getLevel());
+ }
+ } else {
+ mCurrentItem = null;
+ }
+ }
+
+ private void deselect() {
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ if (hasOpenItem()) {
+ PieItem item = closeOpenItem();
+ onEnter(item);
+ } else {
+ mCurrentItem = null;
+ }
+ }
+
+ private int getItemPos(PieItem target) {
+ List<PieItem> items = getOpenItem().getItems();
+ return items.indexOf(target);
+ }
+
+ private int getCurrentCount() {
+ return getOpenItem().getItems().size();
+ }
+
+ private void moveSelection(PieItem from, PieItem to) {
+ final int count = getCurrentCount();
+ final int fromPos = getItemPos(from);
+ final int toPos = getItemPos(to);
+ if (fromPos != -1 && toPos != -1) {
+ float startAngle = getArcCenter(from, getItemPos(from), count)
+ - SWEEP_ARC / 2f;
+ float endAngle = getArcCenter(to, getItemPos(to), count)
+ - SWEEP_ARC / 2f;
+ mSlice = new ValueAnimator();
+ mSlice.setFloatValues(startAngle, endAngle);
+ // linear interpolater
+ mSlice.setInterpolator(null);
+ mSlice.setDuration(PIE_SLICE_DURATION);
+ mSlice.addListener(new AnimatorListener() {
+ @Override
+ public void onAnimationEnd(Animator arg0) {
+ mSlice = null;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator arg0) {
+ }
+
+ @Override
+ public void onAnimationStart(Animator arg0) {
+ }
+
+ @Override
+ public void onAnimationCancel(Animator arg0) {
+ }
+ });
+ mSlice.start();
+ }
+ }
+
+ private void openCurrentItem() {
+ if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
+ mOpen.add(mCurrentItem);
+ layoutLabel(getLevel());
+ mOpening = true;
+ if (mFadeIn != null) {
+ mFadeIn.cancel();
+ }
+ mXFade = new ValueAnimator();
+ mXFade.setFloatValues(1f, 0f);
+ mXFade.setDuration(PIE_XFADE_DURATION);
+ // Linear interpolation
+ mXFade.setInterpolator(null);
+ final PieItem ci = mCurrentItem;
+ mXFade.addListener(new AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mXFade = null;
+ ci.setSelected(false);
+ mOpening = false;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationCancel(Animator arg0) {
+ }
+ });
+ mXFade.start();
+ }
+ }
+
+ /**
+ * @param polar x: angle, y: dist
+ * @return the item at angle/dist or null
+ */
+ private PieItem findItem(PointF polar) {
+ // find the matching item:
+ List<PieItem> items = getOpenItem().getItems();
+ final int count = items.size();
+ int pos = 0;
+ for (PieItem item : items) {
+ if (inside(polar, item, pos, count)) {
+ return item;
+ }
+ pos++;
+ }
+ return null;
+ }
+
+
+ @Override
+ public boolean handlesTouch() {
+ return true;
+ }
+
+ // focus specific code
+
+ public void setBlockFocus(boolean blocked) {
+ mBlockFocus = blocked;
+ if (blocked) {
+ clear();
+ }
+ }
+
+ public void setFocus(int x, int y) {
+ mFocusX = x;
+ mFocusY = y;
+ setCircle(mFocusX, mFocusY);
+ }
+
+ public void alignFocus(int x, int y) {
+ mOverlay.removeCallbacks(mDisappear);
+ mAnimation.cancel();
+ mAnimation.reset();
+ mFocusX = x;
+ mFocusY = y;
+ mDialAngle = DIAL_HORIZONTAL;
+ setCircle(x, y);
+ mFocused = false;
+ }
+
+ public int getSize() {
+ return 2 * mCircleSize;
+ }
+
+ private int getRandomRange() {
+ return (int)(-60 + 120 * Math.random());
+ }
+
+ private void setCircle(int cx, int cy) {
+ mCircle.set(cx - mCircleSize, cy - mCircleSize,
+ cx + mCircleSize, cy + mCircleSize);
+ mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset,
+ cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset);
+ }
+
+ public void drawFocus(Canvas canvas) {
+ if (mBlockFocus) return;
+ mFocusPaint.setStrokeWidth(mOuterStroke);
+ canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
+ if (mState == STATE_PIE) return;
+ int color = mFocusPaint.getColor();
+ if (mState == STATE_FINISHING) {
+ mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor);
+ }
+ mFocusPaint.setStrokeWidth(mInnerStroke);
+ drawLine(canvas, mDialAngle, mFocusPaint);
+ drawLine(canvas, mDialAngle + 45, mFocusPaint);
+ drawLine(canvas, mDialAngle + 180, mFocusPaint);
+ drawLine(canvas, mDialAngle + 225, mFocusPaint);
+ canvas.save();
+ // rotate the arc instead of its offset to better use framework's shape caching
+ canvas.rotate(mDialAngle, mFocusX, mFocusY);
+ canvas.drawArc(mDial, 0, 45, false, mFocusPaint);
+ canvas.drawArc(mDial, 180, 45, false, mFocusPaint);
+ canvas.restore();
+ mFocusPaint.setColor(color);
+ }
+
+ private void drawLine(Canvas canvas, int angle, Paint p) {
+ convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
+ convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
+ canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY,
+ mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
+ }
+
+ private static void convertCart(int angle, int radius, Point out) {
+ double a = 2 * Math.PI * (angle % 360) / 360;
+ out.x = (int) (radius * Math.cos(a) + 0.5);
+ out.y = (int) (radius * Math.sin(a) + 0.5);
+ }
+
+ @Override
+ public void showStart() {
+ if (mState == STATE_PIE) return;
+ cancelFocus();
+ mStartAnimationAngle = 67;
+ int range = getRandomRange();
+ startAnimation(SCALING_UP_TIME,
+ false, mStartAnimationAngle, mStartAnimationAngle + range);
+ mState = STATE_FOCUSING;
+ }
+
+ @Override
+ public void showSuccess(boolean timeout) {
+ if (mState == STATE_FOCUSING) {
+ startAnimation(SCALING_DOWN_TIME,
+ timeout, mStartAnimationAngle);
+ mState = STATE_FINISHING;
+ mFocused = true;
+ }
+ }
+
+ @Override
+ public void showFail(boolean timeout) {
+ if (mState == STATE_FOCUSING) {
+ startAnimation(SCALING_DOWN_TIME,
+ timeout, mStartAnimationAngle);
+ mState = STATE_FINISHING;
+ mFocused = false;
+ }
+ }
+
+ private void cancelFocus() {
+ mFocusCancelled = true;
+ mOverlay.removeCallbacks(mDisappear);
+ if (mAnimation != null && !mAnimation.hasEnded()) {
+ mAnimation.cancel();
+ }
+ mFocusCancelled = false;
+ mFocused = false;
+ mState = STATE_IDLE;
+ }
+
+ @Override
+ public void clear() {
+ if (mState == STATE_PIE) return;
+ cancelFocus();
+ mOverlay.post(mDisappear);
+ }
+
+ private void startAnimation(long duration, boolean timeout,
+ float toScale) {
+ startAnimation(duration, timeout, mDialAngle,
+ toScale);
+ }
+
+ private void startAnimation(long duration, boolean timeout,
+ float fromScale, float toScale) {
+ setVisible(true);
+ mAnimation.reset();
+ mAnimation.setDuration(duration);
+ mAnimation.setScale(fromScale, toScale);
+ mAnimation.setAnimationListener(timeout ? mEndAction : null);
+ mOverlay.startAnimation(mAnimation);
+ update();
+ }
+
+ private class EndAction implements Animation.AnimationListener {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // Keep the focus indicator for some time.
+ if (!mFocusCancelled) {
+ mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+ }
+
+ private class Disappear implements Runnable {
+ @Override
+ public void run() {
+ if (mState == STATE_PIE) return;
+ setVisible(false);
+ mFocusX = mCenterX;
+ mFocusY = mCenterY;
+ mState = STATE_IDLE;
+ setCircle(mFocusX, mFocusY);
+ mFocused = false;
+ }
+ }
+
+ private class ScaleAnimation extends Animation {
+ private float mFrom = 1f;
+ private float mTo = 1f;
+
+ public ScaleAnimation() {
+ setFillAfter(true);
+ }
+
+ public void setScale(float from, float to) {
+ mFrom = from;
+ mTo = to;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime);
+ }
+ }
+
+}
diff --git a/src/com/android/camera/ui/PopupManager.java b/src/com/android/camera/ui/PopupManager.java
new file mode 100644
index 000000000..0dcf34fd7
--- /dev/null
+++ b/src/com/android/camera/ui/PopupManager.java
@@ -0,0 +1,66 @@
+/*
+ * 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.camera.ui;
+
+import android.content.Context;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * A manager which notifies the event of a new popup in order to dismiss the
+ * old popup if exists.
+ */
+public class PopupManager {
+ private static HashMap<Context, PopupManager> sMap =
+ new HashMap<Context, PopupManager>();
+
+ public interface OnOtherPopupShowedListener {
+ public void onOtherPopupShowed();
+ }
+
+ private PopupManager() {}
+
+ private ArrayList<OnOtherPopupShowedListener> mListeners = new ArrayList<OnOtherPopupShowedListener>();
+
+ public void notifyShowPopup(View view) {
+ for (OnOtherPopupShowedListener listener : mListeners) {
+ if ((View) listener != view) {
+ listener.onOtherPopupShowed();
+ }
+ }
+ }
+
+ public void setOnOtherPopupShowedListener(OnOtherPopupShowedListener listener) {
+ mListeners.add(listener);
+ }
+
+ public static PopupManager getInstance(Context context) {
+ PopupManager instance = sMap.get(context);
+ if (instance == null) {
+ instance = new PopupManager();
+ sMap.put(context, instance);
+ }
+ return instance;
+ }
+
+ public static void removeInstance(Context context) {
+ PopupManager instance = sMap.get(context);
+ sMap.remove(context);
+ }
+}
diff --git a/src/com/android/camera/ui/PreviewSurfaceView.java b/src/com/android/camera/ui/PreviewSurfaceView.java
new file mode 100644
index 000000000..9a428e23c
--- /dev/null
+++ b/src/com/android/camera/ui/PreviewSurfaceView.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+
+import com.android.gallery3d.common.ApiHelper;
+
+public class PreviewSurfaceView extends SurfaceView {
+ public PreviewSurfaceView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setZOrderMediaOverlay(true);
+ getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+ }
+
+ public void shrink() {
+ setLayoutSize(1);
+ }
+
+ public void expand() {
+ setLayoutSize(ViewGroup.LayoutParams.MATCH_PARENT);
+ }
+
+ private void setLayoutSize(int size) {
+ ViewGroup.LayoutParams p = getLayoutParams();
+ if (p.width != size || p.height != size) {
+ p.width = size;
+ p.height = size;
+ setLayoutParams(p);
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/RenderOverlay.java b/src/com/android/camera/ui/RenderOverlay.java
new file mode 100644
index 000000000..d82ce18b6
--- /dev/null
+++ b/src/com/android/camera/ui/RenderOverlay.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.camera.PreviewGestures;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class RenderOverlay extends FrameLayout {
+
+ private static final String TAG = "CAM_Overlay";
+
+ interface Renderer {
+
+ public boolean handlesTouch();
+ public boolean onTouchEvent(MotionEvent evt);
+ public void setOverlay(RenderOverlay overlay);
+ public void layout(int left, int top, int right, int bottom);
+ public void draw(Canvas canvas);
+
+ }
+
+ private RenderView mRenderView;
+ private List<Renderer> mClients;
+ private PreviewGestures mGestures;
+ // reverse list of touch clients
+ private List<Renderer> mTouchClients;
+ private int[] mPosition = new int[2];
+
+ public RenderOverlay(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mRenderView = new RenderView(context);
+ addView(mRenderView, new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT));
+ mClients = new ArrayList<Renderer>(10);
+ mTouchClients = new ArrayList<Renderer>(10);
+ setWillNotDraw(false);
+ }
+
+ public void setGestures(PreviewGestures gestures) {
+ mGestures = gestures;
+ }
+
+ public void addRenderer(Renderer renderer) {
+ mClients.add(renderer);
+ renderer.setOverlay(this);
+ if (renderer.handlesTouch()) {
+ mTouchClients.add(0, renderer);
+ }
+ renderer.layout(getLeft(), getTop(), getRight(), getBottom());
+ }
+
+ public void addRenderer(int pos, Renderer renderer) {
+ mClients.add(pos, renderer);
+ renderer.setOverlay(this);
+ renderer.layout(getLeft(), getTop(), getRight(), getBottom());
+ }
+
+ public void remove(Renderer renderer) {
+ mClients.remove(renderer);
+ renderer.setOverlay(null);
+ }
+
+ public int getClientSize() {
+ return mClients.size();
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent m) {
+ if (mGestures != null) {
+ if (!mGestures.isEnabled()) return false;
+ mGestures.dispatchTouch(m);
+ }
+ return true;
+ }
+
+ public boolean directDispatchTouch(MotionEvent m, Renderer target) {
+ mRenderView.setTouchTarget(target);
+ boolean res = mRenderView.dispatchTouchEvent(m);
+ mRenderView.setTouchTarget(null);
+ return res;
+ }
+
+ private void adjustPosition() {
+ getLocationInWindow(mPosition);
+ }
+
+ public int getWindowPositionX() {
+ return mPosition[0];
+ }
+
+ public int getWindowPositionY() {
+ return mPosition[1];
+ }
+
+ public void update() {
+ mRenderView.invalidate();
+ }
+
+ private class RenderView extends View {
+
+ private Renderer mTouchTarget;
+
+ public RenderView(Context context) {
+ super(context);
+ setWillNotDraw(false);
+ }
+
+ public void setTouchTarget(Renderer target) {
+ mTouchTarget = target;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent evt) {
+
+ if (mTouchTarget != null) {
+ return mTouchTarget.onTouchEvent(evt);
+ }
+ if (mTouchClients != null) {
+ boolean res = false;
+ for (Renderer client : mTouchClients) {
+ res |= client.onTouchEvent(evt);
+ }
+ return res;
+ }
+ return false;
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ adjustPosition();
+ super.onLayout(changed, left, top, right, bottom);
+ if (mClients == null) return;
+ for (Renderer renderer : mClients) {
+ renderer.layout(left, top, right, bottom);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (mClients == null) return;
+ boolean redraw = false;
+ for (Renderer renderer : mClients) {
+ renderer.draw(canvas);
+ redraw = redraw || ((OverlayRenderer) renderer).isVisible();
+ }
+ if (redraw) {
+ invalidate();
+ }
+ }
+ }
+
+}
diff --git a/src/com/android/camera/ui/Rotatable.java b/src/com/android/camera/ui/Rotatable.java
new file mode 100644
index 000000000..6d428b8c6
--- /dev/null
+++ b/src/com/android/camera/ui/Rotatable.java
@@ -0,0 +1,22 @@
+/*
+ * 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.camera.ui;
+
+public interface Rotatable {
+ // Set parameter 'animation' to true to have animation when rotation.
+ public void setOrientation(int orientation, boolean animation);
+}
diff --git a/src/com/android/camera/ui/RotatableLayout.java b/src/com/android/camera/ui/RotatableLayout.java
new file mode 100644
index 000000000..965d62a90
--- /dev/null
+++ b/src/com/android/camera/ui/RotatableLayout.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2013 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.camera.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.camera.Util;
+
+/* RotatableLayout rotates itself as well as all its children when orientation
+ * changes. Specifically, when going from portrait to landscape, camera
+ * controls move from the bottom of the screen to right side of the screen
+ * (i.e. counter clockwise). Similarly, when the screen changes to portrait, we
+ * need to move the controls from right side to the bottom of the screen, which
+ * is a clockwise rotation.
+ */
+
+public class RotatableLayout extends FrameLayout {
+
+ private static final String TAG = "RotatableLayout";
+ // Initial orientation of the layout (ORIENTATION_PORTRAIT, or ORIENTATION_LANDSCAPE)
+ private int mInitialOrientation;
+ private int mPrevRotation;
+ private RotationListener mListener = null;
+ public interface RotationListener {
+ public void onRotation(int rotation);
+ }
+ public RotatableLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mInitialOrientation = getResources().getConfiguration().orientation;
+ }
+
+ public RotatableLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mInitialOrientation = getResources().getConfiguration().orientation;
+ }
+
+ public RotatableLayout(Context context) {
+ super(context);
+ mInitialOrientation = getResources().getConfiguration().orientation;
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ mPrevRotation = Util.getDisplayRotation((Activity) getContext());
+ // check if there is any rotation before the view is attached to window
+ int currentOrientation = getResources().getConfiguration().orientation;
+ int orientation = getUnifiedRotation();
+ if (mInitialOrientation == currentOrientation && orientation < 180) {
+ return;
+ }
+
+ if (mInitialOrientation == Configuration.ORIENTATION_LANDSCAPE
+ && currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
+ rotateLayout(true);
+ } else if (mInitialOrientation == Configuration.ORIENTATION_PORTRAIT
+ && currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+ rotateLayout(false);
+ }
+ // In reverse landscape and reverse portrait, camera controls will be laid out
+ // on the wrong side of the screen. We need to make adjustment to move the controls
+ // to the USB side
+ if (orientation >= 180) {
+ flipChildren();
+ }
+ }
+
+ protected int getUnifiedRotation() {
+ // all the layout code assumes camera device orientation to be portrait
+ // adjust rotation for landscape
+ int orientation = getResources().getConfiguration().orientation;
+ int rotation = Util.getDisplayRotation((Activity) getContext());
+ int camOrientation = (rotation % 180 == 0) ? Configuration.ORIENTATION_PORTRAIT
+ : Configuration.ORIENTATION_LANDSCAPE;
+ if (camOrientation != orientation) {
+ return (rotation + 90) % 360;
+ }
+ return rotation;
+ }
+
+ public void checkLayoutFlip() {
+ int currentRotation = Util.getDisplayRotation((Activity) getContext());
+ if ((currentRotation - mPrevRotation + 360) % 360 == 180) {
+ mPrevRotation = currentRotation;
+ flipChildren();
+ getParent().requestLayout();
+ }
+ }
+
+ @Override
+ public void onWindowVisibilityChanged(int visibility) {
+ if (visibility == View.VISIBLE) {
+ // Make sure when coming back from onPause, the layout is rotated correctly
+ checkLayoutFlip();
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration config) {
+ super.onConfigurationChanged(config);
+ int rotation = Util.getDisplayRotation((Activity) getContext());
+ int diff = (rotation - mPrevRotation + 360) % 360;
+ if ( diff == 0) {
+ // No rotation
+ return;
+ } else if (diff == 180) {
+ // 180-degree rotation
+ mPrevRotation = rotation;
+ flipChildren();
+ return;
+ }
+ // 90 or 270-degree rotation
+ boolean clockwise = isClockWiseRotation(mPrevRotation, rotation);
+ mPrevRotation = rotation;
+ rotateLayout(clockwise);
+ }
+
+ protected void rotateLayout(boolean clockwise) {
+ // Change the size of the layout
+ ViewGroup.LayoutParams lp = getLayoutParams();
+ int width = lp.width;
+ int height = lp.height;
+ lp.height = width;
+ lp.width = height;
+ setLayoutParams(lp);
+
+ // rotate all the children
+ rotateChildren(clockwise);
+ }
+
+ protected void rotateChildren(boolean clockwise) {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ rotate(child, clockwise);
+ }
+ if (mListener != null) mListener.onRotation(clockwise ? 90 : 270);
+ }
+
+ protected void flipChildren() {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ flip(child);
+ }
+ if (mListener != null) mListener.onRotation(180);
+ }
+
+ public void setRotationListener(RotationListener listener) {
+ mListener = listener;
+ }
+
+ public static boolean isClockWiseRotation(int prevRotation, int currentRotation) {
+ if (prevRotation == (currentRotation + 90) % 360) {
+ return true;
+ }
+ return false;
+ }
+
+ public static void rotate(View view, boolean isClockwise) {
+ if (isClockwise) {
+ rotateClockwise(view);
+ } else {
+ rotateCounterClockwise(view);
+ }
+ }
+
+ private static boolean contains(int value, int mask) {
+ return (value & mask) == mask;
+ }
+
+ public static void rotateClockwise(View view) {
+ if (view == null) return;
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ int gravity = lp.gravity;
+ int ngravity = 0;
+ // rotate gravity
+ if (contains(gravity, Gravity.LEFT)) {
+ ngravity |= Gravity.TOP;
+ }
+ if (contains(gravity, Gravity.RIGHT)) {
+ ngravity |= Gravity.BOTTOM;
+ }
+ if (contains(gravity, Gravity.TOP)) {
+ ngravity |= Gravity.RIGHT;
+ }
+ if (contains(gravity, Gravity.BOTTOM)) {
+ ngravity |= Gravity.LEFT;
+ }
+ if (contains(gravity, Gravity.CENTER)) {
+ ngravity |= Gravity.CENTER;
+ }
+ if (contains(gravity, Gravity.CENTER_HORIZONTAL)) {
+ ngravity |= Gravity.CENTER_VERTICAL;
+ }
+ if (contains(gravity, Gravity.CENTER_VERTICAL)) {
+ ngravity |= Gravity.CENTER_HORIZONTAL;
+ }
+ lp.gravity = ngravity;
+ int ml = lp.leftMargin;
+ int mr = lp.rightMargin;
+ int mt = lp.topMargin;
+ int mb = lp.bottomMargin;
+ lp.leftMargin = mb;
+ lp.rightMargin = mt;
+ lp.topMargin = ml;
+ lp.bottomMargin = mr;
+ int width = lp.width;
+ int height = lp.height;
+ lp.width = height;
+ lp.height = width;
+ view.setLayoutParams(lp);
+ }
+
+ public static void rotateCounterClockwise(View view) {
+ if (view == null) return;
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ int gravity = lp.gravity;
+ int ngravity = 0;
+ // change gravity
+ if (contains(gravity, Gravity.RIGHT)) {
+ ngravity |= Gravity.TOP;
+ }
+ if (contains(gravity, Gravity.LEFT)) {
+ ngravity |= Gravity.BOTTOM;
+ }
+ if (contains(gravity, Gravity.TOP)) {
+ ngravity |= Gravity.LEFT;
+ }
+ if (contains(gravity, Gravity.BOTTOM)) {
+ ngravity |= Gravity.RIGHT;
+ }
+ if (contains(gravity, Gravity.CENTER)) {
+ ngravity |= Gravity.CENTER;
+ }
+ if (contains(gravity, Gravity.CENTER_HORIZONTAL)) {
+ ngravity |= Gravity.CENTER_VERTICAL;
+ }
+ if (contains(gravity, Gravity.CENTER_VERTICAL)) {
+ ngravity |= Gravity.CENTER_HORIZONTAL;
+ }
+ lp.gravity = ngravity;
+ int ml = lp.leftMargin;
+ int mr = lp.rightMargin;
+ int mt = lp.topMargin;
+ int mb = lp.bottomMargin;
+ lp.leftMargin = mt;
+ lp.rightMargin = mb;
+ lp.topMargin = mr;
+ lp.bottomMargin = ml;
+ int width = lp.width;
+ int height = lp.height;
+ lp.width = height;
+ lp.height = width;
+ view.setLayoutParams(lp);
+ }
+
+ // Rotate a given view 180 degrees
+ public static void flip(View view) {
+ rotateClockwise(view);
+ rotateClockwise(view);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/camera/ui/RotateImageView.java b/src/com/android/camera/ui/RotateImageView.java
new file mode 100644
index 000000000..05e1a7c5b
--- /dev/null
+++ b/src/com/android/camera/ui/RotateImageView.java
@@ -0,0 +1,176 @@
+/*
+ * 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.camera.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.media.ThumbnailUtils;
+import android.util.AttributeSet;
+import android.view.ViewGroup.LayoutParams;
+import android.view.animation.AnimationUtils;
+import android.widget.ImageView;
+
+/**
+ * A @{code ImageView} which can rotate it's content.
+ */
+public class RotateImageView extends TwoStateImageView implements Rotatable {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "RotateImageView";
+
+ private static final int ANIMATION_SPEED = 270; // 270 deg/sec
+
+ private int mCurrentDegree = 0; // [0, 359]
+ private int mStartDegree = 0;
+ private int mTargetDegree = 0;
+
+ private boolean mClockwise = false, mEnableAnimation = true;
+
+ private long mAnimationStartTime = 0;
+ private long mAnimationEndTime = 0;
+
+ public RotateImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public RotateImageView(Context context) {
+ super(context);
+ }
+
+ protected int getDegree() {
+ return mTargetDegree;
+ }
+
+ // Rotate the view counter-clockwise
+ @Override
+ public void setOrientation(int degree, boolean animation) {
+ mEnableAnimation = animation;
+ // make sure in the range of [0, 359]
+ degree = degree >= 0 ? degree % 360 : degree % 360 + 360;
+ if (degree == mTargetDegree) return;
+
+ mTargetDegree = degree;
+ if (mEnableAnimation) {
+ mStartDegree = mCurrentDegree;
+ mAnimationStartTime = AnimationUtils.currentAnimationTimeMillis();
+
+ int diff = mTargetDegree - mCurrentDegree;
+ diff = diff >= 0 ? diff : 360 + diff; // make it in range [0, 359]
+
+ // Make it in range [-179, 180]. That's the shorted distance between the
+ // two angles
+ diff = diff > 180 ? diff - 360 : diff;
+
+ mClockwise = diff >= 0;
+ mAnimationEndTime = mAnimationStartTime
+ + Math.abs(diff) * 1000 / ANIMATION_SPEED;
+ } else {
+ mCurrentDegree = mTargetDegree;
+ }
+
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ Drawable drawable = getDrawable();
+ if (drawable == null) return;
+
+ Rect bounds = drawable.getBounds();
+ int w = bounds.right - bounds.left;
+ int h = bounds.bottom - bounds.top;
+
+ if (w == 0 || h == 0) return; // nothing to draw
+
+ if (mCurrentDegree != mTargetDegree) {
+ long time = AnimationUtils.currentAnimationTimeMillis();
+ if (time < mAnimationEndTime) {
+ int deltaTime = (int)(time - mAnimationStartTime);
+ int degree = mStartDegree + ANIMATION_SPEED
+ * (mClockwise ? deltaTime : -deltaTime) / 1000;
+ degree = degree >= 0 ? degree % 360 : degree % 360 + 360;
+ mCurrentDegree = degree;
+ invalidate();
+ } else {
+ mCurrentDegree = mTargetDegree;
+ }
+ }
+
+ int left = getPaddingLeft();
+ int top = getPaddingTop();
+ int right = getPaddingRight();
+ int bottom = getPaddingBottom();
+ int width = getWidth() - left - right;
+ int height = getHeight() - top - bottom;
+
+ int saveCount = canvas.getSaveCount();
+
+ // Scale down the image first if required.
+ if ((getScaleType() == ImageView.ScaleType.FIT_CENTER) &&
+ ((width < w) || (height < h))) {
+ float ratio = Math.min((float) width / w, (float) height / h);
+ canvas.scale(ratio, ratio, width / 2.0f, height / 2.0f);
+ }
+ canvas.translate(left + width / 2, top + height / 2);
+ canvas.rotate(-mCurrentDegree);
+ canvas.translate(-w / 2, -h / 2);
+ drawable.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+
+ private Bitmap mThumb;
+ private Drawable[] mThumbs;
+ private TransitionDrawable mThumbTransition;
+
+ public void setBitmap(Bitmap bitmap) {
+ // Make sure uri and original are consistently both null or both
+ // non-null.
+ if (bitmap == null) {
+ mThumb = null;
+ mThumbs = null;
+ setImageDrawable(null);
+ setVisibility(GONE);
+ return;
+ }
+
+ LayoutParams param = getLayoutParams();
+ final int miniThumbWidth = param.width
+ - getPaddingLeft() - getPaddingRight();
+ final int miniThumbHeight = param.height
+ - getPaddingTop() - getPaddingBottom();
+ mThumb = ThumbnailUtils.extractThumbnail(
+ bitmap, miniThumbWidth, miniThumbHeight);
+ Drawable drawable;
+ if (mThumbs == null || !mEnableAnimation) {
+ mThumbs = new Drawable[2];
+ mThumbs[1] = new BitmapDrawable(getContext().getResources(), mThumb);
+ setImageDrawable(mThumbs[1]);
+ } else {
+ mThumbs[0] = mThumbs[1];
+ mThumbs[1] = new BitmapDrawable(getContext().getResources(), mThumb);
+ mThumbTransition = new TransitionDrawable(mThumbs);
+ setImageDrawable(mThumbTransition);
+ mThumbTransition.startTransition(500);
+ }
+ setVisibility(VISIBLE);
+ }
+}
diff --git a/src/com/android/camera/ui/RotateLayout.java b/src/com/android/camera/ui/RotateLayout.java
new file mode 100644
index 000000000..86f5c814d
--- /dev/null
+++ b/src/com/android/camera/ui/RotateLayout.java
@@ -0,0 +1,203 @@
+/*
+ * 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.camera.ui;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.util.MotionEventHelper;
+
+// A RotateLayout is designed to display a single item and provides the
+// capabilities to rotate the item.
+public class RotateLayout extends ViewGroup implements Rotatable {
+ @SuppressWarnings("unused")
+ private static final String TAG = "RotateLayout";
+ private int mOrientation;
+ private Matrix mMatrix = new Matrix();
+ protected View mChild;
+
+ public RotateLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ // The transparent background here is a workaround of the render issue
+ // happened when the view is rotated as the device's orientation
+ // changed. The view looks fine in landscape. After rotation, the view
+ // is invisible.
+ setBackgroundResource(android.R.color.transparent);
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ @Override
+ protected void onFinishInflate() {
+ mChild = getChildAt(0);
+ if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) {
+ mChild.setPivotX(0);
+ mChild.setPivotY(0);
+ }
+ }
+
+ @Override
+ protected void onLayout(
+ boolean change, int left, int top, int right, int bottom) {
+ int width = right - left;
+ int height = bottom - top;
+ switch (mOrientation) {
+ case 0:
+ case 180:
+ mChild.layout(0, 0, width, height);
+ break;
+ case 90:
+ case 270:
+ mChild.layout(0, 0, height, width);
+ break;
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ if (!ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) {
+ final int w = getMeasuredWidth();
+ final int h = getMeasuredHeight();
+ switch (mOrientation) {
+ case 0:
+ mMatrix.setTranslate(0, 0);
+ break;
+ case 90:
+ mMatrix.setTranslate(0, -h);
+ break;
+ case 180:
+ mMatrix.setTranslate(-w, -h);
+ break;
+ case 270:
+ mMatrix.setTranslate(-w, 0);
+ break;
+ }
+ mMatrix.postRotate(mOrientation);
+ event = MotionEventHelper.transformEvent(event, mMatrix);
+ }
+ return super.dispatchTouchEvent(event);
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) {
+ super.dispatchDraw(canvas);
+ } else {
+ canvas.save();
+ int w = getMeasuredWidth();
+ int h = getMeasuredHeight();
+ switch (mOrientation) {
+ case 0:
+ canvas.translate(0, 0);
+ break;
+ case 90:
+ canvas.translate(0, h);
+ break;
+ case 180:
+ canvas.translate(w, h);
+ break;
+ case 270:
+ canvas.translate(w, 0);
+ break;
+ }
+ canvas.rotate(-mOrientation, 0, 0);
+ super.dispatchDraw(canvas);
+ canvas.restore();
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ int w = 0, h = 0;
+ switch(mOrientation) {
+ case 0:
+ case 180:
+ measureChild(mChild, widthSpec, heightSpec);
+ w = mChild.getMeasuredWidth();
+ h = mChild.getMeasuredHeight();
+ break;
+ case 90:
+ case 270:
+ measureChild(mChild, heightSpec, widthSpec);
+ w = mChild.getMeasuredHeight();
+ h = mChild.getMeasuredWidth();
+ break;
+ }
+ setMeasuredDimension(w, h);
+
+ if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) {
+ switch (mOrientation) {
+ case 0:
+ mChild.setTranslationX(0);
+ mChild.setTranslationY(0);
+ break;
+ case 90:
+ mChild.setTranslationX(0);
+ mChild.setTranslationY(h);
+ break;
+ case 180:
+ mChild.setTranslationX(w);
+ mChild.setTranslationY(h);
+ break;
+ case 270:
+ mChild.setTranslationX(w);
+ mChild.setTranslationY(0);
+ break;
+ }
+ mChild.setRotation(-mOrientation);
+ }
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+
+ // Rotate the view counter-clockwise
+ @Override
+ public void setOrientation(int orientation, boolean animation) {
+ orientation = orientation % 360;
+ if (mOrientation == orientation) return;
+ mOrientation = orientation;
+ requestLayout();
+ }
+
+ public int getOrientation() {
+ return mOrientation;
+ }
+
+ @Override
+ public ViewParent invalidateChildInParent(int[] location, Rect r) {
+ if (!ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES && mOrientation != 0) {
+ // The workaround invalidates the entire rotate layout. After
+ // rotation, the correct area to invalidate may be larger than the
+ // size of the child. Ex: ListView. There is no way to invalidate
+ // only the necessary area.
+ r.set(0, 0, getWidth(), getHeight());
+ }
+ return super.invalidateChildInParent(location, r);
+ }
+}
diff --git a/src/com/android/camera/ui/RotateTextToast.java b/src/com/android/camera/ui/RotateTextToast.java
new file mode 100644
index 000000000..c78a258b0
--- /dev/null
+++ b/src/com/android/camera/ui/RotateTextToast.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.camera.ui;
+
+import android.app.Activity;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.camera.Util;
+import com.android.gallery3d.R;
+
+public class RotateTextToast {
+ private static final int TOAST_DURATION = 5000; // milliseconds
+ ViewGroup mLayoutRoot;
+ RotateLayout mToast;
+ Handler mHandler;
+
+ public RotateTextToast(Activity activity, int textResourceId, int orientation) {
+ mLayoutRoot = (ViewGroup) activity.getWindow().getDecorView();
+ LayoutInflater inflater = activity.getLayoutInflater();
+ View v = inflater.inflate(R.layout.rotate_text_toast, mLayoutRoot);
+ mToast = (RotateLayout) v.findViewById(R.id.rotate_toast);
+ TextView tv = (TextView) mToast.findViewById(R.id.message);
+ tv.setText(textResourceId);
+ mToast.setOrientation(orientation, false);
+ mHandler = new Handler();
+ }
+
+ private final Runnable mRunnable = new Runnable() {
+ @Override
+ public void run() {
+ Util.fadeOut(mToast);
+ mLayoutRoot.removeView(mToast);
+ mToast = null;
+ }
+ };
+
+ public void show() {
+ mToast.setVisibility(View.VISIBLE);
+ mHandler.postDelayed(mRunnable, TOAST_DURATION);
+ }
+}
diff --git a/src/com/android/camera/ui/Switch.java b/src/com/android/camera/ui/Switch.java
new file mode 100644
index 000000000..ac21758a7
--- /dev/null
+++ b/src/com/android/camera/ui/Switch.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.CompoundButton;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.Arrays;
+
+/**
+ * A Switch is a two-state toggle switch widget that can select between two
+ * options. The user may drag the "thumb" back and forth to choose the selected option,
+ * or simply tap to toggle as if it were a checkbox.
+ */
+public class Switch extends CompoundButton {
+ private static final int TOUCH_MODE_IDLE = 0;
+ private static final int TOUCH_MODE_DOWN = 1;
+ private static final int TOUCH_MODE_DRAGGING = 2;
+
+ private Drawable mThumbDrawable;
+ private Drawable mTrackDrawable;
+ private int mThumbTextPadding;
+ private int mSwitchMinWidth;
+ private int mSwitchTextMaxWidth;
+ private int mSwitchPadding;
+ private CharSequence mTextOn;
+ private CharSequence mTextOff;
+
+ private int mTouchMode;
+ private int mTouchSlop;
+ private float mTouchX;
+ private float mTouchY;
+ private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+ private int mMinFlingVelocity;
+
+ private float mThumbPosition;
+ private int mSwitchWidth;
+ private int mSwitchHeight;
+ private int mThumbWidth; // Does not include padding
+
+ private int mSwitchLeft;
+ private int mSwitchTop;
+ private int mSwitchRight;
+ private int mSwitchBottom;
+
+ private TextPaint mTextPaint;
+ private ColorStateList mTextColors;
+ private Layout mOnLayout;
+ private Layout mOffLayout;
+
+ @SuppressWarnings("hiding")
+ private final Rect mTempRect = new Rect();
+
+ private static final int[] CHECKED_STATE_SET = {
+ android.R.attr.state_checked
+ };
+
+ /**
+ * Construct a new Switch with default styling, overriding specific style
+ * attributes as requested.
+ *
+ * @param context The Context that will determine this widget's theming.
+ * @param attrs Specification of attributes that should deviate from default styling.
+ */
+ public Switch(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.switchStyle);
+ }
+
+ /**
+ * Construct a new Switch with a default style determined by the given theme attribute,
+ * overriding specific style attributes as requested.
+ *
+ * @param context The Context that will determine this widget's theming.
+ * @param attrs Specification of attributes that should deviate from the default styling.
+ * @param defStyle An attribute ID within the active theme containing a reference to the
+ * default style for this widget. e.g. android.R.attr.switchStyle.
+ */
+ public Switch(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+ Resources res = getResources();
+ DisplayMetrics dm = res.getDisplayMetrics();
+ mTextPaint.density = dm.density;
+ mThumbDrawable = res.getDrawable(R.drawable.switch_inner_holo_dark);
+ mTrackDrawable = res.getDrawable(R.drawable.switch_track_holo_dark);
+ mTextOn = res.getString(R.string.capital_on);
+ mTextOff = res.getString(R.string.capital_off);
+ mThumbTextPadding = res.getDimensionPixelSize(R.dimen.thumb_text_padding);
+ mSwitchMinWidth = res.getDimensionPixelSize(R.dimen.switch_min_width);
+ mSwitchTextMaxWidth = res.getDimensionPixelSize(R.dimen.switch_text_max_width);
+ mSwitchPadding = res.getDimensionPixelSize(R.dimen.switch_padding);
+ setSwitchTextAppearance(context, android.R.style.TextAppearance_Holo_Small);
+
+ ViewConfiguration config = ViewConfiguration.get(context);
+ mTouchSlop = config.getScaledTouchSlop();
+ mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
+
+ // Refresh display with current params
+ refreshDrawableState();
+ setChecked(isChecked());
+ }
+
+ /**
+ * Sets the switch text color, size, style, hint color, and highlight color
+ * from the specified TextAppearance resource.
+ */
+ public void setSwitchTextAppearance(Context context, int resid) {
+ Resources res = getResources();
+ mTextColors = getTextColors();
+ int ts = res.getDimensionPixelSize(R.dimen.thumb_text_size);
+ if (ts != mTextPaint.getTextSize()) {
+ mTextPaint.setTextSize(ts);
+ requestLayout();
+ }
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ if (mOnLayout == null) {
+ mOnLayout = makeLayout(mTextOn, mSwitchTextMaxWidth);
+ }
+ if (mOffLayout == null) {
+ mOffLayout = makeLayout(mTextOff, mSwitchTextMaxWidth);
+ }
+
+ mTrackDrawable.getPadding(mTempRect);
+ final int maxTextWidth = Math.min(mSwitchTextMaxWidth,
+ Math.max(mOnLayout.getWidth(), mOffLayout.getWidth()));
+ final int switchWidth = Math.max(mSwitchMinWidth,
+ maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right);
+ final int switchHeight = mTrackDrawable.getIntrinsicHeight();
+
+ mThumbWidth = maxTextWidth + mThumbTextPadding * 2;
+
+ mSwitchWidth = switchWidth;
+ mSwitchHeight = switchHeight;
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ final int measuredHeight = getMeasuredHeight();
+ final int measuredWidth = getMeasuredWidth();
+ if (measuredHeight < switchHeight) {
+ setMeasuredDimension(measuredWidth, switchHeight);
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ @Override
+ public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+ super.onPopulateAccessibilityEvent(event);
+ CharSequence text = isChecked() ? mOnLayout.getText() : mOffLayout.getText();
+ if (!TextUtils.isEmpty(text)) {
+ event.getText().add(text);
+ }
+ }
+
+ private Layout makeLayout(CharSequence text, int maxWidth) {
+ int actual_width = (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint));
+ StaticLayout l = new StaticLayout(text, 0, text.length(), mTextPaint,
+ actual_width,
+ Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true,
+ TextUtils.TruncateAt.END,
+ (int) Math.min(actual_width, maxWidth));
+ return l;
+ }
+
+ /**
+ * @return true if (x, y) is within the target area of the switch thumb
+ */
+ private boolean hitThumb(float x, float y) {
+ mThumbDrawable.getPadding(mTempRect);
+ final int thumbTop = mSwitchTop - mTouchSlop;
+ final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop;
+ final int thumbRight = thumbLeft + mThumbWidth +
+ mTempRect.left + mTempRect.right + mTouchSlop;
+ final int thumbBottom = mSwitchBottom + mTouchSlop;
+ return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ mVelocityTracker.addMovement(ev);
+ final int action = ev.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ if (isEnabled() && hitThumb(x, y)) {
+ mTouchMode = TOUCH_MODE_DOWN;
+ mTouchX = x;
+ mTouchY = y;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ switch (mTouchMode) {
+ case TOUCH_MODE_IDLE:
+ // Didn't target the thumb, treat normally.
+ break;
+
+ case TOUCH_MODE_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ if (Math.abs(x - mTouchX) > mTouchSlop ||
+ Math.abs(y - mTouchY) > mTouchSlop) {
+ mTouchMode = TOUCH_MODE_DRAGGING;
+ getParent().requestDisallowInterceptTouchEvent(true);
+ mTouchX = x;
+ mTouchY = y;
+ return true;
+ }
+ break;
+ }
+
+ case TOUCH_MODE_DRAGGING: {
+ final float x = ev.getX();
+ final float dx = x - mTouchX;
+ float newPos = Math.max(0,
+ Math.min(mThumbPosition + dx, getThumbScrollRange()));
+ if (newPos != mThumbPosition) {
+ mThumbPosition = newPos;
+ mTouchX = x;
+ invalidate();
+ }
+ return true;
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ if (mTouchMode == TOUCH_MODE_DRAGGING) {
+ stopDrag(ev);
+ return true;
+ }
+ mTouchMode = TOUCH_MODE_IDLE;
+ mVelocityTracker.clear();
+ break;
+ }
+ }
+
+ return super.onTouchEvent(ev);
+ }
+
+ private void cancelSuperTouch(MotionEvent ev) {
+ MotionEvent cancel = MotionEvent.obtain(ev);
+ cancel.setAction(MotionEvent.ACTION_CANCEL);
+ super.onTouchEvent(cancel);
+ cancel.recycle();
+ }
+
+ /**
+ * Called from onTouchEvent to end a drag operation.
+ *
+ * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
+ */
+ private void stopDrag(MotionEvent ev) {
+ mTouchMode = TOUCH_MODE_IDLE;
+ // Up and not canceled, also checks the switch has not been disabled during the drag
+ boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
+
+ cancelSuperTouch(ev);
+
+ if (commitChange) {
+ boolean newState;
+ mVelocityTracker.computeCurrentVelocity(1000);
+ float xvel = mVelocityTracker.getXVelocity();
+ if (Math.abs(xvel) > mMinFlingVelocity) {
+ newState = xvel > 0;
+ } else {
+ newState = getTargetCheckedState();
+ }
+ animateThumbToCheckedState(newState);
+ } else {
+ animateThumbToCheckedState(isChecked());
+ }
+ }
+
+ private void animateThumbToCheckedState(boolean newCheckedState) {
+ setChecked(newCheckedState);
+ }
+
+ private boolean getTargetCheckedState() {
+ return mThumbPosition >= getThumbScrollRange() / 2;
+ }
+
+ private void setThumbPosition(boolean checked) {
+ mThumbPosition = checked ? getThumbScrollRange() : 0;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ super.setChecked(checked);
+ setThumbPosition(checked);
+ invalidate();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ setThumbPosition(isChecked());
+
+ int switchRight;
+ int switchLeft;
+
+ switchRight = getWidth() - getPaddingRight();
+ switchLeft = switchRight - mSwitchWidth;
+
+ int switchTop = 0;
+ int switchBottom = 0;
+ switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
+ default:
+ case Gravity.TOP:
+ switchTop = getPaddingTop();
+ switchBottom = switchTop + mSwitchHeight;
+ break;
+
+ case Gravity.CENTER_VERTICAL:
+ switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
+ mSwitchHeight / 2;
+ switchBottom = switchTop + mSwitchHeight;
+ break;
+
+ case Gravity.BOTTOM:
+ switchBottom = getHeight() - getPaddingBottom();
+ switchTop = switchBottom - mSwitchHeight;
+ break;
+ }
+
+ mSwitchLeft = switchLeft;
+ mSwitchTop = switchTop;
+ mSwitchBottom = switchBottom;
+ mSwitchRight = switchRight;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ // Draw the switch
+ int switchLeft = mSwitchLeft;
+ int switchTop = mSwitchTop;
+ int switchRight = mSwitchRight;
+ int switchBottom = mSwitchBottom;
+
+ mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom);
+ mTrackDrawable.draw(canvas);
+
+ canvas.save();
+
+ mTrackDrawable.getPadding(mTempRect);
+ int switchInnerLeft = switchLeft + mTempRect.left;
+ int switchInnerTop = switchTop + mTempRect.top;
+ int switchInnerRight = switchRight - mTempRect.right;
+ int switchInnerBottom = switchBottom - mTempRect.bottom;
+ canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom);
+
+ mThumbDrawable.getPadding(mTempRect);
+ final int thumbPos = (int) (mThumbPosition + 0.5f);
+ int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos;
+ int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right;
+
+ mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
+ mThumbDrawable.draw(canvas);
+
+ // mTextColors should not be null, but just in case
+ if (mTextColors != null) {
+ mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(),
+ mTextColors.getDefaultColor()));
+ }
+ mTextPaint.drawableState = getDrawableState();
+
+ Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
+
+ canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getEllipsizedWidth() / 2,
+ (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2);
+ switchText.draw(canvas);
+
+ canvas.restore();
+ }
+
+ @Override
+ public int getCompoundPaddingRight() {
+ int padding = super.getCompoundPaddingRight() + mSwitchWidth;
+ if (!TextUtils.isEmpty(getText())) {
+ padding += mSwitchPadding;
+ }
+ return padding;
+ }
+
+ private int getThumbScrollRange() {
+ if (mTrackDrawable == null) {
+ return 0;
+ }
+ mTrackDrawable.getPadding(mTempRect);
+ return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right;
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+ if (isChecked()) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ int[] myDrawableState = getDrawableState();
+
+ // Set the state of the Drawable
+ // Drawable may be null when checked state is set from XML, from super constructor
+ if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState);
+ if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState);
+
+ invalidate();
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ mThumbDrawable.jumpToCurrentState();
+ mTrackDrawable.jumpToCurrentState();
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ @Override
+ public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(event);
+ event.setClassName(Switch.class.getName());
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(Switch.class.getName());
+ CharSequence switchText = isChecked() ? mTextOn : mTextOff;
+ if (!TextUtils.isEmpty(switchText)) {
+ CharSequence oldText = info.getText();
+ if (TextUtils.isEmpty(oldText)) {
+ info.setText(switchText);
+ } else {
+ StringBuilder newText = new StringBuilder();
+ newText.append(oldText).append(' ').append(switchText);
+ info.setText(newText);
+ }
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/TimeIntervalPopup.java b/src/com/android/camera/ui/TimeIntervalPopup.java
new file mode 100644
index 000000000..18ad9f5da
--- /dev/null
+++ b/src/com/android/camera/ui/TimeIntervalPopup.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.NumberPicker;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import com.android.camera.IconListPreference;
+import com.android.camera.ListPreference;
+import com.android.gallery3d.R;
+
+/**
+ * This is a popup window that allows users to turn on/off time lapse feature,
+ * and to select a time interval for taking a time lapse video.
+ */
+public class TimeIntervalPopup extends AbstractSettingPopup {
+ private static final String TAG = "TimeIntervalPopup";
+ private NumberPicker mNumberSpinner;
+ private NumberPicker mUnitSpinner;
+ private Switch mTimeLapseSwitch;
+ private final String[] mUnits;
+ private final String[] mDurations;
+ private IconListPreference mPreference;
+ private Listener mListener;
+ private Button mConfirmButton;
+ private TextView mHelpText;
+ private View mTimePicker;
+
+ static public interface Listener {
+ public void onListPrefChanged(ListPreference pref);
+ }
+
+ public void setSettingChangedListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public TimeIntervalPopup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ Resources res = context.getResources();
+ mUnits = res.getStringArray(R.array.pref_video_time_lapse_frame_interval_units);
+ mDurations = res
+ .getStringArray(R.array.pref_video_time_lapse_frame_interval_duration_values);
+ }
+
+ public void initialize(IconListPreference preference) {
+ mPreference = preference;
+
+ // Set title.
+ mTitle.setText(mPreference.getTitle());
+
+ // Duration
+ int durationCount = mDurations.length;
+ mNumberSpinner = (NumberPicker) findViewById(R.id.duration);
+ mNumberSpinner.setMinValue(0);
+ mNumberSpinner.setMaxValue(durationCount - 1);
+ mNumberSpinner.setDisplayedValues(mDurations);
+ mNumberSpinner.setWrapSelectorWheel(false);
+
+ // Units for duration (i.e. seconds, minutes, etc)
+ mUnitSpinner = (NumberPicker) findViewById(R.id.duration_unit);
+ mUnitSpinner.setMinValue(0);
+ mUnitSpinner.setMaxValue(mUnits.length - 1);
+ mUnitSpinner.setDisplayedValues(mUnits);
+ mUnitSpinner.setWrapSelectorWheel(false);
+
+ mTimePicker = findViewById(R.id.time_interval_picker);
+ mTimeLapseSwitch = (Switch) findViewById(R.id.time_lapse_switch);
+ mHelpText = (TextView) findViewById(R.id.set_time_interval_help_text);
+ mConfirmButton = (Button) findViewById(R.id.time_lapse_interval_set_button);
+
+ // Disable focus on the spinners to prevent keyboard from coming up
+ mNumberSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
+ mUnitSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS);
+
+ mTimeLapseSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ setTimeSelectionEnabled(isChecked);
+ }
+ });
+ mConfirmButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ updateInputState();
+ }
+ });
+ }
+
+ private void restoreSetting() {
+ int index = mPreference.findIndexOfValue(mPreference.getValue());
+ if (index == -1) {
+ Log.e(TAG, "Invalid preference value.");
+ mPreference.print();
+ throw new IllegalArgumentException();
+ } else if (index == 0) {
+ // default choice: time lapse off
+ mTimeLapseSwitch.setChecked(false);
+ setTimeSelectionEnabled(false);
+ } else {
+ mTimeLapseSwitch.setChecked(true);
+ setTimeSelectionEnabled(true);
+ int durationCount = mNumberSpinner.getMaxValue() + 1;
+ int unit = (index - 1) / durationCount;
+ int number = (index - 1) % durationCount;
+ mUnitSpinner.setValue(unit);
+ mNumberSpinner.setValue(number);
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ if (visibility == View.VISIBLE) {
+ if (getVisibility() != View.VISIBLE) {
+ // Set the number pickers and on/off switch to be consistent
+ // with the preference
+ restoreSetting();
+ }
+ }
+ super.setVisibility(visibility);
+ }
+
+ protected void setTimeSelectionEnabled(boolean enabled) {
+ mHelpText.setVisibility(enabled ? GONE : VISIBLE);
+ mTimePicker.setVisibility(enabled ? VISIBLE : GONE);
+ }
+
+ @Override
+ public void reloadPreference() {
+ }
+
+ private void updateInputState() {
+ if (mTimeLapseSwitch.isChecked()) {
+ int newId = mUnitSpinner.getValue() * (mNumberSpinner.getMaxValue() + 1)
+ + mNumberSpinner.getValue() + 1;
+ mPreference.setValueIndex(newId);
+ } else {
+ mPreference.setValueIndex(0);
+ }
+
+ if (mListener != null) {
+ mListener.onListPrefChanged(mPreference);
+ }
+ }
+}
diff --git a/src/com/android/camera/ui/TwoStateImageView.java b/src/com/android/camera/ui/TwoStateImageView.java
new file mode 100644
index 000000000..cd5b27fc1
--- /dev/null
+++ b/src/com/android/camera/ui/TwoStateImageView.java
@@ -0,0 +1,55 @@
+/*
+ * 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.camera.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * A @{code ImageView} which change the opacity of the icon if disabled.
+ */
+public class TwoStateImageView extends ImageView {
+ private static final int ENABLED_ALPHA = 255;
+ private static final int DISABLED_ALPHA = (int) (255 * 0.4);
+ private boolean mFilterEnabled = true;
+
+ public TwoStateImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public TwoStateImageView(Context context) {
+ this(context, null);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ if (mFilterEnabled) {
+ if (enabled) {
+ setAlpha(ENABLED_ALPHA);
+ } else {
+ setAlpha(DISABLED_ALPHA);
+ }
+ }
+ }
+
+ public void enableFilter(boolean enabled) {
+ mFilterEnabled = enabled;
+ }
+}
diff --git a/src/com/android/camera/ui/ZoomRenderer.java b/src/com/android/camera/ui/ZoomRenderer.java
new file mode 100644
index 000000000..86b82b459
--- /dev/null
+++ b/src/com/android/camera/ui/ZoomRenderer.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2012 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.camera.ui;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.view.ScaleGestureDetector;
+
+import com.android.gallery3d.R;
+
+public class ZoomRenderer extends OverlayRenderer
+ implements ScaleGestureDetector.OnScaleGestureListener {
+
+ private static final String TAG = "CAM_Zoom";
+
+ private int mMaxZoom;
+ private int mMinZoom;
+ private OnZoomChangedListener mListener;
+
+ private ScaleGestureDetector mDetector;
+ private Paint mPaint;
+ private Paint mTextPaint;
+ private int mCircleSize;
+ private int mCenterX;
+ private int mCenterY;
+ private float mMaxCircle;
+ private float mMinCircle;
+ private int mInnerStroke;
+ private int mOuterStroke;
+ private int mZoomSig;
+ private int mZoomFraction;
+ private Rect mTextBounds;
+
+ public interface OnZoomChangedListener {
+ void onZoomStart();
+ void onZoomEnd();
+ void onZoomValueChanged(int index); // only for immediate zoom
+ }
+
+ public ZoomRenderer(Context ctx) {
+ Resources res = ctx.getResources();
+ mPaint = new Paint();
+ mPaint.setAntiAlias(true);
+ mPaint.setColor(Color.WHITE);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mTextPaint = new Paint(mPaint);
+ mTextPaint.setStyle(Paint.Style.FILL);
+ mTextPaint.setTextSize(res.getDimensionPixelSize(R.dimen.zoom_font_size));
+ mTextPaint.setTextAlign(Paint.Align.LEFT);
+ mTextPaint.setAlpha(192);
+ mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
+ mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
+ mDetector = new ScaleGestureDetector(ctx, this);
+ mMinCircle = res.getDimensionPixelSize(R.dimen.zoom_ring_min);
+ mTextBounds = new Rect();
+ setVisible(false);
+ }
+
+ // set from module
+ public void setZoomMax(int zoomMaxIndex) {
+ mMaxZoom = zoomMaxIndex;
+ mMinZoom = 0;
+ }
+
+ public void setZoom(int index) {
+ mCircleSize = (int) (mMinCircle + index * (mMaxCircle - mMinCircle) / (mMaxZoom - mMinZoom));
+ }
+
+ public void setZoomValue(int value) {
+ value = value / 10;
+ mZoomSig = value / 10;
+ mZoomFraction = value % 10;
+ }
+
+ public void setOnZoomChangeListener(OnZoomChangedListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void layout(int l, int t, int r, int b) {
+ super.layout(l, t, r, b);
+ mCenterX = (r - l) / 2;
+ mCenterY = (b - t) / 2;
+ mMaxCircle = Math.min(getWidth(), getHeight());
+ mMaxCircle = (mMaxCircle - mMinCircle) / 2;
+ }
+
+ public boolean isScaling() {
+ return mDetector.isInProgress();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ mPaint.setStrokeWidth(mInnerStroke);
+ canvas.drawCircle(mCenterX, mCenterY, mMinCircle, mPaint);
+ canvas.drawCircle(mCenterX, mCenterY, mMaxCircle, mPaint);
+ canvas.drawLine(mCenterX - mMinCircle, mCenterY,
+ mCenterX - mMaxCircle - 4, mCenterY, mPaint);
+ mPaint.setStrokeWidth(mOuterStroke);
+ canvas.drawCircle((float) mCenterX, (float) mCenterY,
+ (float) mCircleSize, mPaint);
+ String txt = mZoomSig+"."+mZoomFraction+"x";
+ mTextPaint.getTextBounds(txt, 0, txt.length(), mTextBounds);
+ canvas.drawText(txt, mCenterX - mTextBounds.centerX(), mCenterY - mTextBounds.centerY(),
+ mTextPaint);
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ final float sf = detector.getScaleFactor();
+ float circle = (int) (mCircleSize * sf * sf);
+ circle = Math.max(mMinCircle, circle);
+ circle = Math.min(mMaxCircle, circle);
+ if (mListener != null && (int) circle != mCircleSize) {
+ mCircleSize = (int) circle;
+ int zoom = mMinZoom + (int) ((mCircleSize - mMinCircle) * (mMaxZoom - mMinZoom) / (mMaxCircle - mMinCircle));
+ mListener.onZoomValueChanged(zoom);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ setVisible(true);
+ if (mListener != null) {
+ mListener.onZoomStart();
+ }
+ update();
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ setVisible(false);
+ if (mListener != null) {
+ mListener.onZoomEnd();
+ }
+ }
+
+}
diff --git a/src/com/android/gallery3d/anim/AlphaAnimation.java b/src/com/android/gallery3d/anim/AlphaAnimation.java
new file mode 100644
index 000000000..f9f4cbd2c
--- /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.glrenderer.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..cc117bbce
--- /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 android.view.animation.Interpolator;
+
+import com.android.gallery3d.common.Utils;
+
+// 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/CanvasAnimation.java b/src/com/android/gallery3d/anim/CanvasAnimation.java
new file mode 100644
index 000000000..cdc66c6ba
--- /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.glrenderer.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/anim/StateTransitionAnimation.java b/src/com/android/gallery3d/anim/StateTransitionAnimation.java
new file mode 100644
index 000000000..bf8a54405
--- /dev/null
+++ b/src/com/android/gallery3d/anim/StateTransitionAnimation.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2012 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 android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.TiledScreenNail;
+
+public class StateTransitionAnimation extends Animation {
+
+ public static class Spec {
+ public static final Spec OUTGOING;
+ public static final Spec INCOMING;
+ public static final Spec PHOTO_INCOMING;
+
+ private static final Interpolator DEFAULT_INTERPOLATOR =
+ new DecelerateInterpolator();
+
+ public int duration = 330;
+ public float backgroundAlphaFrom = 0;
+ public float backgroundAlphaTo = 0;
+ public float backgroundScaleFrom = 0;
+ public float backgroundScaleTo = 0;
+ public float contentAlphaFrom = 1;
+ public float contentAlphaTo = 1;
+ public float contentScaleFrom = 1;
+ public float contentScaleTo = 1;
+ public float overlayAlphaFrom = 0;
+ public float overlayAlphaTo = 0;
+ public float overlayScaleFrom = 0;
+ public float overlayScaleTo = 0;
+ public Interpolator interpolator = DEFAULT_INTERPOLATOR;
+
+ static {
+ OUTGOING = new Spec();
+ OUTGOING.backgroundAlphaFrom = 0.5f;
+ OUTGOING.backgroundAlphaTo = 0f;
+ OUTGOING.backgroundScaleFrom = 1f;
+ OUTGOING.backgroundScaleTo = 0f;
+ OUTGOING.contentAlphaFrom = 0.5f;
+ OUTGOING.contentAlphaTo = 1f;
+ OUTGOING.contentScaleFrom = 3f;
+ OUTGOING.contentScaleTo = 1f;
+
+ INCOMING = new Spec();
+ INCOMING.overlayAlphaFrom = 1f;
+ INCOMING.overlayAlphaTo = 0f;
+ INCOMING.overlayScaleFrom = 1f;
+ INCOMING.overlayScaleTo = 3f;
+ INCOMING.contentAlphaFrom = 0f;
+ INCOMING.contentAlphaTo = 1f;
+ INCOMING.contentScaleFrom = 0.25f;
+ INCOMING.contentScaleTo = 1f;
+
+ PHOTO_INCOMING = INCOMING;
+ }
+
+ private static Spec specForTransition(Transition t) {
+ switch (t) {
+ case Outgoing:
+ return Spec.OUTGOING;
+ case Incoming:
+ return Spec.INCOMING;
+ case PhotoIncoming:
+ return Spec.PHOTO_INCOMING;
+ case None:
+ default:
+ return null;
+ }
+ }
+ }
+
+ public static enum Transition { None, Outgoing, Incoming, PhotoIncoming }
+
+ private final Spec mTransitionSpec;
+ private float mCurrentContentScale;
+ private float mCurrentContentAlpha;
+ private float mCurrentBackgroundScale;
+ private float mCurrentBackgroundAlpha;
+ private float mCurrentOverlayScale;
+ private float mCurrentOverlayAlpha;
+ private RawTexture mOldScreenTexture;
+
+ public StateTransitionAnimation(Transition t, RawTexture oldScreen) {
+ this(Spec.specForTransition(t), oldScreen);
+ }
+
+ public StateTransitionAnimation(Spec spec, RawTexture oldScreen) {
+ mTransitionSpec = spec != null ? spec : Spec.OUTGOING;
+ setDuration(mTransitionSpec.duration);
+ setInterpolator(mTransitionSpec.interpolator);
+ mOldScreenTexture = oldScreen;
+ TiledScreenNail.disableDrawPlaceholder();
+ }
+
+ @Override
+ public boolean calculate(long currentTimeMillis) {
+ boolean retval = super.calculate(currentTimeMillis);
+ if (!isActive()) {
+ if (mOldScreenTexture != null) {
+ mOldScreenTexture.recycle();
+ mOldScreenTexture = null;
+ }
+ TiledScreenNail.enableDrawPlaceholder();
+ }
+ return retval;
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mCurrentContentScale = mTransitionSpec.contentScaleFrom
+ + (mTransitionSpec.contentScaleTo - mTransitionSpec.contentScaleFrom) * progress;
+ mCurrentContentAlpha = mTransitionSpec.contentAlphaFrom
+ + (mTransitionSpec.contentAlphaTo - mTransitionSpec.contentAlphaFrom) * progress;
+ mCurrentBackgroundAlpha = mTransitionSpec.backgroundAlphaFrom
+ + (mTransitionSpec.backgroundAlphaTo - mTransitionSpec.backgroundAlphaFrom)
+ * progress;
+ mCurrentBackgroundScale = mTransitionSpec.backgroundScaleFrom
+ + (mTransitionSpec.backgroundScaleTo - mTransitionSpec.backgroundScaleFrom)
+ * progress;
+ mCurrentOverlayScale = mTransitionSpec.overlayScaleFrom
+ + (mTransitionSpec.overlayScaleTo - mTransitionSpec.overlayScaleFrom) * progress;
+ mCurrentOverlayAlpha = mTransitionSpec.overlayAlphaFrom
+ + (mTransitionSpec.overlayAlphaTo - mTransitionSpec.overlayAlphaFrom) * progress;
+ }
+
+ private void applyOldTexture(GLView view, GLCanvas canvas, float alpha, float scale, boolean clear) {
+ if (mOldScreenTexture == null)
+ return;
+ if (clear) canvas.clearBuffer(view.getBackgroundColor());
+ canvas.save();
+ canvas.setAlpha(alpha);
+ int xOffset = view.getWidth() / 2;
+ int yOffset = view.getHeight() / 2;
+ canvas.translate(xOffset, yOffset);
+ canvas.scale(scale, scale, 1);
+ mOldScreenTexture.draw(canvas, -xOffset, -yOffset);
+ canvas.restore();
+ }
+
+ public void applyBackground(GLView view, GLCanvas canvas) {
+ if (mCurrentBackgroundAlpha > 0f) {
+ applyOldTexture(view, canvas, mCurrentBackgroundAlpha, mCurrentBackgroundScale, true);
+ }
+ }
+
+ public void applyContentTransform(GLView view, GLCanvas canvas) {
+ int xOffset = view.getWidth() / 2;
+ int yOffset = view.getHeight() / 2;
+ canvas.translate(xOffset, yOffset);
+ canvas.scale(mCurrentContentScale, mCurrentContentScale, 1);
+ canvas.translate(-xOffset, -yOffset);
+ canvas.setAlpha(mCurrentContentAlpha);
+ }
+
+ public void applyOverlay(GLView view, GLCanvas canvas) {
+ if (mCurrentOverlayAlpha > 0f) {
+ applyOldTexture(view, canvas, mCurrentOverlayAlpha, mCurrentOverlayScale, false);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/AbstractGalleryActivity.java b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
new file mode 100644
index 000000000..ac39aa560
--- /dev/null
+++ b/src/com/android/gallery3d/app/AbstractGalleryActivity.java
@@ -0,0 +1,343 @@
+/*
+ * 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 android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+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.content.ServiceConnection;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRootView;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.photos.data.GalleryBitmapPool;
+
+public class AbstractGalleryActivity extends Activity implements GalleryContext {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AbstractGalleryActivity";
+ private GLRootView mGLRootView;
+ private StateManager mStateManager;
+ private GalleryActionBar mActionBar;
+ private OrientationManager mOrientationManager;
+ private TransitionStore mTransitionStore = new TransitionStore();
+ private boolean mDisableToggleStatusBar;
+ private PanoramaViewHelper mPanoramaViewHelper;
+
+ 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 onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mOrientationManager = new OrientationManager(this);
+ toggleStatusBarByOrientation();
+ getWindow().setBackgroundDrawable(null);
+ mPanoramaViewHelper = new PanoramaViewHelper(this);
+ mPanoramaViewHelper.onCreate();
+ doBindBatchService();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ mGLRootView.lockRenderThread();
+ try {
+ super.onSaveInstanceState(outState);
+ getStateManager().saveState(outState);
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration config) {
+ super.onConfigurationChanged(config);
+ mStateManager.onConfigurationChange(config);
+ getGalleryActionBar().onConfigurationChanged();
+ invalidateOptionsMenu();
+ toggleStatusBarByOrientation();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ return getStateManager().createOptionsMenu(menu);
+ }
+
+ @Override
+ public Context getAndroidContext() {
+ return this;
+ }
+
+ @Override
+ public DataManager getDataManager() {
+ return ((GalleryApp) getApplication()).getDataManager();
+ }
+
+ @Override
+ public ThreadPool getThreadPool() {
+ return ((GalleryApp) getApplication()).getThreadPool();
+ }
+
+ public synchronized StateManager getStateManager() {
+ if (mStateManager == null) {
+ mStateManager = new StateManager(this);
+ }
+ return mStateManager;
+ }
+
+ public GLRoot getGLRoot() {
+ return mGLRootView;
+ }
+
+ public OrientationManager getOrientationManager() {
+ return mOrientationManager;
+ }
+
+ @Override
+ public void setContentView(int resId) {
+ super.setContentView(resId);
+ mGLRootView = (GLRootView) findViewById(R.id.gl_root_view);
+ }
+
+ 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();
+ }
+ };
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.no_external_storage_title)
+ .setMessage(R.string.no_external_storage)
+ .setNegativeButton(android.R.string.cancel, onClick)
+ .setOnCancelListener(onCancel);
+ if (ApiHelper.HAS_SET_ICON_ATTRIBUTE) {
+ setAlertDialogIconAttribute(builder);
+ } else {
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ }
+ mAlertDialog = builder.show();
+ registerReceiver(mMountReceiver, mMountFilter);
+ }
+ mPanoramaViewHelper.onStart();
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ private static void setAlertDialogIconAttribute(
+ AlertDialog.Builder builder) {
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mAlertDialog != null) {
+ unregisterReceiver(mMountReceiver);
+ mAlertDialog.dismiss();
+ mAlertDialog = null;
+ }
+ mPanoramaViewHelper.onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().resume();
+ getDataManager().resume();
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ mGLRootView.onResume();
+ mOrientationManager.resume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mOrientationManager.pause();
+ mGLRootView.onPause();
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().pause();
+ getDataManager().pause();
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ GalleryBitmapPool.getInstance().clear();
+ MediaItem.getBytesBufferPool().clear();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().destroy();
+ } finally {
+ mGLRootView.unlockRenderThread();
+ }
+ doUnbindBatchService();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ mGLRootView.lockRenderThread();
+ try {
+ getStateManager().notifyActivityResult(
+ requestCode, resultCode, data);
+ } finally {
+ mGLRootView.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();
+ }
+ }
+
+ public GalleryActionBar getGalleryActionBar() {
+ if (mActionBar == null) {
+ mActionBar = new GalleryActionBar(this);
+ }
+ return mActionBar;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ return getStateManager().itemSelected(item);
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ protected void disableToggleStatusBar() {
+ mDisableToggleStatusBar = true;
+ }
+
+ // Shows status bar in portrait view, hide in landscape view
+ private void toggleStatusBarByOrientation() {
+ if (mDisableToggleStatusBar) return;
+
+ Window win = getWindow();
+ if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ } else {
+ win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ }
+
+ public TransitionStore getTransitionStore() {
+ return mTransitionStore;
+ }
+
+ public PanoramaViewHelper getPanoramaViewHelper() {
+ return mPanoramaViewHelper;
+ }
+
+ protected boolean isFullscreen() {
+ return (getWindow().getAttributes().flags
+ & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0;
+ }
+
+ private BatchService mBatchService;
+ private boolean mBatchServiceIsBound = false;
+ private ServiceConnection mBatchServiceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mBatchService = ((BatchService.LocalBinder)service).getService();
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ mBatchService = null;
+ }
+ };
+
+ private void doBindBatchService() {
+ bindService(new Intent(this, BatchService.class), mBatchServiceConnection, Context.BIND_AUTO_CREATE);
+ mBatchServiceIsBound = true;
+ }
+
+ private void doUnbindBatchService() {
+ if (mBatchServiceIsBound) {
+ // Detach our existing connection.
+ unbindService(mBatchServiceConnection);
+ mBatchServiceIsBound = false;
+ }
+ }
+
+ public ThreadPool getBatchServiceThreadPoolIfAvailable() {
+ if (mBatchServiceIsBound && mBatchService != null) {
+ return mBatchService.getThreadPool();
+ } else {
+ throw new RuntimeException("Batch service unavailable");
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/ActivityState.java b/src/com/android/gallery3d/app/ActivityState.java
new file mode 100644
index 000000000..2f1e0c9d9
--- /dev/null
+++ b/src/com/android/gallery3d/app/ActivityState.java
@@ -0,0 +1,276 @@
+/*
+ * 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 android.app.ActionBar;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.view.HapticFeedbackConstants;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.anim.StateTransitionAnimation;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.PreparePageFadeoutTexture;
+import com.android.gallery3d.util.GalleryUtils;
+
+abstract public class ActivityState {
+ protected static final int FLAG_HIDE_ACTION_BAR = 1;
+ protected static final int FLAG_HIDE_STATUS_BAR = 2;
+ protected static final int FLAG_SCREEN_ON_WHEN_PLUGGED = 4;
+ protected static final int FLAG_SCREEN_ON_ALWAYS = 8;
+ protected static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 16;
+ protected static final int FLAG_SHOW_WHEN_LOCKED = 32;
+
+ protected AbstractGalleryActivity 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;
+ }
+
+ private boolean mDestroyed = false;
+ private boolean mPlugged = false;
+ boolean mIsFinishing = false;
+
+ private static final String KEY_TRANSITION_IN = "transition-in";
+
+ private StateTransitionAnimation.Transition mNextTransition =
+ StateTransitionAnimation.Transition.None;
+ private StateTransitionAnimation mIntroAnimation;
+ private GLView mContentPane;
+
+ protected ActivityState() {
+ }
+
+ protected void setContentPane(GLView content) {
+ mContentPane = content;
+ if (mIntroAnimation != null) {
+ mContentPane.setIntroAnimation(mIntroAnimation);
+ mIntroAnimation = null;
+ }
+ mContentPane.setBackgroundColor(getBackgroundColor());
+ mActivity.getGLRoot().setContentPane(mContentPane);
+ }
+
+ void initialize(AbstractGalleryActivity 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 onConfigurationChanged(Configuration config) {
+ }
+
+ protected void onSaveState(Bundle outState) {
+ }
+
+ protected void onStateResult(int requestCode, int resultCode, Intent data) {
+ }
+
+ protected float[] mBackgroundColor;
+
+ protected int getBackgroundColorId() {
+ return R.color.default_background;
+ }
+
+ protected float[] getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ protected void onCreate(Bundle data, Bundle storedState) {
+ mBackgroundColor = GalleryUtils.intColorToFloatARGBArray(
+ mActivity.getResources().getColor(getBackgroundColorId()));
+ }
+
+ protected void clearStateResult() {
+ }
+
+ BroadcastReceiver mPowerIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
+ boolean plugged = (0 != intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0));
+
+ if (plugged != mPlugged) {
+ mPlugged = plugged;
+ setScreenFlags();
+ }
+ }
+ }
+ };
+
+ private void setScreenFlags() {
+ final Window win = mActivity.getWindow();
+ final WindowManager.LayoutParams params = win.getAttributes();
+ if ((0 != (mFlags & FLAG_SCREEN_ON_ALWAYS)) ||
+ (mPlugged && 0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED))) {
+ params.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+ } else {
+ params.flags &= ~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+ }
+ if (0 != (mFlags & FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)) {
+ params.flags |= WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON;
+ } else {
+ params.flags &= ~WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON;
+ }
+ if (0 != (mFlags & FLAG_SHOW_WHEN_LOCKED)) {
+ params.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
+ } else {
+ params.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
+ }
+ win.setAttributes(params);
+ }
+
+ protected void transitionOnNextPause(Class<? extends ActivityState> outgoing,
+ Class<? extends ActivityState> incoming, StateTransitionAnimation.Transition hint) {
+ if (outgoing == SinglePhotoPage.class && incoming == AlbumPage.class) {
+ mNextTransition = StateTransitionAnimation.Transition.Outgoing;
+ } else if (outgoing == AlbumPage.class && incoming == SinglePhotoPage.class) {
+ mNextTransition = StateTransitionAnimation.Transition.PhotoIncoming;
+ } else {
+ mNextTransition = hint;
+ }
+ }
+
+ protected void performHapticFeedback(int feedbackConstant) {
+ mActivity.getWindow().getDecorView().performHapticFeedback(feedbackConstant,
+ HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+ }
+
+ protected void onPause() {
+ if (0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED)) {
+ ((Activity) mActivity).unregisterReceiver(mPowerIntentReceiver);
+ }
+ if (mNextTransition != StateTransitionAnimation.Transition.None) {
+ mActivity.getTransitionStore().put(KEY_TRANSITION_IN, mNextTransition);
+ PreparePageFadeoutTexture.prepareFadeOutTexture(mActivity, mContentPane);
+ mNextTransition = StateTransitionAnimation.Transition.None;
+ }
+ }
+
+ // should only be called by StateManager
+ void resume() {
+ AbstractGalleryActivity 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();
+ mActivity.getGalleryActionBar().setDisplayOptions(stateCount > 1, true);
+ // Default behavior, this can be overridden in ActivityState's onResume.
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ }
+
+ activity.invalidateOptionsMenu();
+
+ setScreenFlags();
+
+ boolean lightsOut = ((mFlags & FLAG_HIDE_STATUS_BAR) != 0);
+ mActivity.getGLRoot().setLightsOutMode(lightsOut);
+
+ ResultEntry entry = mReceivedResults;
+ if (entry != null) {
+ mReceivedResults = null;
+ onStateResult(entry.requestCode, entry.resultCode, entry.resultData);
+ }
+
+ if (0 != (mFlags & FLAG_SCREEN_ON_WHEN_PLUGGED)) {
+ // we need to know whether the device is plugged in to do this correctly
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ activity.registerReceiver(mPowerIntentReceiver, filter);
+ }
+
+ onResume();
+
+ // the transition store should be cleared after resume;
+ mActivity.getTransitionStore().clear();
+ }
+
+ // a subclass of ActivityState should override the method to resume itself
+ protected void onResume() {
+ RawTexture fade = mActivity.getTransitionStore().get(
+ PreparePageFadeoutTexture.KEY_FADE_TEXTURE);
+ mNextTransition = mActivity.getTransitionStore().get(
+ KEY_TRANSITION_IN, StateTransitionAnimation.Transition.None);
+ if (mNextTransition != StateTransitionAnimation.Transition.None) {
+ mIntroAnimation = new StateTransitionAnimation(mNextTransition, fade);
+ mNextTransition = StateTransitionAnimation.Transition.None;
+ }
+ }
+
+ protected boolean onCreateActionBar(Menu menu) {
+ // TODO: we should return false if there is no menu to show
+ // this is a workaround for a bug in system
+ return true;
+ }
+
+ protected boolean onItemSelected(MenuItem item) {
+ return false;
+ }
+
+ protected void onDestroy() {
+ mDestroyed = true;
+ }
+
+ boolean isDestroyed() {
+ return mDestroyed;
+ }
+
+ public boolean isFinishing() {
+ return mIsFinishing;
+ }
+
+ protected MenuInflater getSupportMenuInflater() {
+ return mActivity.getMenuInflater();
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumDataLoader.java b/src/com/android/gallery3d/app/AlbumDataLoader.java
new file mode 100644
index 000000000..28a822830
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumDataLoader.java
@@ -0,0 +1,397 @@
+/*
+ * 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 android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+
+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.data.Path;
+import com.android.gallery3d.ui.SynchronizedHandler;
+
+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 AlbumDataLoader {
+ @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;
+
+ public static interface DataListener {
+ public void onContentChanged(int index);
+ public void onSizeChanged(int size);
+ }
+
+ 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 DataListener mDataListener;
+ private MySourceListener mSourceListener = new MySourceListener();
+ private LoadingListener mLoadingListener;
+
+ private ReloadTask mReloadTask;
+ // the data version on which last loading failed
+ private long mFailedVersion = MediaObject.INVALID_DATA_VERSION;
+
+ public AlbumDataLoader(AbstractGalleryActivity 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) {
+ boolean loadingFailed =
+ (mFailedVersion != MediaObject.INVALID_DATA_VERSION);
+ mLoadingListener.onLoadingFinished(loadingFailed);
+ }
+ 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)) {
+ return mSource.getMediaItem(index, 1).get(0);
+ }
+ return mData[index % mData.length];
+ }
+
+ public int getActiveStart() {
+ return mActiveStart;
+ }
+
+ public boolean isActive(int index) {
+ return index >= mActiveStart && index < mActiveEnd;
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ // Returns the index of the MediaItem with the given path or
+ // -1 if the path is not cached
+ public int findItem(Path id) {
+ for (int i = mContentStart; i < mContentEnd; i++) {
+ MediaItem item = mData[i % DATA_CACHE_SIZE];
+ if (item != null && id == item.getPath()) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ 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;
+ }
+ 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;
+
+ 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 {
+ @Override
+ public void onContentDirty() {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ public void setDataListener(DataListener listener) {
+ mDataListener = listener;
+ }
+
+ public void setLoadingListener(LoadingListener listener) {
+ mLoadingListener = listener;
+ }
+
+ private <T> T executeAndWait(Callable<T> callable) {
+ FutureTask<T> task = new FutureTask<T>(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<MediaItem> items;
+ }
+
+ private class GetUpdateInfo implements Callable<UpdateInfo> {
+ private final long mVersion;
+
+ public GetUpdateInfo(long version) {
+ mVersion = version;
+ }
+
+ @Override
+ public UpdateInfo call() throws Exception {
+ if (mFailedVersion == mVersion) {
+ // previous loading failed, return null to pause loading
+ return null;
+ }
+ 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<Void> {
+
+ 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 (mDataListener != null) mDataListener.onSizeChanged(mSize);
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+
+ ArrayList<MediaItem> items = info.items;
+
+ mFailedVersion = MediaObject.INVALID_DATA_VERSION;
+ if ((items == null) || items.isEmpty()) {
+ if (info.reloadCount > 0) {
+ mFailedVersion = info.version;
+ Log.d(TAG, "loading failed: " + mFailedVersion);
+ }
+ 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 (mDataListener != null && i >= mActiveStart && i < mActiveEnd) {
+ mDataListener.onContentChanged(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() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ boolean updateComplete = false;
+ while (mActive) {
+ synchronized (this) {
+ if (mActive && !mDirty && updateComplete) {
+ updateLoading(false);
+ if (mFailedVersion != MediaObject.INVALID_DATA_VERSION) {
+ Log.d(TAG, "reload pause");
+ }
+ Utils.waitWithoutInterrupt(this);
+ if (mActive && (mFailedVersion != MediaObject.INVALID_DATA_VERSION)) {
+ Log.d(TAG, "reload resume");
+ }
+ continue;
+ }
+ mDirty = false;
+ }
+ updateLoading(true);
+ long version = mSource.reload();
+ UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
+ updateComplete = info == null;
+ if (updateComplete) continue;
+ 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..658abbbd4
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -0,0 +1,786 @@
+/*
+ * 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 android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.view.HapticFeedbackConstants;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+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.Path;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+import com.android.gallery3d.glrenderer.FadeTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.ui.ActionModeHandler;
+import com.android.gallery3d.ui.ActionModeHandler.ActionModeListener;
+import com.android.gallery3d.ui.AlbumSlotRenderer;
+import com.android.gallery3d.ui.DetailsHelper;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.PhotoFallbackEffect;
+import com.android.gallery3d.ui.RelativePosition;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MediaSetUtils;
+
+
+public class AlbumPage extends ActivityState implements GalleryActionBar.ClusterRunner,
+ SelectionManager.SelectionListener, MediaSet.SyncListener, GalleryActionBar.OnAlbumModeSelectedListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumPage";
+
+ public static final String KEY_MEDIA_PATH = "media-path";
+ public static final String KEY_PARENT_MEDIA_PATH = "parent-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";
+ public static final String KEY_EMPTY_ALBUM = "empty-album";
+ public static final String KEY_RESUME_ANIMATION = "resume_animation";
+
+ private static final int REQUEST_SLIDESHOW = 1;
+ public static final int REQUEST_PHOTO = 2;
+ private static final int REQUEST_DO_ANIMATION = 3;
+
+ private static final int BIT_LOADING_RELOAD = 1;
+ private static final int BIT_LOADING_SYNC = 2;
+
+ private static final float USER_DISTANCE_METER = 0.3f;
+
+ private boolean mIsActive = false;
+ private AlbumSlotRenderer mAlbumView;
+ private Path mMediaSetPath;
+ private String mParentMediaSetString;
+ private SlotView mSlotView;
+
+ private AlbumDataLoader mAlbumDataAdapter;
+
+ protected SelectionManager mSelectionManager;
+
+ private boolean mGetContent;
+ private boolean mShowClusterMenu;
+
+ private ActionModeHandler mActionModeHandler;
+ private int mFocusIndex = 0;
+ private DetailsHelper mDetailsHelper;
+ private MyDetailsSource mDetailsSource;
+ private MediaSet mMediaSet;
+ private boolean mShowDetails;
+ private float mUserDistance; // in pixel
+ private Future<Integer> mSyncTask = null;
+ private boolean mLaunchedFromPhotoPage;
+ private boolean mInCameraApp;
+ private boolean mInCameraAndWantQuitOnPause;
+
+ private int mLoadingBits = 0;
+ private boolean mInitialSynced = false;
+ private int mSyncResult;
+ private boolean mLoadingFailed;
+ private RelativePosition mOpenCenter = new RelativePosition();
+
+ private Handler mHandler;
+ private static final int MSG_PICK_PHOTO = 0;
+
+ private PhotoFallbackEffect mResumeEffect;
+ private PhotoFallbackEffect.PositionProvider mPositionProvider =
+ new PhotoFallbackEffect.PositionProvider() {
+ @Override
+ public Rect getPosition(int index) {
+ Rect rect = mSlotView.getSlotRect(index);
+ Rect bounds = mSlotView.bounds();
+ rect.offset(bounds.left - mSlotView.getScrollX(),
+ bounds.top - mSlotView.getScrollY());
+ return rect;
+ }
+
+ @Override
+ public int getItemIndex(Path path) {
+ int start = mSlotView.getVisibleStart();
+ int end = mSlotView.getVisibleEnd();
+ for (int i = start; i < end; ++i) {
+ MediaItem item = mAlbumDataAdapter.get(i);
+ if (item != null && item.getPath() == path) return i;
+ }
+ return -1;
+ }
+ };
+
+ @Override
+ protected int getBackgroundColorId() {
+ return R.color.album_background;
+ }
+
+ private final GLView mRootPane = new GLView() {
+ private final float mMatrix[] = new float[16];
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+
+ int slotViewTop = mActivity.getGalleryActionBar().getHeight();
+ int slotViewBottom = bottom - top;
+ int slotViewRight = right - left;
+
+ if (mShowDetails) {
+ mDetailsHelper.layout(left, slotViewTop, right, bottom);
+ } else {
+ mAlbumView.setHighlightItemPath(null);
+ }
+
+ // Set the mSlotView as a reference point to the open animation
+ mOpenCenter.setReferencePosition(0, slotViewTop);
+ mSlotView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+ GalleryUtils.setViewPointMatrix(mMatrix,
+ (right - left) / 2, (bottom - top) / 2, -mUserDistance);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.multiplyMatrix(mMatrix, 0);
+ super.render(canvas);
+
+ if (mResumeEffect != null) {
+ boolean more = mResumeEffect.draw(canvas);
+ if (!more) {
+ mResumeEffect = null;
+ mAlbumView.setSlotFilter(null);
+ }
+ // We want to render one more time even when no more effect
+ // required. So that the animated thumbnails could be draw
+ // with declarations in super.render().
+ invalidate();
+ }
+ canvas.restore();
+ }
+ };
+
+ // This are the transitions we want:
+ //
+ // +--------+ +------------+ +-------+ +----------+
+ // | Camera |---------->| Fullscreen |--->| Album |--->| AlbumSet |
+ // | View | thumbnail | Photo | up | Page | up | Page |
+ // +--------+ +------------+ +-------+ +----------+
+ // ^ | | ^ |
+ // | | | | | close
+ // +----------back--------+ +----back----+ +--back-> app
+ //
+ @Override
+ protected void onBackPressed() {
+ if (mShowDetails) {
+ hideDetails();
+ } else if (mSelectionManager.inSelectionMode()) {
+ mSelectionManager.leaveSelectionMode();
+ } else {
+ if(mLaunchedFromPhotoPage) {
+ mActivity.getTransitionStore().putIfNotPresent(
+ PhotoPage.KEY_ALBUMPAGE_TRANSITION,
+ PhotoPage.MSG_ALBUMPAGE_RESUMED);
+ }
+ // TODO: fix this regression
+ // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ if (mInCameraApp) {
+ super.onBackPressed();
+ } else {
+ onUpPressed();
+ }
+ }
+ }
+
+ private void onUpPressed() {
+ if (mInCameraApp) {
+ GalleryUtils.startGalleryActivity(mActivity);
+ } else if (mActivity.getStateManager().getStateCount() > 1) {
+ super.onBackPressed();
+ } else if (mParentMediaSetString != null) {
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, mParentMediaSetString);
+ mActivity.getStateManager().switchState(
+ this, AlbumSetPage.class, data);
+ }
+ }
+
+ private void onDown(int index) {
+ mAlbumView.setPressedIndex(index);
+ }
+
+ private void onUp(boolean followedByLongPress) {
+ if (followedByLongPress) {
+ // Avoid showing press-up animations for long-press.
+ mAlbumView.setPressedIndex(-1);
+ } else {
+ mAlbumView.setPressedUp();
+ }
+ }
+
+ private void onSingleTapUp(int slotIndex) {
+ if (!mIsActive) return;
+
+ if (mSelectionManager.inSelectionMode()) {
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return; // Item not ready yet, ignore the click
+ mSelectionManager.toggle(item.getPath());
+ mSlotView.invalidate();
+ } else {
+ // Render transition in pressed state
+ mAlbumView.setPressedIndex(slotIndex);
+ mAlbumView.setPressedUp();
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PICK_PHOTO, slotIndex, 0),
+ FadeTexture.DURATION);
+ }
+ }
+
+ private void pickPhoto(int slotIndex) {
+ pickPhoto(slotIndex, false);
+ }
+
+ private void pickPhoto(int slotIndex, boolean startInFilmstrip) {
+ if (!mIsActive) return;
+
+ if (!startInFilmstrip) {
+ // Launch photos in lights out mode
+ mActivity.getGLRoot().setLightsOutMode(true);
+ }
+
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return; // Item not ready yet, ignore the click
+ if (mGetContent) {
+ onGetContent(item);
+ } else if (mLaunchedFromPhotoPage) {
+ TransitionStore transitions = mActivity.getTransitionStore();
+ transitions.put(
+ PhotoPage.KEY_ALBUMPAGE_TRANSITION,
+ PhotoPage.MSG_ALBUMPAGE_PICKED);
+ transitions.put(PhotoPage.KEY_INDEX_HINT, slotIndex);
+ onBackPressed();
+ } else {
+ // Get into the PhotoPage.
+ // mAlbumView.savePositions(PositionRepository.getInstance(mActivity));
+ Bundle data = new Bundle();
+ data.putInt(PhotoPage.KEY_INDEX_HINT, slotIndex);
+ data.putParcelable(PhotoPage.KEY_OPEN_ANIMATION_RECT,
+ mSlotView.getSlotRect(slotIndex, mRootPane));
+ data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+ mMediaSetPath.toString());
+ data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH,
+ item.getPath().toString());
+ data.putInt(PhotoPage.KEY_ALBUMPAGE_TRANSITION,
+ PhotoPage.MSG_ALBUMPAGE_STARTED);
+ data.putBoolean(PhotoPage.KEY_START_IN_FILMSTRIP,
+ startInFilmstrip);
+ data.putBoolean(PhotoPage.KEY_IN_CAMERA_ROLL, mMediaSet.isCameraRoll());
+ if (startInFilmstrip) {
+ mActivity.getStateManager().switchState(this, FilmstripPage.class, data);
+ } else {
+ mActivity.getStateManager().startStateForResult(
+ SinglePhotoPage.class, REQUEST_PHOTO, data);
+ }
+ }
+ }
+
+ private void onGetContent(final MediaItem item) {
+ DataManager dm = mActivity.getDataManager();
+ Activity activity = mActivity;
+ if (mData.getString(Gallery.EXTRA_CROP) != null) {
+ Uri uri = dm.getContentUri(item.getPath());
+ Intent intent = new Intent(CropActivity.CROP_ACTION, uri)
+ .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+ .putExtras(getData());
+ if (mData.getParcelable(MediaStore.EXTRA_OUTPUT) == null) {
+ intent.putExtra(CropExtras.KEY_RETURN_DATA, true);
+ }
+ activity.startActivity(intent);
+ activity.finish();
+ } else {
+ Intent intent = new Intent(null, item.getContentUri())
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ activity.setResult(Activity.RESULT_OK, intent);
+ activity.finish();
+ }
+ }
+
+ public void onLongTap(int slotIndex) {
+ if (mGetContent) return;
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return;
+ mSelectionManager.setAutoLeaveSelectionMode(true);
+ mSelectionManager.toggle(item.getPath());
+ mSlotView.invalidate();
+ }
+
+ @Override
+ 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);
+ }
+
+ @Override
+ protected void onCreate(Bundle data, Bundle restoreState) {
+ super.onCreate(data, 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);
+ mDetailsSource = new MyDetailsSource();
+ Context context = mActivity.getAndroidContext();
+
+ if (data.getBoolean(KEY_AUTO_SELECT_ALL)) {
+ mSelectionManager.selectAll();
+ }
+
+ mLaunchedFromPhotoPage =
+ mActivity.getStateManager().hasStateClass(FilmstripPage.class);
+ mInCameraApp = data.getBoolean(PhotoPage.KEY_APP_BRIDGE, false);
+
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_PICK_PHOTO: {
+ pickPhoto(message.arg1);
+ break;
+ }
+ default:
+ throw new AssertionError(message.what);
+ }
+ }
+ };
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsActive = true;
+
+ mResumeEffect = mActivity.getTransitionStore().get(KEY_RESUME_ANIMATION);
+ if (mResumeEffect != null) {
+ mAlbumView.setSlotFilter(mResumeEffect);
+ mResumeEffect.setPositionProvider(mPositionProvider);
+ mResumeEffect.start();
+ }
+
+ setContentPane(mRootPane);
+
+ boolean enableHomeButton = (mActivity.getStateManager().getStateCount() > 1) |
+ mParentMediaSetString != null;
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ actionBar.setDisplayOptions(enableHomeButton, false);
+ if (!mGetContent) {
+ actionBar.enableAlbumModeMenu(GalleryActionBar.ALBUM_GRID_MODE_SELECTED, this);
+ }
+
+ // Set the reload bit here to prevent it exit this page in clearLoadingBit().
+ setLoadingBit(BIT_LOADING_RELOAD);
+ mLoadingFailed = false;
+ mAlbumDataAdapter.resume();
+
+ mAlbumView.resume();
+ mAlbumView.setPressedIndex(-1);
+ mActionModeHandler.resume();
+ if (!mInitialSynced) {
+ setLoadingBit(BIT_LOADING_SYNC);
+ mSyncTask = mMediaSet.requestSync(this);
+ }
+ mInCameraAndWantQuitOnPause = mInCameraApp;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mIsActive = false;
+
+ if (mSelectionManager.inSelectionMode()) {
+ mSelectionManager.leaveSelectionMode();
+ }
+ mAlbumView.setSlotFilter(null);
+ mActionModeHandler.pause();
+ mAlbumDataAdapter.pause();
+ mAlbumView.pause();
+ DetailsHelper.pause();
+ if (!mGetContent) {
+ mActivity.getGalleryActionBar().disableAlbumModeMenu(true);
+ }
+
+ if (mSyncTask != null) {
+ mSyncTask.cancel();
+ mSyncTask = null;
+ clearLoadingBit(BIT_LOADING_SYNC);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mAlbumDataAdapter != null) {
+ mAlbumDataAdapter.setLoadingListener(null);
+ }
+ mActionModeHandler.destroy();
+ }
+
+ private void initializeViews() {
+ mSelectionManager = new SelectionManager(mActivity, false);
+ mSelectionManager.setSelectionListener(this);
+ Config.AlbumPage config = Config.AlbumPage.get(mActivity);
+ mSlotView = new SlotView(mActivity, config.slotViewSpec);
+ mAlbumView = new AlbumSlotRenderer(mActivity, mSlotView,
+ mSelectionManager, config.placeholderColor);
+ mSlotView.setSlotRenderer(mAlbumView);
+ mRootPane.addComponent(mSlotView);
+ mSlotView.setListener(new SlotView.SimpleListener() {
+ @Override
+ public void onDown(int index) {
+ AlbumPage.this.onDown(index);
+ }
+
+ @Override
+ public void onUp(boolean followedByLongPress) {
+ AlbumPage.this.onUp(followedByLongPress);
+ }
+
+ @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() {
+ @Override
+ public boolean onActionItemClicked(MenuItem item) {
+ return onItemSelected(item);
+ }
+ });
+ }
+
+ private void initializeData(Bundle data) {
+ mMediaSetPath = Path.fromString(data.getString(KEY_MEDIA_PATH));
+ mParentMediaSetString = data.getString(KEY_PARENT_MEDIA_PATH);
+ mMediaSet = mActivity.getDataManager().getMediaSet(mMediaSetPath);
+ if (mMediaSet == null) {
+ Utils.fail("MediaSet is null. Path = %s", mMediaSetPath);
+ }
+ mSelectionManager.setSourceMediaSet(mMediaSet);
+ mAlbumDataAdapter = new AlbumDataLoader(mActivity, mMediaSet);
+ mAlbumDataAdapter.setLoadingListener(new MyLoadingListener());
+ mAlbumView.setModel(mAlbumDataAdapter);
+ }
+
+ private void showDetails() {
+ mShowDetails = true;
+ if (mDetailsHelper == null) {
+ mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource);
+ mDetailsHelper.setCloseListener(new CloseListener() {
+ @Override
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ }
+ mDetailsHelper.show();
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mDetailsHelper.hide();
+ mAlbumView.setHighlightItemPath(null);
+ mSlotView.invalidate();
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ GalleryActionBar actionBar = mActivity.getGalleryActionBar();
+ MenuInflater inflator = getSupportMenuInflater();
+ if (mGetContent) {
+ inflator.inflate(R.menu.pickup, menu);
+ int typeBits = mData.getInt(Gallery.KEY_TYPE_BITS,
+ DataManager.INCLUDE_IMAGE);
+ actionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
+ } else {
+ inflator.inflate(R.menu.album, menu);
+ actionBar.setTitle(mMediaSet.getName());
+
+ FilterUtils.setupMenuItems(actionBar, mMediaSetPath, true);
+
+ menu.findItem(R.id.action_group_by).setVisible(mShowClusterMenu);
+ menu.findItem(R.id.action_camera).setVisible(
+ MediaSetUtils.isCameraSource(mMediaSetPath)
+ && GalleryUtils.isCameraAvailable(mActivity));
+
+ }
+ actionBar.setSubtitle(null);
+ return true;
+ }
+
+ private void prepareAnimationBackToFilmstrip(int slotIndex) {
+ if (mAlbumDataAdapter == null || !mAlbumDataAdapter.isActive(slotIndex)) return;
+ MediaItem item = mAlbumDataAdapter.get(slotIndex);
+ if (item == null) return;
+ TransitionStore transitions = mActivity.getTransitionStore();
+ transitions.put(PhotoPage.KEY_INDEX_HINT, slotIndex);
+ transitions.put(PhotoPage.KEY_OPEN_ANIMATION_RECT,
+ mSlotView.getSlotRect(slotIndex, mRootPane));
+ }
+
+ private void switchToFilmstrip() {
+ if (mAlbumDataAdapter.size() < 1) return;
+ int targetPhoto = mSlotView.getVisibleStart();
+ prepareAnimationBackToFilmstrip(targetPhoto);
+ if(mLaunchedFromPhotoPage) {
+ onBackPressed();
+ } else {
+ pickPhoto(targetPhoto, true);
+ }
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home: {
+ onUpPressed();
+ return true;
+ }
+ case R.id.action_cancel:
+ mActivity.getStateManager().finishState(this);
+ return true;
+ 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: {
+ mInCameraAndWantQuitOnPause = false;
+ 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;
+ }
+ case R.id.action_camera: {
+ GalleryUtils.startCameraActivity(mActivity);
+ 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);
+ mSlotView.setCenterIndex(mFocusIndex);
+ break;
+ }
+ case REQUEST_PHOTO: {
+ if (data == null) return;
+ mFocusIndex = data.getIntExtra(PhotoPage.KEY_RETURN_INDEX_HINT, 0);
+ mSlotView.makeSlotVisible(mFocusIndex);
+ break;
+ }
+ case REQUEST_DO_ANIMATION: {
+ mSlotView.startRisingAnimation();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onSelectionModeChange(int mode) {
+ switch (mode) {
+ case SelectionManager.ENTER_SELECTION_MODE: {
+ mActionModeHandler.startActionMode();
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ break;
+ }
+ case SelectionManager.LEAVE_SELECTION_MODE: {
+ mActionModeHandler.finishActionMode();
+ mRootPane.invalidate();
+ break;
+ }
+ case SelectionManager.SELECT_ALL_MODE: {
+ mActionModeHandler.updateSupportedOperation();
+ mRootPane.invalidate();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onSelectionChange(Path path, boolean selected) {
+ 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);
+ }
+
+ @Override
+ public void onSyncDone(final MediaSet mediaSet, final int resultCode) {
+ Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) + " result="
+ + resultCode);
+ ((Activity) mActivity).runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GLRoot root = mActivity.getGLRoot();
+ root.lockRenderThread();
+ mSyncResult = resultCode;
+ try {
+ if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
+ mInitialSynced = true;
+ }
+ clearLoadingBit(BIT_LOADING_SYNC);
+ showSyncErrorIfNecessary(mLoadingFailed);
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+ });
+ }
+
+ // Show sync error toast when all the following conditions are met:
+ // (1) both loading and sync are done,
+ // (2) sync result is error,
+ // (3) the page is still active, and
+ // (4) no photo is shown or loading fails.
+ private void showSyncErrorIfNecessary(boolean loadingFailed) {
+ if ((mLoadingBits == 0) && (mSyncResult == MediaSet.SYNC_RESULT_ERROR) && mIsActive
+ && (loadingFailed || (mAlbumDataAdapter.size() == 0))) {
+ Toast.makeText(mActivity, R.string.sync_album_error,
+ Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void setLoadingBit(int loadTaskBit) {
+ mLoadingBits |= loadTaskBit;
+ }
+
+ private void clearLoadingBit(int loadTaskBit) {
+ mLoadingBits &= ~loadTaskBit;
+ if (mLoadingBits == 0 && mIsActive) {
+ if (mAlbumDataAdapter.size() == 0) {
+ Intent result = new Intent();
+ result.putExtra(KEY_EMPTY_ALBUM, true);
+ setStateResult(Activity.RESULT_OK, result);
+ mActivity.getStateManager().finishState(this);
+ }
+ }
+ }
+
+ private class MyLoadingListener implements LoadingListener {
+ @Override
+ public void onLoadingStarted() {
+ setLoadingBit(BIT_LOADING_RELOAD);
+ mLoadingFailed = false;
+ }
+
+ @Override
+ public void onLoadingFinished(boolean loadingFailed) {
+ clearLoadingBit(BIT_LOADING_RELOAD);
+ mLoadingFailed = loadingFailed;
+ showSyncErrorIfNecessary(loadingFailed);
+ }
+ }
+
+ private class MyDetailsSource implements DetailsHelper.DetailsSource {
+ private int mIndex;
+
+ @Override
+ public int size() {
+ return mAlbumDataAdapter.size();
+ }
+
+ @Override
+ public int setIndex() {
+ Path id = mSelectionManager.getSelected(false).get(0);
+ mIndex = mAlbumDataAdapter.findItem(id);
+ return mIndex;
+ }
+
+ @Override
+ public MediaDetails getDetails() {
+ // this relies on setIndex() being called beforehand
+ MediaObject item = mAlbumDataAdapter.get(mIndex);
+ if (item != null) {
+ mAlbumView.setHighlightItemPath(item.getPath());
+ return item.getDetails();
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public void onAlbumModeSelected(int mode) {
+ if (mode == GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED) {
+ switchToFilmstrip();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumPicker.java b/src/com/android/gallery3d/app/AlbumPicker.java
new file mode 100644
index 000000000..65eb77291
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumPicker.java
@@ -0,0 +1,40 @@
+/*
+ * 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.content.Intent;
+import android.os.Bundle;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.DataManager;
+
+public class AlbumPicker extends PickerActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ 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);
+ }
+}
diff --git a/src/com/android/gallery3d/app/AlbumSetDataLoader.java b/src/com/android/gallery3d/app/AlbumSetDataLoader.java
new file mode 100644
index 000000000..cf380f812
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumSetDataLoader.java
@@ -0,0 +1,393 @@
+/*
+ * 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 android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+
+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.data.Path;
+import com.android.gallery3d.ui.SynchronizedHandler;
+
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+public class AlbumSetDataLoader {
+ @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 MSG_LOAD_START = 1;
+ private static final int MSG_LOAD_FINISH = 2;
+ private static final int MSG_RUN_OBJECT = 3;
+
+ public static interface DataListener {
+ public void onContentChanged(int index);
+ public void onSizeChanged(int size);
+ }
+
+ private final MediaSet[] mData;
+ private final MediaItem[] mCoverItem;
+ private final int[] mTotalCount;
+ 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 DataListener mDataListener;
+ private LoadingListener mLoadingListener;
+ private ReloadTask mReloadTask;
+
+ private final Handler mMainHandler;
+
+ private final MySourceListener mSourceListener = new MySourceListener();
+
+ public AlbumSetDataLoader(AbstractGalleryActivity activity, MediaSet albumSet, int cacheSize) {
+ mSource = Utils.checkNotNull(albumSet);
+ mCoverItem = new MediaItem[cacheSize];
+ mData = new MediaSet[cacheSize];
+ mTotalCount = new int[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(false);
+ return;
+ }
+ }
+ };
+ }
+
+ public void pause() {
+ mReloadTask.terminate();
+ mReloadTask = null;
+ mSource.removeContentListener(mSourceListener);
+ }
+
+ public void resume() {
+ mSource.addContentListener(mSourceListener);
+ mReloadTask = new ReloadTask();
+ mReloadTask.start();
+ }
+
+ private void assertIsActive(int index) {
+ if (index < mActiveStart && index >= mActiveEnd) {
+ throw new IllegalArgumentException(String.format(
+ "%s not in (%s, %s)", index, mActiveStart, mActiveEnd));
+ }
+ }
+
+ public MediaSet getMediaSet(int index) {
+ assertIsActive(index);
+ return mData[index % mData.length];
+ }
+
+ public MediaItem getCoverItem(int index) {
+ assertIsActive(index);
+ return mCoverItem[index % mCoverItem.length];
+ }
+
+ public int getTotalCount(int index) {
+ assertIsActive(index);
+ return mTotalCount[index % mTotalCount.length];
+ }
+
+ public int getActiveStart() {
+ return mActiveStart;
+ }
+
+ public boolean isActive(int index) {
+ return index >= mActiveStart && index < mActiveEnd;
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ // Returns the index of the MediaSet with the given path or
+ // -1 if the path is not cached
+ public int findSet(Path id) {
+ int length = mData.length;
+ for (int i = mContentStart; i < mContentEnd; i++) {
+ MediaSet set = mData[i % length];
+ if (set != null && id == set.getPath()) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void clearSlot(int slotIndex) {
+ mData[slotIndex] = null;
+ mCoverItem[slotIndex] = null;
+ mTotalCount[slotIndex] = 0;
+ 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 length = mCoverItem.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 <= mCoverItem.length && end <= mSize);
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ int length = mCoverItem.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 {
+ @Override
+ public void onContentDirty() {
+ mReloadTask.notifyDirty();
+ }
+ }
+
+ public void setModelListener(DataListener listener) {
+ mDataListener = listener;
+ }
+
+ public void setLoadingListener(LoadingListener listener) {
+ mLoadingListener = listener;
+ }
+
+ private static class UpdateInfo {
+ public long version;
+ public int index;
+
+ public int size;
+ public MediaSet item;
+ public MediaItem cover;
+ public int totalCount;
+ }
+
+ private class GetUpdateInfo implements Callable<UpdateInfo> {
+
+ 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<Void> {
+ private final UpdateInfo mUpdateInfo;
+
+ public UpdateContent(UpdateInfo info) {
+ mUpdateInfo = info;
+ }
+
+ @Override
+ public Void call() {
+ // Avoid notifying listeners of status change after pause
+ // Otherwise gallery will be in inconsistent state after resume.
+ if (mReloadTask == null) return null;
+ UpdateInfo info = mUpdateInfo;
+ mSourceVersion = info.version;
+ if (mSize != info.size) {
+ mSize = info.size;
+ if (mDataListener != null) mDataListener.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 % mCoverItem.length;
+ mSetVersion[pos] = info.version;
+ long itemVersion = info.item.getDataVersion();
+ if (mItemVersion[pos] == itemVersion) return null;
+ mItemVersion[pos] = itemVersion;
+ mData[pos] = info.item;
+ mCoverItem[pos] = info.cover;
+ mTotalCount[pos] = info.totalCount;
+ if (mDataListener != null
+ && info.index >= mActiveStart && info.index < mActiveEnd) {
+ mDataListener.onContentChanged(info.index);
+ }
+ }
+ return null;
+ }
+ }
+
+ private <T> T executeAndWait(Callable<T> callable) {
+ FutureTask<T> task = new FutureTask<T>(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() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ boolean updateComplete = false;
+ while (mActive) {
+ synchronized (this) {
+ if (mActive && !mDirty && updateComplete) {
+ if (!mSource.isLoading()) updateLoading(false);
+ Utils.waitWithoutInterrupt(this);
+ continue;
+ }
+ }
+ mDirty = false;
+ updateLoading(true);
+
+ long version = mSource.reload();
+ UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
+ updateComplete = info == null;
+ if (updateComplete) continue;
+ if (info.version != version) {
+ info.version = version;
+ info.size = mSource.getSubMediaSetCount();
+
+ // If the size becomes smaller after reload(), we may
+ // receive from GetUpdateInfo an index which is too
+ // big. Because the main thread is not aware of the size
+ // change until we call UpdateContent.
+ if (info.index >= info.size) {
+ info.index = INDEX_NONE;
+ }
+ }
+ if (info.index != INDEX_NONE) {
+ info.item = mSource.getSubMediaSet(info.index);
+ if (info.item == null) continue;
+ info.cover = info.item.getCoverMediaItem();
+ info.totalCount = info.item.getTotalMediaItemCount();
+ }
+ 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..dd9d8ec41
--- /dev/null
+++ b/src/com/android/gallery3d/app/AlbumSetPage.java
@@ -0,0 +1,764 @@
+/*
+ * 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 android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.HapticFeedbackConstants;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.RelativeLayout;
+import android.widget.Toast;
+
+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.Path;
+import com.android.gallery3d.glrenderer.FadeTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+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.AlbumSetSlotRenderer;
+import com.android.gallery3d.ui.DetailsHelper;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.HelpUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+public class AlbumSetPage extends ActivityState implements
+ SelectionManager.SelectionListener, GalleryActionBar.ClusterRunner,
+ EyePosition.EyePositionListener, MediaSet.SyncListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSetPage";
+
+ private static final int MSG_PICK_ALBUM = 1;
+
+ 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";
+ public static final String KEY_SELECTED_CLUSTER_TYPE = "selected-cluster";
+
+ private static final int DATA_CACHE_SIZE = 256;
+ private static final int REQUEST_DO_ANIMATION = 1;
+
+ private static final int BIT_LOADING_RELOAD = 1;
+ private static final int BIT_LOADING_SYNC = 2;
+
+ private boolean mIsActive = false;
+ private SlotView mSlotView;
+ private AlbumSetSlotRenderer mAlbumSetView;
+ private Config.AlbumSetPage mConfig;
+
+ private MediaSet mMediaSet;
+ private String mTitle;
+ private String mSubtitle;
+ private boolean mShowClusterMenu;
+ private GalleryActionBar mActionBar;
+ private int mSelectedAction;
+
+ protected SelectionManager mSelectionManager;
+ private AlbumSetDataLoader mAlbumSetDataAdapter;
+
+ private boolean mGetContent;
+ private boolean mGetAlbum;
+ private ActionModeHandler mActionModeHandler;
+ private DetailsHelper mDetailsHelper;
+ private MyDetailsSource mDetailsSource;
+ private boolean mShowDetails;
+ private EyePosition mEyePosition;
+ private Handler mHandler;
+
+ // 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 Future<Integer> mSyncTask = null;
+
+ private int mLoadingBits = 0;
+ private boolean mInitialSynced = false;
+
+ private Button mCameraButton;
+ private boolean mShowedEmptyToastForSelf = false;
+
+ @Override
+ protected int getBackgroundColorId() {
+ return R.color.albumset_background;
+ }
+
+ private final GLView mRootPane = new GLView() {
+ private final float mMatrix[] = new float[16];
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mEyePosition.resetPosition();
+
+ int slotViewTop = mActionBar.getHeight() + mConfig.paddingTop;
+ int slotViewBottom = bottom - top - mConfig.paddingBottom;
+ int slotViewRight = right - left;
+
+ if (mShowDetails) {
+ mDetailsHelper.layout(left, slotViewTop, right, bottom);
+ } else {
+ mAlbumSetView.setHighlightItemPath(null);
+ }
+
+ mSlotView.layout(0, slotViewTop, slotViewRight, slotViewBottom);
+ }
+
+ @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 {
+ super.onBackPressed();
+ }
+ }
+
+ private void getSlotCenter(int slotIndex, int center[]) {
+ Rect offset = new Rect();
+ mRootPane.getBoundsOf(mSlotView, offset);
+ Rect r = mSlotView.getSlotRect(slotIndex);
+ int scrollX = mSlotView.getScrollX();
+ int scrollY = mSlotView.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) {
+ if (!mIsActive) return;
+
+ if (mSelectionManager.inSelectionMode()) {
+ MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+ if (targetSet == null) return; // Content is dirty, we shall reload soon
+ mSelectionManager.toggle(targetSet.getPath());
+ mSlotView.invalidate();
+ } else {
+ // Show pressed-up animation for the single-tap.
+ mAlbumSetView.setPressedIndex(slotIndex);
+ mAlbumSetView.setPressedUp();
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_PICK_ALBUM, slotIndex, 0),
+ FadeTexture.DURATION);
+ }
+ }
+
+ private static boolean albumShouldOpenInFilmstrip(MediaSet album) {
+ int itemCount = album.getMediaItemCount();
+ ArrayList<MediaItem> list = (itemCount == 1) ? album.getMediaItem(0, 1) : null;
+ // open in film strip only if there's one item in the album and the item exists
+ return (list != null && !list.isEmpty());
+ }
+
+ WeakReference<Toast> mEmptyAlbumToast = null;
+
+ private void showEmptyAlbumToast(int toastLength) {
+ Toast toast;
+ if (mEmptyAlbumToast != null) {
+ toast = mEmptyAlbumToast.get();
+ if (toast != null) {
+ toast.show();
+ return;
+ }
+ }
+ toast = Toast.makeText(mActivity, R.string.empty_album, toastLength);
+ mEmptyAlbumToast = new WeakReference<Toast>(toast);
+ toast.show();
+ }
+
+ private void hideEmptyAlbumToast() {
+ if (mEmptyAlbumToast != null) {
+ Toast toast = mEmptyAlbumToast.get();
+ if (toast != null) toast.cancel();
+ }
+ }
+
+ private void pickAlbum(int slotIndex) {
+ if (!mIsActive) return;
+
+ MediaSet targetSet = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+ if (targetSet == null) return; // Content is dirty, we shall reload soon
+ if (targetSet.getTotalMediaItemCount() == 0) {
+ showEmptyAlbumToast(Toast.LENGTH_SHORT);
+ return;
+ }
+ hideEmptyAlbumToast();
+
+ String mediaPath = targetSet.getPath().toString();
+
+ Bundle data = new Bundle(getData());
+ int[] center = new int[2];
+ getSlotCenter(slotIndex, center);
+ data.putIntArray(AlbumPage.KEY_SET_CENTER, center);
+ if (mGetAlbum && targetSet.isLeafAlbum()) {
+ 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 && albumShouldOpenInFilmstrip(targetSet)) {
+ data.putParcelable(PhotoPage.KEY_OPEN_ANIMATION_RECT,
+ mSlotView.getSlotRect(slotIndex, mRootPane));
+ data.putInt(PhotoPage.KEY_INDEX_HINT, 0);
+ data.putString(PhotoPage.KEY_MEDIA_SET_PATH,
+ mediaPath);
+ data.putBoolean(PhotoPage.KEY_START_IN_FILMSTRIP, true);
+ data.putBoolean(PhotoPage.KEY_IN_CAMERA_ROLL, targetSet.isCameraRoll());
+ mActivity.getStateManager().startStateForResult(
+ FilmstripPage.class, AlbumPage.REQUEST_PHOTO, data);
+ return;
+ }
+ data.putString(AlbumPage.KEY_MEDIA_PATH, mediaPath);
+
+ // We only show cluster menu in the first AlbumPage in stack
+ boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
+ data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum);
+ mActivity.getStateManager().startStateForResult(
+ AlbumPage.class, REQUEST_DO_ANIMATION, data);
+ }
+ }
+
+ private void onDown(int index) {
+ mAlbumSetView.setPressedIndex(index);
+ }
+
+ private void onUp(boolean followedByLongPress) {
+ if (followedByLongPress) {
+ // Avoid showing press-up animations for long-press.
+ mAlbumSetView.setPressedIndex(-1);
+ } else {
+ mAlbumSetView.setPressedUp();
+ }
+ }
+
+ public void onLongTap(int slotIndex) {
+ if (mGetContent || mGetAlbum) return;
+ MediaSet set = mAlbumSetDataAdapter.getMediaSet(slotIndex);
+ if (set == null) return;
+ mSelectionManager.setAutoLeaveSelectionMode(true);
+ mSelectionManager.toggle(set.getPath());
+ mSlotView.invalidate();
+ }
+
+ @Override
+ 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);
+ data.putInt(KEY_SELECTED_CLUSTER_TYPE, clusterType);
+ mActivity.getStateManager().switchState(this, AlbumSetPage.class, data);
+ }
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ super.onCreate(data, restoreState);
+ initializeViews();
+ initializeData(data);
+ Context context = mActivity.getAndroidContext();
+ 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(context, this);
+ mDetailsSource = new MyDetailsSource();
+ mActionBar = mActivity.getGalleryActionBar();
+ mSelectedAction = data.getInt(AlbumSetPage.KEY_SELECTED_CLUSTER_TYPE,
+ FilterUtils.CLUSTER_BY_ALBUM);
+
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_PICK_ALBUM: {
+ pickAlbum(message.arg1);
+ break;
+ }
+ default: throw new AssertionError(message.what);
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ cleanupCameraButton();
+ mActionModeHandler.destroy();
+ }
+
+ private boolean setupCameraButton() {
+ if (!GalleryUtils.isCameraAvailable(mActivity)) return false;
+ RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity)
+ .findViewById(R.id.gallery_root);
+ if (galleryRoot == null) return false;
+
+ mCameraButton = new Button(mActivity);
+ mCameraButton.setText(R.string.camera_label);
+ mCameraButton.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.frame_overlay_gallery_camera, 0, 0);
+ mCameraButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ GalleryUtils.startCameraActivity(mActivity);
+ }
+ });
+ RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.WRAP_CONTENT,
+ RelativeLayout.LayoutParams.WRAP_CONTENT);
+ lp.addRule(RelativeLayout.CENTER_IN_PARENT);
+ galleryRoot.addView(mCameraButton, lp);
+ return true;
+ }
+
+ private void cleanupCameraButton() {
+ if (mCameraButton == null) return;
+ RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity)
+ .findViewById(R.id.gallery_root);
+ if (galleryRoot == null) return;
+ galleryRoot.removeView(mCameraButton);
+ mCameraButton = null;
+ }
+
+ private void showCameraButton() {
+ if (mCameraButton == null && !setupCameraButton()) return;
+ mCameraButton.setVisibility(View.VISIBLE);
+ }
+
+ private void hideCameraButton() {
+ if (mCameraButton == null) return;
+ mCameraButton.setVisibility(View.GONE);
+ }
+
+ private void clearLoadingBit(int loadingBit) {
+ mLoadingBits &= ~loadingBit;
+ if (mLoadingBits == 0 && mIsActive) {
+ if (mAlbumSetDataAdapter.size() == 0) {
+ // If this is not the top of the gallery folder hierarchy,
+ // tell the parent AlbumSetPage instance to handle displaying
+ // the empty album toast, otherwise show it within this
+ // instance
+ if (mActivity.getStateManager().getStateCount() > 1) {
+ Intent result = new Intent();
+ result.putExtra(AlbumPage.KEY_EMPTY_ALBUM, true);
+ setStateResult(Activity.RESULT_OK, result);
+ mActivity.getStateManager().finishState(this);
+ } else {
+ mShowedEmptyToastForSelf = true;
+ showEmptyAlbumToast(Toast.LENGTH_LONG);
+ mSlotView.invalidate();
+ showCameraButton();
+ }
+ return;
+ }
+ }
+ // Hide the empty album toast if we are in the root instance of
+ // AlbumSetPage and the album is no longer empty (for instance,
+ // after a sync is completed and web albums have been synced)
+ if (mShowedEmptyToastForSelf) {
+ mShowedEmptyToastForSelf = false;
+ hideEmptyAlbumToast();
+ hideCameraButton();
+ }
+ }
+
+ private void setLoadingBit(int loadingBit) {
+ mLoadingBits |= loadingBit;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mIsActive = false;
+ mAlbumSetDataAdapter.pause();
+ mAlbumSetView.pause();
+ mActionModeHandler.pause();
+ mEyePosition.pause();
+ DetailsHelper.pause();
+ // Call disableClusterMenu to avoid receiving callback after paused.
+ // Don't hide menu here otherwise the list menu will disappear earlier than
+ // the action bar, which is janky and unwanted behavior.
+ mActionBar.disableClusterMenu(false);
+ if (mSyncTask != null) {
+ mSyncTask.cancel();
+ mSyncTask = null;
+ clearLoadingBit(BIT_LOADING_SYNC);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mIsActive = true;
+ setContentPane(mRootPane);
+
+ // Set the reload bit here to prevent it exit this page in clearLoadingBit().
+ setLoadingBit(BIT_LOADING_RELOAD);
+ mAlbumSetDataAdapter.resume();
+
+ mAlbumSetView.resume();
+ mEyePosition.resume();
+ mActionModeHandler.resume();
+ if (mShowClusterMenu) {
+ mActionBar.enableClusterMenu(mSelectedAction, this);
+ }
+ if (!mInitialSynced) {
+ setLoadingBit(BIT_LOADING_SYNC);
+ mSyncTask = mMediaSet.requestSync(AlbumSetPage.this);
+ }
+ }
+
+ private void initializeData(Bundle data) {
+ String mediaPath = data.getString(AlbumSetPage.KEY_MEDIA_PATH);
+ mMediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+ mSelectionManager.setSourceMediaSet(mMediaSet);
+ mAlbumSetDataAdapter = new AlbumSetDataLoader(
+ mActivity, mMediaSet, DATA_CACHE_SIZE);
+ mAlbumSetDataAdapter.setLoadingListener(new MyLoadingListener());
+ mAlbumSetView.setModel(mAlbumSetDataAdapter);
+ }
+
+ private void initializeViews() {
+ mSelectionManager = new SelectionManager(mActivity, true);
+ mSelectionManager.setSelectionListener(this);
+
+ mConfig = Config.AlbumSetPage.get(mActivity);
+ mSlotView = new SlotView(mActivity, mConfig.slotViewSpec);
+ mAlbumSetView = new AlbumSetSlotRenderer(
+ mActivity, mSelectionManager, mSlotView, mConfig.labelSpec,
+ mConfig.placeholderColor);
+ mSlotView.setSlotRenderer(mAlbumSetView);
+ mSlotView.setListener(new SlotView.SimpleListener() {
+ @Override
+ public void onDown(int index) {
+ AlbumSetPage.this.onDown(index);
+ }
+
+ @Override
+ public void onUp(boolean followedByLongPress) {
+ AlbumSetPage.this.onUp(followedByLongPress);
+ }
+
+ @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() {
+ @Override
+ public boolean onActionItemClicked(MenuItem item) {
+ return onItemSelected(item);
+ }
+ });
+ mRootPane.addComponent(mSlotView);
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ Activity activity = mActivity;
+ final boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
+ MenuInflater inflater = getSupportMenuInflater();
+
+ if (mGetContent) {
+ inflater.inflate(R.menu.pickup, menu);
+ int typeBits = mData.getInt(
+ Gallery.KEY_TYPE_BITS, DataManager.INCLUDE_IMAGE);
+ mActionBar.setTitle(GalleryUtils.getSelectionModePrompt(typeBits));
+ } else if (mGetAlbum) {
+ inflater.inflate(R.menu.pickup, menu);
+ mActionBar.setTitle(R.string.select_album);
+ } else {
+ inflater.inflate(R.menu.albumset, menu);
+ boolean wasShowingClusterMenu = mShowClusterMenu;
+ mShowClusterMenu = !inAlbum;
+ boolean selectAlbums = !inAlbum &&
+ mActionBar.getClusterTypeAction() == FilterUtils.CLUSTER_BY_ALBUM;
+ MenuItem selectItem = menu.findItem(R.id.action_select);
+ selectItem.setTitle(activity.getString(
+ selectAlbums ? R.string.select_album : R.string.select_group));
+
+ MenuItem cameraItem = menu.findItem(R.id.action_camera);
+ cameraItem.setVisible(GalleryUtils.isCameraAvailable(activity));
+
+ FilterUtils.setupMenuItems(mActionBar, mMediaSet.getPath(), false);
+
+ Intent helpIntent = HelpUtils.getHelpIntent(activity);
+
+ MenuItem helpItem = menu.findItem(R.id.action_general_help);
+ helpItem.setVisible(helpIntent != null);
+ if (helpIntent != null) helpItem.setIntent(helpIntent);
+
+ mActionBar.setTitle(mTitle);
+ mActionBar.setSubtitle(mSubtitle);
+ if (mShowClusterMenu != wasShowingClusterMenu) {
+ if (mShowClusterMenu) {
+ mActionBar.enableClusterMenu(mSelectedAction, this);
+ } else {
+ mActionBar.disableClusterMenu(true);
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ Activity activity = mActivity;
+ switch (item.getItemId()) {
+ case R.id.action_cancel:
+ activity.setResult(Activity.RESULT_CANCELED);
+ activity.finish();
+ return true;
+ 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: {
+ GalleryUtils.startCameraActivity(activity);
+ return true;
+ }
+ case R.id.action_manage_offline: {
+ 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);
+ 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) {
+ if (data != null && data.getBooleanExtra(AlbumPage.KEY_EMPTY_ALBUM, false)) {
+ showEmptyAlbumToast(Toast.LENGTH_SHORT);
+ }
+ switch (requestCode) {
+ case REQUEST_DO_ANIMATION: {
+ mSlotView.startRisingAnimation();
+ }
+ }
+ }
+
+ private String getSelectedString() {
+ int count = mSelectionManager.getSelectedCount();
+ int action = mActionBar.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);
+ }
+
+ @Override
+ public void onSelectionModeChange(int mode) {
+ switch (mode) {
+ case SelectionManager.ENTER_SELECTION_MODE: {
+ mActionBar.disableClusterMenu(true);
+ mActionModeHandler.startActionMode();
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ break;
+ }
+ case SelectionManager.LEAVE_SELECTION_MODE: {
+ mActionModeHandler.finishActionMode();
+ if (mShowClusterMenu) {
+ mActionBar.enableClusterMenu(mSelectedAction, this);
+ }
+ mRootPane.invalidate();
+ break;
+ }
+ case SelectionManager.SELECT_ALL_MODE: {
+ mActionModeHandler.updateSupportedOperation();
+ mRootPane.invalidate();
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onSelectionChange(Path path, boolean selected) {
+ mActionModeHandler.setTitle(getSelectedString());
+ mActionModeHandler.updateSupportedOperation(path, selected);
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mDetailsHelper.hide();
+ mAlbumSetView.setHighlightItemPath(null);
+ mSlotView.invalidate();
+ }
+
+ private void showDetails() {
+ mShowDetails = true;
+ if (mDetailsHelper == null) {
+ mDetailsHelper = new DetailsHelper(mActivity, mRootPane, mDetailsSource);
+ mDetailsHelper.setCloseListener(new CloseListener() {
+ @Override
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ }
+ mDetailsHelper.show();
+ }
+
+ @Override
+ public void onSyncDone(final MediaSet mediaSet, final int resultCode) {
+ if (resultCode == MediaSet.SYNC_RESULT_ERROR) {
+ Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName()) + " result="
+ + resultCode);
+ }
+ ((Activity) mActivity).runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GLRoot root = mActivity.getGLRoot();
+ root.lockRenderThread();
+ try {
+ if (resultCode == MediaSet.SYNC_RESULT_SUCCESS) {
+ mInitialSynced = true;
+ }
+ clearLoadingBit(BIT_LOADING_SYNC);
+ if (resultCode == MediaSet.SYNC_RESULT_ERROR && mIsActive) {
+ Log.w(TAG, "failed to load album set");
+ }
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+ });
+ }
+
+ private class MyLoadingListener implements LoadingListener {
+ @Override
+ public void onLoadingStarted() {
+ setLoadingBit(BIT_LOADING_RELOAD);
+ }
+
+ @Override
+ public void onLoadingFinished(boolean loadingFailed) {
+ clearLoadingBit(BIT_LOADING_RELOAD);
+ }
+ }
+
+ private class MyDetailsSource implements DetailsHelper.DetailsSource {
+ private int mIndex;
+
+ @Override
+ public int size() {
+ return mAlbumSetDataAdapter.size();
+ }
+
+ @Override
+ public int setIndex() {
+ Path id = mSelectionManager.getSelected(false).get(0);
+ mIndex = mAlbumSetDataAdapter.findSet(id);
+ return mIndex;
+ }
+
+ @Override
+ public MediaDetails getDetails() {
+ MediaObject item = mAlbumSetDataAdapter.getMediaSet(mIndex);
+ if (item != null) {
+ mAlbumSetView.setHighlightItemPath(item.getPath());
+ return item.getDetails();
+ } else {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/AppBridge.java b/src/com/android/gallery3d/app/AppBridge.java
new file mode 100644
index 000000000..ee55fa6db
--- /dev/null
+++ b/src/com/android/gallery3d/app/AppBridge.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2012 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.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.gallery3d.ui.ScreenNail;
+
+// This is the bridge to connect a PhotoPage to the external environment.
+public abstract class AppBridge implements Parcelable {
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // These are requests sent from PhotoPage to the app
+ //////////////////////////////////////////////////////////////////////////
+
+ public abstract boolean isPanorama();
+ public abstract boolean isStaticCamera();
+ public abstract ScreenNail attachScreenNail();
+ public abstract void detachScreenNail();
+
+ // Return true if the tap is consumed.
+ public abstract boolean onSingleTapUp(int x, int y);
+
+ // This is used to notify that the screen nail will be drawn in full screen
+ // or not in next draw() call.
+ public abstract void onFullScreenChanged(boolean full);
+
+ //////////////////////////////////////////////////////////////////////////
+ // These are requests send from app to PhotoPage
+ //////////////////////////////////////////////////////////////////////////
+
+ public interface Server {
+ // Set the camera frame relative to GLRootView.
+ public void setCameraRelativeFrame(Rect frame);
+ // Switch to the previous or next picture using the capture animation.
+ // The offset is -1 to switch to the previous picture, 1 to switch to
+ // the next picture.
+ public boolean switchWithCaptureAnimation(int offset);
+ // Enable or disable the swiping gestures (the default is enabled).
+ public void setSwipingEnabled(boolean enabled);
+ // Notify that the ScreenNail is changed.
+ public void notifyScreenNailChanged();
+ // Add a new media item to the secure album.
+ public void addSecureAlbumItem(boolean isVideo, int id);
+ }
+
+ // If server is null, the services are not available.
+ public abstract void setServer(Server server);
+}
diff --git a/src/com/android/gallery3d/app/BatchService.java b/src/com/android/gallery3d/app/BatchService.java
new file mode 100644
index 000000000..564001d5b
--- /dev/null
+++ b/src/com/android/gallery3d/app/BatchService.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 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.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import com.android.gallery3d.util.ThreadPool;
+
+public class BatchService extends Service {
+
+ public class LocalBinder extends Binder {
+ BatchService getService() {
+ return BatchService.this;
+ }
+ }
+
+ private final IBinder mBinder = new LocalBinder();
+ private ThreadPool mThreadPool = new ThreadPool(1, 1);
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ // The threadpool returned by getThreadPool must have only 1 thread
+ // running at a time, as MenuExecutor (atrociously) depends on this
+ // guarantee for synchronization.
+ public ThreadPool getThreadPool() {
+ return mThreadPool;
+ }
+}
diff --git a/src/com/android/gallery3d/app/CommonControllerOverlay.java b/src/com/android/gallery3d/app/CommonControllerOverlay.java
new file mode 100644
index 000000000..9adb4e7a8
--- /dev/null
+++ b/src/com/android/gallery3d/app/CommonControllerOverlay.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2012 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.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+/**
+ * The common playback controller for the Movie Player or Video Trimming.
+ */
+public abstract class CommonControllerOverlay extends FrameLayout implements
+ ControllerOverlay,
+ OnClickListener,
+ TimeBar.Listener {
+
+ protected enum State {
+ PLAYING,
+ PAUSED,
+ ENDED,
+ ERROR,
+ LOADING
+ }
+
+ private static final float ERROR_MESSAGE_RELATIVE_PADDING = 1.0f / 6;
+
+ protected Listener mListener;
+
+ protected final View mBackground;
+ protected TimeBar mTimeBar;
+
+ protected View mMainView;
+ protected final LinearLayout mLoadingView;
+ protected final TextView mErrorView;
+ protected final ImageView mPlayPauseReplayView;
+
+ protected State mState;
+
+ protected boolean mCanReplay = true;
+
+ public void setSeekable(boolean canSeek) {
+ mTimeBar.setSeekable(canSeek);
+ }
+
+ public CommonControllerOverlay(Context context) {
+ super(context);
+
+ mState = State.LOADING;
+ // TODO: Move the following layout code into xml file.
+ LayoutParams wrapContent =
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ LayoutParams matchParent =
+ new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+
+ mBackground = new View(context);
+ mBackground.setBackgroundColor(context.getResources().getColor(R.color.darker_transparent));
+ addView(mBackground, matchParent);
+
+ // Depending on the usage, the timeBar can show a single scrubber, or
+ // multiple ones for trimming.
+ createTimeBar(context);
+ addView(mTimeBar, wrapContent);
+ mTimeBar.setContentDescription(
+ context.getResources().getString(R.string.accessibility_time_bar));
+ mLoadingView = new LinearLayout(context);
+ mLoadingView.setOrientation(LinearLayout.VERTICAL);
+ mLoadingView.setGravity(Gravity.CENTER_HORIZONTAL);
+ ProgressBar spinner = new ProgressBar(context);
+ spinner.setIndeterminate(true);
+ mLoadingView.addView(spinner, wrapContent);
+ TextView loadingText = createOverlayTextView(context);
+ loadingText.setText(R.string.loading_video);
+ mLoadingView.addView(loadingText, wrapContent);
+ addView(mLoadingView, wrapContent);
+
+ mPlayPauseReplayView = new ImageView(context);
+ mPlayPauseReplayView.setImageResource(R.drawable.ic_vidcontrol_play);
+ mPlayPauseReplayView.setContentDescription(
+ context.getResources().getString(R.string.accessibility_play_video));
+ mPlayPauseReplayView.setBackgroundResource(R.drawable.bg_vidcontrol);
+ mPlayPauseReplayView.setScaleType(ScaleType.CENTER);
+ mPlayPauseReplayView.setFocusable(true);
+ mPlayPauseReplayView.setClickable(true);
+ mPlayPauseReplayView.setOnClickListener(this);
+ addView(mPlayPauseReplayView, wrapContent);
+
+ mErrorView = createOverlayTextView(context);
+ addView(mErrorView, matchParent);
+
+ RelativeLayout.LayoutParams params =
+ new RelativeLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ setLayoutParams(params);
+ hide();
+ }
+
+ abstract protected void createTimeBar(Context context);
+
+ private TextView createOverlayTextView(Context context) {
+ TextView view = new TextView(context);
+ view.setGravity(Gravity.CENTER);
+ view.setTextColor(0xFFFFFFFF);
+ view.setPadding(0, 15, 0, 15);
+ return view;
+ }
+
+ @Override
+ public void setListener(Listener listener) {
+ this.mListener = listener;
+ }
+
+ @Override
+ public void setCanReplay(boolean canReplay) {
+ this.mCanReplay = canReplay;
+ }
+
+ @Override
+ public View getView() {
+ return this;
+ }
+
+ @Override
+ public void showPlaying() {
+ mState = State.PLAYING;
+ showMainView(mPlayPauseReplayView);
+ }
+
+ @Override
+ public void showPaused() {
+ mState = State.PAUSED;
+ showMainView(mPlayPauseReplayView);
+ }
+
+ @Override
+ public void showEnded() {
+ mState = State.ENDED;
+ if (mCanReplay) showMainView(mPlayPauseReplayView);
+ }
+
+ @Override
+ public void showLoading() {
+ mState = State.LOADING;
+ showMainView(mLoadingView);
+ }
+
+ @Override
+ public void showErrorMessage(String message) {
+ mState = State.ERROR;
+ int padding = (int) (getMeasuredWidth() * ERROR_MESSAGE_RELATIVE_PADDING);
+ mErrorView.setPadding(
+ padding, mErrorView.getPaddingTop(), padding, mErrorView.getPaddingBottom());
+ mErrorView.setText(message);
+ showMainView(mErrorView);
+ }
+
+ @Override
+ public void setTimes(int currentTime, int totalTime,
+ int trimStartTime, int trimEndTime) {
+ mTimeBar.setTime(currentTime, totalTime, trimStartTime, trimEndTime);
+ }
+
+ public void hide() {
+ mPlayPauseReplayView.setVisibility(View.INVISIBLE);
+ mLoadingView.setVisibility(View.INVISIBLE);
+ mBackground.setVisibility(View.INVISIBLE);
+ mTimeBar.setVisibility(View.INVISIBLE);
+ setVisibility(View.INVISIBLE);
+ setFocusable(true);
+ requestFocus();
+ }
+
+ private void showMainView(View view) {
+ mMainView = view;
+ mErrorView.setVisibility(mMainView == mErrorView ? View.VISIBLE : View.INVISIBLE);
+ mLoadingView.setVisibility(mMainView == mLoadingView ? View.VISIBLE : View.INVISIBLE);
+ mPlayPauseReplayView.setVisibility(
+ mMainView == mPlayPauseReplayView ? View.VISIBLE : View.INVISIBLE);
+ show();
+ }
+
+ @Override
+ public void show() {
+ updateViews();
+ setVisibility(View.VISIBLE);
+ setFocusable(false);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (mListener != null) {
+ if (view == mPlayPauseReplayView) {
+ if (mState == State.ENDED) {
+ if (mCanReplay) {
+ mListener.onReplay();
+ }
+ } else if (mState == State.PAUSED || mState == State.PLAYING) {
+ mListener.onPlayPause();
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (super.onTouchEvent(event)) {
+ return true;
+ }
+ return false;
+ }
+
+ // The paddings of 4 sides which covered by system components. E.g.
+ // +-----------------+\
+ // | Action Bar | insets.top
+ // +-----------------+/
+ // | |
+ // | Content Area | insets.right = insets.left = 0
+ // | |
+ // +-----------------+\
+ // | Navigation Bar | insets.bottom
+ // +-----------------+/
+ // Please see View.fitSystemWindows() for more details.
+ private final Rect mWindowInsets = new Rect();
+
+ @Override
+ protected boolean fitSystemWindows(Rect insets) {
+ // We don't set the paddings of this View, otherwise,
+ // the content will get cropped outside window
+ mWindowInsets.set(insets);
+ return true;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ Rect insets = mWindowInsets;
+ int pl = insets.left; // the left paddings
+ int pr = insets.right;
+ int pt = insets.top;
+ int pb = insets.bottom;
+
+ int h = bottom - top;
+ int w = right - left;
+ boolean error = mErrorView.getVisibility() == View.VISIBLE;
+
+ int y = h - pb;
+ // Put both TimeBar and Background just above the bottom system
+ // component.
+ // But extend the background to the width of the screen, since we don't
+ // care if it will be covered by a system component and it looks better.
+ mBackground.layout(0, y - mTimeBar.getBarHeight(), w, y);
+ mTimeBar.layout(pl, y - mTimeBar.getPreferredHeight(), w - pr, y);
+
+ // Put the play/pause/next/ previous button in the center of the screen
+ layoutCenteredView(mPlayPauseReplayView, 0, 0, w, h);
+
+ if (mMainView != null) {
+ layoutCenteredView(mMainView, 0, 0, w, h);
+ }
+ }
+
+ private void layoutCenteredView(View view, int l, int t, int r, int b) {
+ int cw = view.getMeasuredWidth();
+ int ch = view.getMeasuredHeight();
+ int cl = (r - l - cw) / 2;
+ int ct = (b - t - ch) / 2;
+ view.layout(cl, ct, cl + cw, ct + ch);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ measureChildren(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ protected void updateViews() {
+ mBackground.setVisibility(View.VISIBLE);
+ mTimeBar.setVisibility(View.VISIBLE);
+ Resources resources = getContext().getResources();
+ int imageResource = R.drawable.ic_vidcontrol_reload;
+ String contentDescription = resources.getString(R.string.accessibility_reload_video);
+ if (mState == State.PAUSED) {
+ imageResource = R.drawable.ic_vidcontrol_play;
+ contentDescription = resources.getString(R.string.accessibility_play_video);
+ } else if (mState == State.PLAYING) {
+ imageResource = R.drawable.ic_vidcontrol_pause;
+ contentDescription = resources.getString(R.string.accessibility_pause_video);
+ }
+
+ mPlayPauseReplayView.setImageResource(imageResource);
+ mPlayPauseReplayView.setContentDescription(contentDescription);
+ mPlayPauseReplayView.setVisibility(
+ (mState != State.LOADING && mState != State.ERROR &&
+ !(mState == State.ENDED && !mCanReplay))
+ ? View.VISIBLE : View.GONE);
+ requestLayout();
+ }
+
+ // TimeBar listener
+
+ @Override
+ public void onScrubbingStart() {
+ mListener.onSeekStart();
+ }
+
+ @Override
+ public void onScrubbingMove(int time) {
+ mListener.onSeekMove(time);
+ }
+
+ @Override
+ public void onScrubbingEnd(int time, int trimStartTime, int trimEndTime) {
+ mListener.onSeekEnd(time, trimStartTime, trimEndTime);
+ }
+}
diff --git a/src/com/android/gallery3d/app/Config.java b/src/com/android/gallery3d/app/Config.java
new file mode 100644
index 000000000..7183acc33
--- /dev/null
+++ b/src/com/android/gallery3d/app/Config.java
@@ -0,0 +1,127 @@
+/*
+ * 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 android.content.Context;
+import android.content.res.Resources;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.AlbumSetSlotRenderer;
+import com.android.gallery3d.ui.SlotView;
+
+final class Config {
+ public static class AlbumSetPage {
+ private static AlbumSetPage sInstance;
+
+ public SlotView.Spec slotViewSpec;
+ public AlbumSetSlotRenderer.LabelSpec labelSpec;
+ public int paddingTop;
+ public int paddingBottom;
+ public int placeholderColor;
+
+ public static synchronized AlbumSetPage get(Context context) {
+ if (sInstance == null) {
+ sInstance = new AlbumSetPage(context);
+ }
+ return sInstance;
+ }
+
+ private AlbumSetPage(Context context) {
+ Resources r = context.getResources();
+
+ placeholderColor = r.getColor(R.color.albumset_placeholder);
+
+ slotViewSpec = new SlotView.Spec();
+ slotViewSpec.rowsLand = r.getInteger(R.integer.albumset_rows_land);
+ slotViewSpec.rowsPort = r.getInteger(R.integer.albumset_rows_port);
+ slotViewSpec.slotGap = r.getDimensionPixelSize(R.dimen.albumset_slot_gap);
+ slotViewSpec.slotHeightAdditional = 0;
+
+ paddingTop = r.getDimensionPixelSize(R.dimen.albumset_padding_top);
+ paddingBottom = r.getDimensionPixelSize(R.dimen.albumset_padding_bottom);
+
+ labelSpec = new AlbumSetSlotRenderer.LabelSpec();
+ labelSpec.labelBackgroundHeight = r.getDimensionPixelSize(
+ R.dimen.albumset_label_background_height);
+ labelSpec.titleOffset = r.getDimensionPixelSize(
+ R.dimen.albumset_title_offset);
+ labelSpec.countOffset = r.getDimensionPixelSize(
+ R.dimen.albumset_count_offset);
+ labelSpec.titleFontSize = r.getDimensionPixelSize(
+ R.dimen.albumset_title_font_size);
+ labelSpec.countFontSize = r.getDimensionPixelSize(
+ R.dimen.albumset_count_font_size);
+ labelSpec.leftMargin = r.getDimensionPixelSize(
+ R.dimen.albumset_left_margin);
+ labelSpec.titleRightMargin = r.getDimensionPixelSize(
+ R.dimen.albumset_title_right_margin);
+ labelSpec.iconSize = r.getDimensionPixelSize(
+ R.dimen.albumset_icon_size);
+ labelSpec.backgroundColor = r.getColor(
+ R.color.albumset_label_background);
+ labelSpec.titleColor = r.getColor(R.color.albumset_label_title);
+ labelSpec.countColor = r.getColor(R.color.albumset_label_count);
+ }
+ }
+
+ public static class AlbumPage {
+ private static AlbumPage sInstance;
+
+ public SlotView.Spec slotViewSpec;
+ public int placeholderColor;
+
+ public static synchronized AlbumPage get(Context context) {
+ if (sInstance == null) {
+ sInstance = new AlbumPage(context);
+ }
+ return sInstance;
+ }
+
+ private AlbumPage(Context context) {
+ Resources r = context.getResources();
+
+ placeholderColor = r.getColor(R.color.album_placeholder);
+
+ slotViewSpec = new SlotView.Spec();
+ slotViewSpec.rowsLand = r.getInteger(R.integer.album_rows_land);
+ slotViewSpec.rowsPort = r.getInteger(R.integer.album_rows_port);
+ slotViewSpec.slotGap = r.getDimensionPixelSize(R.dimen.album_slot_gap);
+ }
+ }
+
+ public static class ManageCachePage extends AlbumSetPage {
+ private static ManageCachePage sInstance;
+
+ public final int cachePinSize;
+ public final int cachePinMargin;
+
+ 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();
+ cachePinSize = r.getDimensionPixelSize(R.dimen.cache_pin_size);
+ cachePinMargin = r.getDimensionPixelSize(R.dimen.cache_pin_margin);
+ }
+ }
+}
+
diff --git a/src/com/android/gallery3d/app/ControllerOverlay.java b/src/com/android/gallery3d/app/ControllerOverlay.java
new file mode 100644
index 000000000..078f59e28
--- /dev/null
+++ b/src/com/android/gallery3d/app/ControllerOverlay.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.app;
+
+import android.view.View;
+
+public interface ControllerOverlay {
+
+ interface Listener {
+ void onPlayPause();
+ void onSeekStart();
+ void onSeekMove(int time);
+ void onSeekEnd(int time, int trimStartTime, int trimEndTime);
+ void onShown();
+ void onHidden();
+ void onReplay();
+ }
+
+ void setListener(Listener listener);
+
+ void setCanReplay(boolean canReplay);
+
+ /**
+ * @return The overlay view that should be added to the player.
+ */
+ View getView();
+
+ void show();
+
+ void showPlaying();
+
+ void showPaused();
+
+ void showEnded();
+
+ void showLoading();
+
+ void showErrorMessage(String message);
+
+ void setTimes(int currentTime, int totalTime,
+ int trimStartTime, int trimEndTime);
+}
diff --git a/src/com/android/gallery3d/app/DialogPicker.java b/src/com/android/gallery3d/app/DialogPicker.java
new file mode 100644
index 000000000..7ca86e5b4
--- /dev/null
+++ b/src/com/android/gallery3d/app/DialogPicker.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.app;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.android.gallery3d.util.GalleryUtils;
+
+public class DialogPicker extends PickerActivity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ 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);
+ }
+}
diff --git a/src/com/android/gallery3d/app/EyePosition.java b/src/com/android/gallery3d/app/EyePosition.java
new file mode 100644
index 000000000..d99d97b0e
--- /dev/null
+++ b/src/com/android/gallery3d/app/EyePosition.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.app;
+
+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.util.FloatMath;
+import android.view.Display;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
+public class EyePosition {
+ @SuppressWarnings("unused")
+ 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 float USER_ANGEL = (float) Math.toRadians(10);
+ private static final float USER_ANGEL_COS = FloatMath.cos(USER_ANGEL);
+ private static final float USER_ANGEL_SIN = FloatMath.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();
+
+ // The 3D effect where the photo albums fan out in 3D based on angle
+ // of device tilt is currently disabled.
+/*
+ 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 = FloatMath.sqrt(tx * tx + ty * ty + tz * tz);
+ float glength = FloatMath.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 = -FloatMath.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 = -FloatMath.sqrt(
+ mUserDistance * mUserDistance - mX * mX - mY * mY);
+ mListener.onEyePositionChanged(mX, mY, mZ);
+ }
+
+ private class PositionListener implements SensorEventListener {
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+
+ @Override
+ 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/FilmstripPage.java b/src/com/android/gallery3d/app/FilmstripPage.java
new file mode 100644
index 000000000..a9726cdc9
--- /dev/null
+++ b/src/com/android/gallery3d/app/FilmstripPage.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2012 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 FilmstripPage extends PhotoPage {
+
+}
diff --git a/src/com/android/gallery3d/app/FilterUtils.java b/src/com/android/gallery3d/app/FilterUtils.java
new file mode 100644
index 000000000..bc28a9cc1
--- /dev/null
+++ b/src/com/android/gallery3d/app/FilterUtils.java
@@ -0,0 +1,257 @@
+/*
+ * 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 {
+ @SuppressWarnings("unused")
+ 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 actionBar, 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(actionBar, CLUSTER_BY_TIME,
+ (ctype & CLUSTER_BY_TIME) != 0, (ccurrent & CLUSTER_BY_TIME) != 0);
+ setMenuItemApplied(actionBar, CLUSTER_BY_LOCATION,
+ (ctype & CLUSTER_BY_LOCATION) != 0, (ccurrent & CLUSTER_BY_LOCATION) != 0);
+ setMenuItemApplied(actionBar, CLUSTER_BY_TAG,
+ (ctype & CLUSTER_BY_TAG) != 0, (ccurrent & CLUSTER_BY_TAG) != 0);
+ setMenuItemApplied(actionBar, CLUSTER_BY_FACE,
+ (ctype & CLUSTER_BY_FACE) != 0, (ccurrent & CLUSTER_BY_FACE) != 0);
+
+ actionBar.setClusterItemVisibility(CLUSTER_BY_ALBUM, !inAlbum || ctype == 0);
+
+ setMenuItemApplied(actionBar, 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(actionBar, R.string.show_images_only,
+ (ftype & FILTER_IMAGE_ONLY) != 0,
+ (ftype & FILTER_IMAGE_ONLY) == 0 && ftypef == 0,
+ (fcurrent & FILTER_IMAGE_ONLY) != 0);
+ setMenuItemAppliedEnabled(actionBar, R.string.show_videos_only,
+ (ftype & FILTER_VIDEO_ONLY) != 0,
+ (ftype & FILTER_VIDEO_ONLY) == 0 && ftypef == 0,
+ (fcurrent & FILTER_VIDEO_ONLY) != 0);
+ setMenuItemAppliedEnabled(actionBar, 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 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();
+ }
+}
diff --git a/src/com/android/gallery3d/app/Gallery.java b/src/com/android/gallery3d/app/Gallery.java
new file mode 100644
index 000000000..baef56b44
--- /dev/null
+++ b/src/com/android/gallery3d/app/Gallery.java
@@ -0,0 +1,274 @@
+/*
+ * 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 android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+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.picasasource.PicasaSource;
+import com.android.gallery3d.util.GalleryUtils;
+
+public final class Gallery extends AbstractGalleryActivity implements OnCancelListener {
+ public static final String EXTRA_SLIDESHOW = "slideshow";
+ public static final String EXTRA_DREAM = "dream";
+ 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";
+ public static final String KEY_DISMISS_KEYGUARD = "dismiss-keyguard";
+
+ private static final String TAG = "Gallery";
+ private Dialog mVersionCheckDialog;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+
+ if (getIntent().getBooleanExtra(KEY_DISMISS_KEYGUARD, false)) {
+ getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ }
+
+ setContentView(R.layout.main);
+
+ 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() {
+ PicasaSource.showSignInReminder(this);
+ Bundle data = new Bundle();
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH,
+ getDataManager().getTopSetPath(DataManager.INCLUDE_ALL));
+ getStateManager().startState(AlbumSetPage.class, data);
+ mVersionCheckDialog = PicasaSource.getVersionCheckDialog(this);
+ if (mVersionCheckDialog != null) {
+ mVersionCheckDialog.setOnCancelListener(this);
+ }
+ }
+
+ 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 GalleryUtils.MIME_TYPE_PANORAMA360.equals(type)
+ ? MediaItem.MIME_TYPE_JPEG : 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(), intent.getType());
+ 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);
+ if (intent.getBooleanExtra(EXTRA_DREAM, false)) {
+ data.putBoolean(SlideshowPage.KEY_DREAM, 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 (uri == null) {
+ 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);
+ } else 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 setPath = dm.findPathByUri(uri, null);
+ MediaSet mediaSet = null;
+ if (setPath != null) {
+ mediaSet = (MediaSet) dm.getMediaObject(setPath);
+ }
+ if (mediaSet != null) {
+ if (mediaSet.isLeafAlbum()) {
+ data.putString(AlbumPage.KEY_MEDIA_PATH, setPath.toString());
+ data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
+ dm.getTopSetPath(DataManager.INCLUDE_ALL));
+ getStateManager().startState(AlbumPage.class, data);
+ } else {
+ data.putString(AlbumSetPage.KEY_MEDIA_PATH, setPath.toString());
+ getStateManager().startState(AlbumSetPage.class, data);
+ }
+ } else {
+ startDefaultPage();
+ }
+ } else {
+ Path itemPath = dm.findPathByUri(uri, contentType);
+ Path albumPath = dm.getDefaultSetOf(itemPath);
+
+ data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, itemPath.toString());
+
+ // TODO: Make the parameter "SingleItemOnly" public so other
+ // activities can reference it.
+ boolean singleItemOnly = (albumPath == null)
+ || intent.getBooleanExtra("SingleItemOnly", false);
+ if (!singleItemOnly) {
+ data.putString(PhotoPage.KEY_MEDIA_SET_PATH, albumPath.toString());
+ // when FLAG_ACTIVITY_NEW_TASK is set, (e.g. when intent is fired
+ // from notification), back button should behave the same as up button
+ // rather than taking users back to the home screen
+ if (intent.getBooleanExtra(PhotoPage.KEY_TREAT_BACK_AS_UP, false)
+ || ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0)) {
+ data.putBoolean(PhotoPage.KEY_TREAT_BACK_AS_UP, true);
+ }
+ }
+
+ getStateManager().startState(SinglePhotoPage.class, data);
+ }
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ Utils.assertTrue(getStateManager().getStateCount() > 0);
+ super.onResume();
+ if (mVersionCheckDialog != null) {
+ mVersionCheckDialog.show();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (mVersionCheckDialog != null) {
+ mVersionCheckDialog.dismiss();
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (dialog == mVersionCheckDialog) {
+ mVersionCheckDialog = null;
+ }
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ final boolean isTouchPad = (event.getSource()
+ & InputDevice.SOURCE_CLASS_POSITION) != 0;
+ if (isTouchPad) {
+ float maxX = event.getDevice().getMotionRange(MotionEvent.AXIS_X).getMax();
+ float maxY = event.getDevice().getMotionRange(MotionEvent.AXIS_Y).getMax();
+ View decor = getWindow().getDecorView();
+ float scaleX = decor.getWidth() / maxX;
+ float scaleY = decor.getHeight() / maxY;
+ float x = event.getX() * scaleX;
+ //x = decor.getWidth() - x; // invert x
+ float y = event.getY() * scaleY;
+ //y = decor.getHeight() - y; // invert y
+ MotionEvent touchEvent = MotionEvent.obtain(event.getDownTime(),
+ event.getEventTime(), event.getAction(), x, y, event.getMetaState());
+ return dispatchTouchEvent(touchEvent);
+ }
+ return super.onGenericMotionEvent(event);
+ }
+}
diff --git a/src/com/android/gallery3d/app/GalleryActionBar.java b/src/com/android/gallery3d/app/GalleryActionBar.java
new file mode 100644
index 000000000..588f5842a
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryActionBar.java
@@ -0,0 +1,438 @@
+/*
+ * 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.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.ActionBar.OnMenuVisibilityListener;
+import android.app.ActionBar.OnNavigationListener;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ShareActionProvider;
+import android.widget.TextView;
+import android.widget.TwoLineListItem;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.ArrayList;
+
+public class GalleryActionBar implements OnNavigationListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "GalleryActionBar";
+
+ private ClusterRunner mClusterRunner;
+ private CharSequence[] mTitles;
+ private ArrayList<Integer> mActions;
+ private Context mContext;
+ private LayoutInflater mInflater;
+ private AbstractGalleryActivity mActivity;
+ private ActionBar mActionBar;
+ private int mCurrentIndex;
+ private ClusterAdapter mAdapter = new ClusterAdapter();
+
+ private AlbumModeAdapter mAlbumModeAdapter;
+ private OnAlbumModeSelectedListener mAlbumModeListener;
+ private int mLastAlbumModeSelected;
+ private CharSequence [] mAlbumModes;
+ public static final int ALBUM_FILMSTRIP_MODE_SELECTED = 0;
+ public static final int ALBUM_GRID_MODE_SELECTED = 1;
+
+ public interface ClusterRunner {
+ public void doCluster(int id);
+ }
+
+ public interface OnAlbumModeSelectedListener {
+ public void onAlbumModeSelected(int mode);
+ }
+
+ private static class ActionItem {
+ public int action;
+ public boolean enabled;
+ public boolean visible;
+ public int spinnerTitle;
+ 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 spinnerTitle,
+ int dialogTitle, int clusterBy) {
+ this.action = action;
+ this.enabled = enabled;
+ this.spinnerTitle = spinnerTitle;
+ 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 class ClusterAdapter extends BaseAdapter {
+
+ @Override
+ public int getCount() {
+ return sClusterItems.length;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return sClusterItems[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return sClusterItems[position].action;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.action_bar_text,
+ parent, false);
+ }
+ TextView view = (TextView) convertView;
+ view.setText(sClusterItems[position].spinnerTitle);
+ return convertView;
+ }
+ }
+
+ private class AlbumModeAdapter extends BaseAdapter {
+ @Override
+ public int getCount() {
+ return mAlbumModes.length;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mAlbumModes[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.action_bar_two_line_text,
+ parent, false);
+ }
+ TwoLineListItem view = (TwoLineListItem) convertView;
+ view.getText1().setText(mActionBar.getTitle());
+ view.getText2().setText((CharSequence) getItem(position));
+ return convertView;
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.action_bar_text,
+ parent, false);
+ }
+ TextView view = (TextView) convertView;
+ view.setText((CharSequence) getItem(position));
+ return convertView;
+ }
+ }
+
+ public static String getClusterByTypeString(Context context, int type) {
+ for (ActionItem item : sClusterItems) {
+ if (item.action == type) {
+ return context.getString(item.clusterBy);
+ }
+ }
+ return null;
+ }
+
+ public GalleryActionBar(AbstractGalleryActivity activity) {
+ mActionBar = activity.getActionBar();
+ mContext = activity.getAndroidContext();
+ mActivity = activity;
+ mInflater = ((Activity) mActivity).getLayoutInflater();
+ mCurrentIndex = 0;
+ }
+
+ private void createDialogData() {
+ ArrayList<CharSequence> titles = new ArrayList<CharSequence>();
+ mActions = new ArrayList<Integer>();
+ 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 int getHeight() {
+ return mActionBar != null ? mActionBar.getHeight() : 0;
+ }
+
+ 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() {
+ return sClusterItems[mCurrentIndex].action;
+ }
+
+ public void enableClusterMenu(int action, ClusterRunner runner) {
+ if (mActionBar != null) {
+ // Don't set cluster runner until action bar is ready.
+ mClusterRunner = null;
+ mActionBar.setListNavigationCallbacks(mAdapter, this);
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+ setSelectedAction(action);
+ mClusterRunner = runner;
+ }
+ }
+
+ // The only use case not to hideMenu in this method is to ensure
+ // all elements disappear at the same time when exiting gallery.
+ // hideMenu should always be true in all other cases.
+ public void disableClusterMenu(boolean hideMenu) {
+ if (mActionBar != null) {
+ mClusterRunner = null;
+ if (hideMenu) {
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ }
+ }
+ }
+
+ public void onConfigurationChanged() {
+ if (mActionBar != null && mAlbumModeListener != null) {
+ OnAlbumModeSelectedListener listener = mAlbumModeListener;
+ enableAlbumModeMenu(mLastAlbumModeSelected, listener);
+ }
+ }
+
+ public void enableAlbumModeMenu(int selected, OnAlbumModeSelectedListener listener) {
+ if (mActionBar != null) {
+ if (mAlbumModeAdapter == null) {
+ // Initialize the album mode options if they haven't been already
+ Resources res = mActivity.getResources();
+ mAlbumModes = new CharSequence[] {
+ res.getString(R.string.switch_photo_filmstrip),
+ res.getString(R.string.switch_photo_grid)};
+ mAlbumModeAdapter = new AlbumModeAdapter();
+ }
+ mAlbumModeListener = null;
+ mLastAlbumModeSelected = selected;
+ mActionBar.setListNavigationCallbacks(mAlbumModeAdapter, this);
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
+ mActionBar.setSelectedNavigationItem(selected);
+ mAlbumModeListener = listener;
+ }
+ }
+
+ public void disableAlbumModeMenu(boolean hideMenu) {
+ if (mActionBar != null) {
+ mAlbumModeListener = null;
+ if (hideMenu) {
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ }
+ }
+ }
+
+ public void showClusterDialog(final ClusterRunner clusterRunner) {
+ createDialogData();
+ final ArrayList<Integer> actions = mActions;
+ new AlertDialog.Builder(mContext).setTitle(R.string.group_by).setItems(
+ mTitles, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // Need to lock rendering when operations invoked by system UI (main thread) are
+ // modifying slot data used in GL thread for rendering.
+ mActivity.getGLRoot().lockRenderThread();
+ try {
+ clusterRunner.doCluster(actions.get(which).intValue());
+ } finally {
+ mActivity.getGLRoot().unlockRenderThread();
+ }
+ }
+ }).create().show();
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private void setHomeButtonEnabled(boolean enabled) {
+ if (mActionBar != null) mActionBar.setHomeButtonEnabled(enabled);
+ }
+
+ public void setDisplayOptions(boolean displayHomeAsUp, boolean showTitle) {
+ if (mActionBar == null) return;
+ int options = 0;
+ if (displayHomeAsUp) options |= ActionBar.DISPLAY_HOME_AS_UP;
+ if (showTitle) options |= ActionBar.DISPLAY_SHOW_TITLE;
+
+ mActionBar.setDisplayOptions(options,
+ ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE);
+ mActionBar.setHomeButtonEnabled(displayHomeAsUp);
+ }
+
+ public void setTitle(String title) {
+ if (mActionBar != null) mActionBar.setTitle(title);
+ }
+
+ public void setTitle(int titleId) {
+ if (mActionBar != null) {
+ mActionBar.setTitle(mContext.getString(titleId));
+ }
+ }
+
+ public void setSubtitle(String title) {
+ if (mActionBar != null) mActionBar.setSubtitle(title);
+ }
+
+ public void show() {
+ if (mActionBar != null) mActionBar.show();
+ }
+
+ public void hide() {
+ if (mActionBar != null) mActionBar.hide();
+ }
+
+ public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+ if (mActionBar != null) mActionBar.addOnMenuVisibilityListener(listener);
+ }
+
+ public void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener) {
+ if (mActionBar != null) mActionBar.removeOnMenuVisibilityListener(listener);
+ }
+
+ public boolean setSelectedAction(int type) {
+ if (mActionBar == null) return false;
+
+ for (int i = 0, n = sClusterItems.length; i < n; i++) {
+ ActionItem item = sClusterItems[i];
+ if (item.action == type) {
+ mActionBar.setSelectedNavigationItem(i);
+ mCurrentIndex = i;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onNavigationItemSelected(int itemPosition, long itemId) {
+ if (itemPosition != mCurrentIndex && mClusterRunner != null
+ || mAlbumModeListener != null) {
+ // Need to lock rendering when operations invoked by system UI (main thread) are
+ // modifying slot data used in GL thread for rendering.
+ mActivity.getGLRoot().lockRenderThread();
+ try {
+ if (mAlbumModeListener != null) {
+ mAlbumModeListener.onAlbumModeSelected(itemPosition);
+ } else {
+ mClusterRunner.doCluster(sClusterItems[itemPosition].action);
+ }
+ } finally {
+ mActivity.getGLRoot().unlockRenderThread();
+ }
+ }
+ return false;
+ }
+
+ private Menu mActionBarMenu;
+ private ShareActionProvider mSharePanoramaActionProvider;
+ private ShareActionProvider mShareActionProvider;
+ private Intent mSharePanoramaIntent;
+ private Intent mShareIntent;
+
+ public void createActionBarMenu(int menuRes, Menu menu) {
+ mActivity.getMenuInflater().inflate(menuRes, menu);
+ mActionBarMenu = menu;
+
+ MenuItem item = menu.findItem(R.id.action_share_panorama);
+ if (item != null) {
+ mSharePanoramaActionProvider = (ShareActionProvider)
+ item.getActionProvider();
+ mSharePanoramaActionProvider
+ .setShareHistoryFileName("panorama_share_history.xml");
+ mSharePanoramaActionProvider.setShareIntent(mSharePanoramaIntent);
+ }
+
+ item = menu.findItem(R.id.action_share);
+ if (item != null) {
+ mShareActionProvider = (ShareActionProvider)
+ item.getActionProvider();
+ mShareActionProvider
+ .setShareHistoryFileName("share_history.xml");
+ mShareActionProvider.setShareIntent(mShareIntent);
+ }
+ }
+
+ public Menu getMenu() {
+ return mActionBarMenu;
+ }
+
+ public void setShareIntents(Intent sharePanoramaIntent, Intent shareIntent,
+ ShareActionProvider.OnShareTargetSelectedListener onShareListener) {
+ mSharePanoramaIntent = sharePanoramaIntent;
+ if (mSharePanoramaActionProvider != null) {
+ mSharePanoramaActionProvider.setShareIntent(sharePanoramaIntent);
+ }
+ mShareIntent = shareIntent;
+ if (mShareActionProvider != null) {
+ mShareActionProvider.setShareIntent(shareIntent);
+ mShareActionProvider.setOnShareTargetSelectedListener(
+ onShareListener);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/GalleryApp.java b/src/com/android/gallery3d/app/GalleryApp.java
new file mode 100644
index 000000000..b56b8a82c
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryApp.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.app;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.DownloadCache;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.util.ThreadPool;
+
+public interface GalleryApp {
+ public DataManager getDataManager();
+
+ public StitchingProgressManager getStitchingProgressManager();
+ 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..2abdaa0c1
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryAppImpl.java
@@ -0,0 +1,127 @@
+/*
+ * 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 android.app.Application;
+import android.content.Context;
+import android.os.AsyncTask;
+
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.DownloadCache;
+import com.android.gallery3d.data.ImageCacheService;
+import com.android.gallery3d.gadget.WidgetUtils;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.UsageStatistics;
+import com.android.photos.data.MediaCache;
+
+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 Object mLock = new Object();
+ private DataManager mDataManager;
+ private ThreadPool mThreadPool;
+ private DownloadCache mDownloadCache;
+ private StitchingProgressManager mStitchingProgressManager;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ com.android.camera.Util.initialize(this);
+ initializeAsyncTask();
+ GalleryUtils.initialize(this);
+ WidgetUtils.initialize(this);
+ PicasaSource.initialize(this);
+ UsageStatistics.initialize(this);
+ MediaCache.initialize(this);
+
+ mStitchingProgressManager = LightCycleHelper.createStitchingManagerInstance(this);
+ if (mStitchingProgressManager != null) {
+ mStitchingProgressManager.addChangeListener(getDataManager());
+ }
+ }
+
+ @Override
+ public Context getAndroidContext() {
+ return this;
+ }
+
+ @Override
+ public synchronized DataManager getDataManager() {
+ if (mDataManager == null) {
+ mDataManager = new DataManager(this);
+ mDataManager.initializeSourceMap();
+ }
+ return mDataManager;
+ }
+
+ @Override
+ public StitchingProgressManager getStitchingProgressManager() {
+ return mStitchingProgressManager;
+ }
+
+ @Override
+ public ImageCacheService getImageCacheService() {
+ // This method may block on file I/O so a dedicated lock is needed here.
+ synchronized (mLock) {
+ if (mImageCacheService == null) {
+ mImageCacheService = new ImageCacheService(getAndroidContext());
+ }
+ return mImageCacheService;
+ }
+ }
+
+ @Override
+ public synchronized ThreadPool getThreadPool() {
+ if (mThreadPool == null) {
+ mThreadPool = new ThreadPool();
+ }
+ return mThreadPool;
+ }
+
+ @Override
+ 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;
+ }
+
+ private void initializeAsyncTask() {
+ // AsyncTask class needs to be loaded in UI thread.
+ // So we load it here to comply the rule.
+ try {
+ Class.forName(AsyncTask.class.getName());
+ } catch (ClassNotFoundException e) {
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/GalleryContext.java b/src/com/android/gallery3d/app/GalleryContext.java
new file mode 100644
index 000000000..06f4fe4d1
--- /dev/null
+++ b/src/com/android/gallery3d/app/GalleryContext.java
@@ -0,0 +1,34 @@
+/*
+ * 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 android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.util.ThreadPool;
+
+public interface GalleryContext {
+ public DataManager getDataManager();
+
+ public Context getAndroidContext();
+
+ public Looper getMainLooper();
+ public Resources getResources();
+ 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..e94df9307
--- /dev/null
+++ b/src/com/android/gallery3d/app/LoadingListener.java
@@ -0,0 +1,27 @@
+/*
+ * 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();
+ /**
+ * Called when loading is complete or no further progress can be made.
+ *
+ * @param loadingFailed true if data source cannot provide requested data
+ */
+ public void onLoadingFinished(boolean loadingFailed);
+}
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..4f5c35819
--- /dev/null
+++ b/src/com/android/gallery3d/app/ManageCachePage.java
@@ -0,0 +1,419 @@
+/*
+ * 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 android.app.Activity;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.format.Formatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.FrameLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.ui.CacheStorageUsageInfo;
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.ManageCacheDrawer;
+import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SlotView;
+import com.android.gallery3d.ui.SynchronizedHandler;
+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 java.util.ArrayList;
+
+public class ManageCachePage extends ActivityState implements
+ SelectionManager.SelectionListener, MenuExecutor.ProgressListener,
+ EyePosition.EyePositionListener, OnClickListener {
+ public static final String KEY_MEDIA_PATH = "media-path";
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "ManageCachePage";
+
+ private static final int DATA_CACHE_SIZE = 256;
+ private static final int MSG_REFRESH_STORAGE_INFO = 1;
+ private static final int MSG_REQUEST_LAYOUT = 2;
+ private static final int PROGRESS_BAR_MAX = 10000;
+
+ private SlotView mSlotView;
+ private MediaSet mMediaSet;
+
+ protected SelectionManager mSelectionManager;
+ protected ManageCacheDrawer mSelectionDrawer;
+ private AlbumSetDataLoader mAlbumSetDataAdapter;
+
+ 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 View mFooterContent;
+ private CacheStorageUsageInfo mCacheStorageInfo;
+ private Future<Void> mUpdateStorageInfo;
+ private Handler mHandler;
+ private boolean mLayoutReady = false;
+
+ @Override
+ protected int getBackgroundColorId() {
+ return R.color.cache_background;
+ }
+
+ private GLView mRootPane = new GLView() {
+ private float mMatrix[] = new float[16];
+
+ @Override
+ protected void renderBackground(GLCanvas view) {
+ view.clearBuffer(getBackgroundColor());
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ // Hack: our layout depends on other components on the screen.
+ // We assume the other components will complete before we get a change
+ // to run a message in main thread.
+ if (!mLayoutReady) {
+ mHandler.sendEmptyMessage(MSG_REQUEST_LAYOUT);
+ return;
+ }
+ mLayoutReady = false;
+
+ mEyePosition.resetPosition();
+ int slotViewTop = mActivity.getGalleryActionBar().getHeight();
+ int slotViewBottom = bottom - top;
+
+ View footer = mActivity.findViewById(R.id.footer);
+ if (footer != null) {
+ int location[] = {0, 0};
+ footer.getLocationOnScreen(location);
+ slotViewBottom = location[1];
+ }
+
+ mSlotView.layout(0, slotViewTop, right - left, slotViewBottom);
+ }
+
+ @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();
+ }
+
+ private void onDown(int index) {
+ mSelectionDrawer.setPressedIndex(index);
+ }
+
+ private void onUp() {
+ mSelectionDrawer.setPressedIndex(-1);
+ }
+
+ 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();
+ mCacheStorageInfo.increaseTargetCacheSize(
+ (isFullyCached ^ isSelected) ? -sizeOfTarget : sizeOfTarget);
+ refreshCacheStorageInfo();
+
+ mSelectionManager.toggle(path);
+ mSlotView.invalidate();
+ }
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ super.onCreate(data, restoreState);
+ mCacheStorageInfo = new CacheStorageUsageInfo(mActivity);
+ initializeViews();
+ initializeData(data);
+ mEyePosition = new EyePosition(mActivity.getAndroidContext(), this);
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_REFRESH_STORAGE_INFO:
+ refreshCacheStorageInfo();
+ break;
+ case MSG_REQUEST_LAYOUT: {
+ mLayoutReady = true;
+ removeMessages(MSG_REQUEST_LAYOUT);
+ mRootPane.requestLayout();
+ break;
+ }
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration config) {
+ // We use different layout resources for different configs
+ initializeFooterViews();
+ FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer);
+ if (layout.getVisibility() == View.VISIBLE) {
+ layout.removeAllViews();
+ layout.addView(mFooterContent);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAlbumSetDataAdapter.pause();
+ mSelectionDrawer.pause();
+ mEyePosition.pause();
+
+ if (mUpdateStorageInfo != null) {
+ mUpdateStorageInfo.cancel();
+ mUpdateStorageInfo = null;
+ }
+ mHandler.removeMessages(MSG_REFRESH_STORAGE_INFO);
+
+ FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer);
+ layout.removeAllViews();
+ layout.setVisibility(View.INVISIBLE);
+ }
+
+ private Job<Void> mUpdateStorageInfoJob = new Job<Void>() {
+ @Override
+ public Void run(JobContext jc) {
+ mCacheStorageInfo.loadStorageInfo(jc);
+ if (!jc.isCancelled()) {
+ mHandler.sendEmptyMessage(MSG_REFRESH_STORAGE_INFO);
+ }
+ return null;
+ }
+ };
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ setContentPane(mRootPane);
+ mAlbumSetDataAdapter.resume();
+ mSelectionDrawer.resume();
+ mEyePosition.resume();
+ mUpdateStorageInfo = mActivity.getThreadPool().submit(mUpdateStorageInfoJob);
+ FrameLayout layout = (FrameLayout) ((Activity) mActivity).findViewById(R.id.footer);
+ layout.addView(mFooterContent);
+ layout.setVisibility(View.VISIBLE);
+ }
+
+ private void initializeData(Bundle data) {
+ 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 AlbumSetDataLoader(
+ mActivity, mMediaSet, DATA_CACHE_SIZE);
+ mSelectionDrawer.setModel(mAlbumSetDataAdapter);
+ }
+
+ private void initializeViews() {
+ Activity activity = mActivity;
+
+ mSelectionManager = new SelectionManager(mActivity, true);
+ mSelectionManager.setSelectionListener(this);
+
+ Config.ManageCachePage config = Config.ManageCachePage.get(activity);
+ mSlotView = new SlotView(mActivity, config.slotViewSpec);
+ mSelectionDrawer = new ManageCacheDrawer(mActivity, mSelectionManager, mSlotView,
+ config.labelSpec, config.cachePinSize, config.cachePinMargin);
+ mSlotView.setSlotRenderer(mSelectionDrawer);
+ mSlotView.setListener(new SlotView.SimpleListener() {
+ @Override
+ public void onDown(int index) {
+ ManageCachePage.this.onDown(index);
+ }
+
+ @Override
+ public void onUp(boolean followedByLongPress) {
+ ManageCachePage.this.onUp();
+ }
+
+ @Override
+ public void onSingleTapUp(int slotIndex) {
+ ManageCachePage.this.onSingleTapUp(slotIndex);
+ }
+ });
+ mRootPane.addComponent(mSlotView);
+ initializeFooterViews();
+ }
+
+ private void initializeFooterViews() {
+ Activity activity = mActivity;
+
+ LayoutInflater inflater = activity.getLayoutInflater();
+ mFooterContent = inflater.inflate(R.layout.manage_offline_bar, null);
+
+ mFooterContent.findViewById(R.id.done).setOnClickListener(this);
+ refreshCacheStorageInfo();
+ }
+
+ @Override
+ public void onClick(View view) {
+ Utils.assertTrue(view.getId() == R.id.done);
+ GLRoot root = mActivity.getGLRoot();
+ root.lockRenderThread();
+ try {
+ ArrayList<Path> 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);
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ private void showToast() {
+ if (mAlbumCountToMakeAvailableOffline > 0) {
+ 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 = mActivity;
+ Toast.makeText(activity, activity.getResources().getString(
+ R.string.try_to_set_local_album_available_offline),
+ Toast.LENGTH_SHORT).show();
+ }
+
+ private void refreshCacheStorageInfo() {
+ ProgressBar progressBar = (ProgressBar) mFooterContent.findViewById(R.id.progress);
+ TextView status = (TextView) mFooterContent.findViewById(R.id.status);
+ progressBar.setMax(PROGRESS_BAR_MAX);
+ long totalBytes = mCacheStorageInfo.getTotalBytes();
+ long usedBytes = mCacheStorageInfo.getUsedBytes();
+ long expectedBytes = mCacheStorageInfo.getExpectedUsedBytes();
+ long freeBytes = mCacheStorageInfo.getFreeBytes();
+
+ Activity activity = mActivity;
+ if (totalBytes == 0) {
+ progressBar.setProgress(0);
+ progressBar.setSecondaryProgress(0);
+
+ // TODO: get the string translated
+ String label = activity.getString(R.string.free_space_format, "-");
+ status.setText(label);
+ } else {
+ progressBar.setProgress((int) (usedBytes * PROGRESS_BAR_MAX / totalBytes));
+ progressBar.setSecondaryProgress(
+ (int) (expectedBytes * PROGRESS_BAR_MAX / totalBytes));
+ String label = activity.getString(R.string.free_space_format,
+ Formatter.formatFileSize(activity, freeBytes));
+ status.setText(label);
+ }
+ }
+
+ @Override
+ public void onProgressComplete(int result) {
+ onBackPressed();
+ }
+
+ @Override
+ public void onProgressUpdate(int index) {
+ }
+
+ @Override
+ public void onSelectionModeChange(int mode) {
+ }
+
+ @Override
+ public void onSelectionChange(Path path, boolean selected) {
+ }
+
+ @Override
+ public void onConfirmDialogDismissed(boolean confirmed) {
+ }
+
+ @Override
+ public void onConfirmDialogShown() {
+ }
+
+ @Override
+ public void onProgressStart() {
+ }
+}
diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java
new file mode 100644
index 000000000..40edbbe4d
--- /dev/null
+++ b/src/com/android/gallery3d/app/MovieActivity.java
@@ -0,0 +1,263 @@
+/*
+ * 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.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.ShareActionProvider;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+
+/**
+ * This activity plays a video from a specified URI.
+ *
+ * The client of this activity can pass a logo bitmap in the intent (KEY_LOGO_BITMAP)
+ * to set the action bar logo so the playback process looks more seamlessly integrated with
+ * the original activity.
+ */
+public class MovieActivity extends Activity {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MovieActivity";
+ public static final String KEY_LOGO_BITMAP = "logo-bitmap";
+ public static final String KEY_TREAT_UP_AS_BACK = "treat-up-as-back";
+
+ private MoviePlayer mPlayer;
+ private boolean mFinishOnCompletion;
+ private Uri mUri;
+ private boolean mTreatUpAsBack;
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void setSystemUiVisibility(View rootView) {
+ if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) {
+ rootView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
+ }
+ }
+
+ @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.movie_view_root);
+
+ setSystemUiVisibility(rootView);
+
+ Intent intent = getIntent();
+ initializeActionBar(intent);
+ mFinishOnCompletion = intent.getBooleanExtra(
+ MediaStore.EXTRA_FINISH_ON_COMPLETION, true);
+ mTreatUpAsBack = intent.getBooleanExtra(KEY_TREAT_UP_AS_BACK, false);
+ mPlayer = new MoviePlayer(rootView, this, intent.getData(), savedInstanceState,
+ !mFinishOnCompletion) {
+ @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);
+ }
+ }
+ Window win = getWindow();
+ WindowManager.LayoutParams winParams = win.getAttributes();
+ winParams.buttonBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF;
+ winParams.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
+ win.setAttributes(winParams);
+
+ // We set the background in the theme to have the launching animation.
+ // But for the performance (and battery), we remove the background here.
+ win.setBackgroundDrawable(null);
+ }
+
+ private void setActionBarLogoFromIntent(Intent intent) {
+ Bitmap logo = intent.getParcelableExtra(KEY_LOGO_BITMAP);
+ if (logo != null) {
+ getActionBar().setLogo(
+ new BitmapDrawable(getResources(), logo));
+ }
+ }
+
+ private void initializeActionBar(Intent intent) {
+ mUri = intent.getData();
+ final ActionBar actionBar = getActionBar();
+ if (actionBar == null) {
+ return;
+ }
+ setActionBarLogoFromIntent(intent);
+ actionBar.setDisplayOptions(
+ ActionBar.DISPLAY_HOME_AS_UP,
+ ActionBar.DISPLAY_HOME_AS_UP);
+
+ String title = intent.getStringExtra(Intent.EXTRA_TITLE);
+ if (title != null) {
+ actionBar.setTitle(title);
+ } else {
+ // Displays the filename as title, reading the filename from the
+ // interface: {@link android.provider.OpenableColumns#DISPLAY_NAME}.
+ AsyncQueryHandler queryHandler =
+ new AsyncQueryHandler(getContentResolver()) {
+ @Override
+ protected void onQueryComplete(int token, Object cookie,
+ Cursor cursor) {
+ try {
+ if ((cursor != null) && cursor.moveToFirst()) {
+ String displayName = cursor.getString(0);
+
+ // Just show empty title if other apps don't set
+ // DISPLAY_NAME
+ actionBar.setTitle((displayName == null) ? "" :
+ displayName);
+ }
+ } finally {
+ Utils.closeSilently(cursor);
+ }
+ }
+ };
+ queryHandler.startQuery(0, null, mUri,
+ new String[] {OpenableColumns.DISPLAY_NAME}, null, null,
+ null);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.movie, menu);
+
+ // Document says EXTRA_STREAM should be a content: Uri
+ // So, we only share the video if it's "content:".
+ MenuItem shareItem = menu.findItem(R.id.action_share);
+ if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme())) {
+ shareItem.setVisible(true);
+ ((ShareActionProvider) shareItem.getActionProvider())
+ .setShareIntent(createShareIntent());
+ } else {
+ shareItem.setVisible(false);
+ }
+ return true;
+ }
+
+ private Intent createShareIntent() {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("video/*");
+ intent.putExtra(Intent.EXTRA_STREAM, mUri);
+ return intent;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ if (mTreatUpAsBack) {
+ finish();
+ } else {
+ startActivity(new Intent(this, Gallery.class));
+ finish();
+ }
+ return true;
+ } else if (id == R.id.action_share) {
+ startActivity(Intent.createChooser(createShareIntent(),
+ getString(R.string.share)));
+ return true;
+ }
+ return false;
+ }
+
+ @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 onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mPlayer.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onDestroy() {
+ mPlayer.onDestroy();
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return mPlayer.onKeyDown(keyCode, event)
+ || super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return mPlayer.onKeyUp(keyCode, event)
+ || super.onKeyUp(keyCode, event);
+ }
+}
diff --git a/src/com/android/gallery3d/app/MovieControllerOverlay.java b/src/com/android/gallery3d/app/MovieControllerOverlay.java
new file mode 100644
index 000000000..f01e619c6
--- /dev/null
+++ b/src/com/android/gallery3d/app/MovieControllerOverlay.java
@@ -0,0 +1,185 @@
+/*
+ * 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.content.Context;
+import android.os.Handler;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.AnimationUtils;
+import com.android.gallery3d.R;
+
+/**
+ * The playback controller for the Movie Player.
+ */
+public class MovieControllerOverlay extends CommonControllerOverlay implements
+ AnimationListener {
+
+ private boolean hidden;
+
+ private final Handler handler;
+ private final Runnable startHidingRunnable;
+ private final Animation hideAnimation;
+
+ public MovieControllerOverlay(Context context) {
+ super(context);
+
+ handler = new Handler();
+ startHidingRunnable = new Runnable() {
+ @Override
+ public void run() {
+ startHiding();
+ }
+ };
+
+ hideAnimation = AnimationUtils.loadAnimation(context, R.anim.player_out);
+ hideAnimation.setAnimationListener(this);
+
+ hide();
+ }
+
+ @Override
+ protected void createTimeBar(Context context) {
+ mTimeBar = new TimeBar(context, this);
+ }
+
+ @Override
+ public void hide() {
+ boolean wasHidden = hidden;
+ hidden = true;
+ super.hide();
+ if (mListener != null && wasHidden != hidden) {
+ mListener.onHidden();
+ }
+ }
+
+
+ @Override
+ public void show() {
+ boolean wasHidden = hidden;
+ hidden = false;
+ super.show();
+ if (mListener != null && wasHidden != hidden) {
+ mListener.onShown();
+ }
+ maybeStartHiding();
+ }
+
+ private void maybeStartHiding() {
+ cancelHiding();
+ if (mState == State.PLAYING) {
+ handler.postDelayed(startHidingRunnable, 2500);
+ }
+ }
+
+ private void startHiding() {
+ startHideAnimation(mBackground);
+ startHideAnimation(mTimeBar);
+ startHideAnimation(mPlayPauseReplayView);
+ }
+
+ private void startHideAnimation(View view) {
+ if (view.getVisibility() == View.VISIBLE) {
+ view.startAnimation(hideAnimation);
+ }
+ }
+
+ private void cancelHiding() {
+ handler.removeCallbacks(startHidingRunnable);
+ mBackground.setAnimation(null);
+ mTimeBar.setAnimation(null);
+ mPlayPauseReplayView.setAnimation(null);
+ }
+
+ @Override
+ public void onAnimationStart(Animation animation) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ hide();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (hidden) {
+ show();
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (super.onTouchEvent(event)) {
+ return true;
+ }
+
+ if (hidden) {
+ show();
+ return true;
+ }
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ cancelHiding();
+ if (mState == State.PLAYING || mState == State.PAUSED) {
+ mListener.onPlayPause();
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ maybeStartHiding();
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected void updateViews() {
+ if (hidden) {
+ return;
+ }
+ super.updateViews();
+ }
+
+ // TimeBar listener
+
+ @Override
+ public void onScrubbingStart() {
+ cancelHiding();
+ super.onScrubbingStart();
+ }
+
+ @Override
+ public void onScrubbingMove(int time) {
+ cancelHiding();
+ super.onScrubbingMove(time);
+ }
+
+ @Override
+ public void onScrubbingEnd(int time, int trimStartTime, int trimEndTime) {
+ maybeStartHiding();
+ super.onScrubbingEnd(time, trimStartTime, trimEndTime);
+ }
+}
diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java
new file mode 100644
index 000000000..ce9183483
--- /dev/null
+++ b/src/com/android/gallery3d/app/MoviePlayer.java
@@ -0,0 +1,525 @@
+/*
+ * 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 android.annotation.TargetApi;
+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.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.VideoView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.BlobCache;
+import com.android.gallery3d.util.CacheManager;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+
+public class MoviePlayer implements
+ MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener,
+ ControllerOverlay.Listener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MoviePlayer";
+
+ private static final String KEY_VIDEO_POSITION = "video-position";
+ private static final String KEY_RESUMEABLE_TIME = "resumeable-timeout";
+
+ // These are constants in KeyEvent, appearing on API level 11.
+ private static final int KEYCODE_MEDIA_PLAY = 126;
+ private static final int KEYCODE_MEDIA_PAUSE = 127;
+
+ // 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 static final long BLACK_TIMEOUT = 500;
+
+ // If we resume the acitivty with in RESUMEABLE_TIMEOUT, we will keep playing.
+ // Otherwise, we pause the player.
+ private static final long RESUMEABLE_TIMEOUT = 3 * 60 * 1000; // 3 mins
+
+ private Context mContext;
+ private final VideoView mVideoView;
+ private final View mRootView;
+ private final Bookmarker mBookmarker;
+ private final Uri mUri;
+ private final Handler mHandler = new Handler();
+ private final AudioBecomingNoisyReceiver mAudioBecomingNoisyReceiver;
+ private final MovieControllerOverlay mController;
+
+ private long mResumeableTime = Long.MAX_VALUE;
+ private int mVideoPosition = 0;
+ private boolean mHasPaused = false;
+ private int mLastSystemUiVis = 0;
+
+ // If the time bar is being dragged.
+ private boolean mDragging;
+
+ // If the time bar is visible.
+ private boolean mShowing;
+
+ private final Runnable mPlayingChecker = new Runnable() {
+ @Override
+ public void run() {
+ if (mVideoView.isPlaying()) {
+ mController.showPlaying();
+ } else {
+ mHandler.postDelayed(mPlayingChecker, 250);
+ }
+ }
+ };
+
+ private final Runnable mProgressChecker = new Runnable() {
+ @Override
+ public void run() {
+ int pos = setProgress();
+ mHandler.postDelayed(mProgressChecker, 1000 - (pos % 1000));
+ }
+ };
+
+ public MoviePlayer(View rootView, final MovieActivity movieActivity,
+ Uri videoUri, Bundle savedInstance, boolean canReplay) {
+ mContext = movieActivity.getApplicationContext();
+ mRootView = rootView;
+ mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
+ mBookmarker = new Bookmarker(movieActivity);
+ mUri = videoUri;
+
+ mController = new MovieControllerOverlay(mContext);
+ ((ViewGroup)rootView).addView(mController.getView());
+ mController.setListener(this);
+ mController.setCanReplay(canReplay);
+
+ mVideoView.setOnErrorListener(this);
+ mVideoView.setOnCompletionListener(this);
+ mVideoView.setVideoURI(mUri);
+ mVideoView.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ mController.show();
+ return true;
+ }
+ });
+ mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(MediaPlayer player) {
+ if (!mVideoView.canSeekForward() || !mVideoView.canSeekBackward()) {
+ mController.setSeekable(false);
+ } else {
+ mController.setSeekable(true);
+ }
+ setProgress();
+ }
+ });
+
+ // The SurfaceView is transparent before drawing the first frame.
+ // This makes the UI flashing when open a video. (black -> old screen
+ // -> video) However, we have no way to know the timing of the first
+ // frame. So, we hide the VideoView for a while to make sure the
+ // video has been drawn on it.
+ mVideoView.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mVideoView.setVisibility(View.VISIBLE);
+ }
+ }, BLACK_TIMEOUT);
+
+ setOnSystemUiVisibilityChangeListener();
+ // Hide system UI by default
+ showSystemUi(false);
+
+ mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver();
+ mAudioBecomingNoisyReceiver.register();
+
+ Intent i = new Intent(SERVICECMD);
+ i.putExtra(CMDNAME, CMDPAUSE);
+ movieActivity.sendBroadcast(i);
+
+ if (savedInstance != null) { // this is a resumed activity
+ mVideoPosition = savedInstance.getInt(KEY_VIDEO_POSITION, 0);
+ mResumeableTime = savedInstance.getLong(KEY_RESUMEABLE_TIME, Long.MAX_VALUE);
+ mVideoView.start();
+ mVideoView.suspend();
+ mHasPaused = true;
+ } else {
+ final Integer bookmark = mBookmarker.getBookmark(mUri);
+ if (bookmark != null) {
+ showResumeDialog(movieActivity, bookmark);
+ } else {
+ startVideo();
+ }
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void setOnSystemUiVisibilityChangeListener() {
+ if (!ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_HIDE_NAVIGATION) return;
+
+ // When the user touches the screen or uses some hard key, the framework
+ // will change system ui visibility from invisible to visible. We show
+ // the media control and enable system UI (e.g. ActionBar) to be visible at this point
+ mVideoView.setOnSystemUiVisibilityChangeListener(
+ new View.OnSystemUiVisibilityChangeListener() {
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ int diff = mLastSystemUiVis ^ visibility;
+ mLastSystemUiVis = visibility;
+ if ((diff & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
+ && (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
+ mController.show();
+ }
+ }
+ });
+ }
+
+ @SuppressWarnings("deprecation")
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void showSystemUi(boolean visible) {
+ if (!ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) return;
+
+ int flag = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
+ if (!visible) {
+ // We used the deprecated "STATUS_BAR_HIDDEN" for unbundling
+ flag |= View.STATUS_BAR_HIDDEN | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+ }
+ mVideoView.setSystemUiVisibility(flag);
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
+ outState.putLong(KEY_RESUMEABLE_TIME, mResumeableTime);
+ }
+
+ 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() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ onCompletion();
+ }
+ });
+ builder.setPositiveButton(
+ R.string.resume_playing_resume, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mVideoView.seekTo(bookmark);
+ startVideo();
+ }
+ });
+ builder.setNegativeButton(
+ R.string.resume_playing_restart, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ startVideo();
+ }
+ });
+ builder.show();
+ }
+
+ public void onPause() {
+ mHasPaused = true;
+ mHandler.removeCallbacksAndMessages(null);
+ mVideoPosition = mVideoView.getCurrentPosition();
+ mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration());
+ mVideoView.suspend();
+ mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT;
+ }
+
+ public void onResume() {
+ if (mHasPaused) {
+ mVideoView.seekTo(mVideoPosition);
+ mVideoView.resume();
+
+ // If we have slept for too long, pause the play
+ if (System.currentTimeMillis() > mResumeableTime) {
+ pauseVideo();
+ }
+ }
+ mHandler.post(mProgressChecker);
+ }
+
+ public void onDestroy() {
+ mVideoView.stopPlayback();
+ mAudioBecomingNoisyReceiver.unregister();
+ }
+
+ // This updates the time bar display (if necessary). It is called every
+ // second by mProgressChecker and also from places where the time bar needs
+ // to be updated immediately.
+ private int setProgress() {
+ if (mDragging || !mShowing) {
+ return 0;
+ }
+ int position = mVideoView.getCurrentPosition();
+ int duration = mVideoView.getDuration();
+ mController.setTimes(position, duration, 0, 0);
+ return position;
+ }
+
+ private void startVideo() {
+ // 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)) {
+ mController.showLoading();
+ mHandler.removeCallbacks(mPlayingChecker);
+ mHandler.postDelayed(mPlayingChecker, 250);
+ } else {
+ mController.showPlaying();
+ mController.hide();
+ }
+
+ mVideoView.start();
+ setProgress();
+ }
+
+ private void playVideo() {
+ mVideoView.start();
+ mController.showPlaying();
+ setProgress();
+ }
+
+ private void pauseVideo() {
+ mVideoView.pause();
+ mController.showPaused();
+ }
+
+ // Below are notifications from VideoView
+ @Override
+ public boolean onError(MediaPlayer player, int arg1, int arg2) {
+ mHandler.removeCallbacksAndMessages(null);
+ // VideoView will show an error dialog if we return false, so no need
+ // to show more message.
+ mController.showErrorMessage("");
+ return false;
+ }
+
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ mController.showEnded();
+ onCompletion();
+ }
+
+ public void onCompletion() {
+ }
+
+ // Below are notifications from ControllerOverlay
+ @Override
+ public void onPlayPause() {
+ if (mVideoView.isPlaying()) {
+ pauseVideo();
+ } else {
+ playVideo();
+ }
+ }
+
+ @Override
+ public void onSeekStart() {
+ mDragging = true;
+ }
+
+ @Override
+ public void onSeekMove(int time) {
+ mVideoView.seekTo(time);
+ }
+
+ @Override
+ public void onSeekEnd(int time, int start, int end) {
+ mDragging = false;
+ mVideoView.seekTo(time);
+ setProgress();
+ }
+
+ @Override
+ public void onShown() {
+ mShowing = true;
+ setProgress();
+ showSystemUi(true);
+ }
+
+ @Override
+ public void onHidden() {
+ mShowing = false;
+ showSystemUi(false);
+ }
+
+ @Override
+ public void onReplay() {
+ startVideo();
+ }
+
+ // Below are key events passed from MovieActivity.
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+
+ // Some headsets will fire off 7-10 events on a single click
+ if (event.getRepeatCount() > 0) {
+ return isMediaKey(keyCode);
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_HEADSETHOOK:
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ if (mVideoView.isPlaying()) {
+ pauseVideo();
+ } else {
+ playVideo();
+ }
+ return true;
+ case KEYCODE_MEDIA_PAUSE:
+ if (mVideoView.isPlaying()) {
+ pauseVideo();
+ }
+ return true;
+ case KEYCODE_MEDIA_PLAY:
+ if (!mVideoView.isPlaying()) {
+ playVideo();
+ }
+ return true;
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ // TODO: Handle next / previous accordingly, for now we're
+ // just consuming the events.
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ return isMediaKey(keyCode);
+ }
+
+ private static boolean isMediaKey(int keyCode) {
+ return keyCode == KeyEvent.KEYCODE_HEADSETHOOK
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS
+ || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE;
+ }
+
+ // We want to pause when the headset is unplugged.
+ 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()) pauseVideo();
+ }
+ }
+}
+
+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 = DataInputStream.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/MuteVideo.java b/src/com/android/gallery3d/app/MuteVideo.java
new file mode 100644
index 000000000..d3f3aa594
--- /dev/null
+++ b/src/com/android/gallery3d/app/MuteVideo.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2012 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.app.ProgressDialog;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.SaveVideoFileInfo;
+import com.android.gallery3d.util.SaveVideoFileUtils;
+
+import java.io.IOException;
+
+public class MuteVideo {
+
+ private ProgressDialog mMuteProgress;
+
+ private String mFilePath = null;
+ private Uri mUri = null;
+ private SaveVideoFileInfo mDstFileInfo = null;
+ private Activity mActivity = null;
+ private final Handler mHandler = new Handler();
+
+ final String TIME_STAMP_NAME = "'MUTE'_yyyyMMdd_HHmmss";
+
+ public MuteVideo(String filePath, Uri uri, Activity activity) {
+ mUri = uri;
+ mFilePath = filePath;
+ mActivity = activity;
+ }
+
+ public void muteInBackground() {
+ mDstFileInfo = SaveVideoFileUtils.getDstMp4FileInfo(TIME_STAMP_NAME,
+ mActivity.getContentResolver(), mUri,
+ mActivity.getString(R.string.folder_download));
+
+ showProgressDialog();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ VideoUtils.startMute(mFilePath, mDstFileInfo);
+ SaveVideoFileUtils.insertContent(
+ mDstFileInfo, mActivity.getContentResolver(), mUri);
+ } catch (IOException e) {
+ Toast.makeText(mActivity, mActivity.getString(R.string.video_mute_err),
+ Toast.LENGTH_SHORT).show();
+ }
+ // After muting is done, trigger the UI changed.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(mActivity.getApplicationContext(),
+ mActivity.getString(R.string.save_into,
+ mDstFileInfo.mFolderName),
+ Toast.LENGTH_SHORT)
+ .show();
+
+ if (mMuteProgress != null) {
+ mMuteProgress.dismiss();
+ mMuteProgress = null;
+
+ // Show the result only when the activity not
+ // stopped.
+ Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.fromFile(mDstFileInfo.mFile), "video/*");
+ intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false);
+ mActivity.startActivity(intent);
+ }
+ }
+ });
+ }
+ }).start();
+ }
+
+ private void showProgressDialog() {
+ mMuteProgress = new ProgressDialog(mActivity);
+ mMuteProgress.setTitle(mActivity.getString(R.string.muting));
+ mMuteProgress.setMessage(mActivity.getString(R.string.please_wait));
+ mMuteProgress.setCancelable(false);
+ mMuteProgress.setCanceledOnTouchOutside(false);
+ mMuteProgress.show();
+ }
+}
diff --git a/src/com/android/gallery3d/app/NotificationIds.java b/src/com/android/gallery3d/app/NotificationIds.java
new file mode 100644
index 000000000..d697d854b
--- /dev/null
+++ b/src/com/android/gallery3d/app/NotificationIds.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2013 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 NotificationIds {
+ public static final int INGEST_NOTIFICATION_SCANNING = 10;
+ public static final int INGEST_NOTIFICATION_IMPORTING = 11;
+}
diff --git a/src/com/android/gallery3d/app/OrientationManager.java b/src/com/android/gallery3d/app/OrientationManager.java
new file mode 100644
index 000000000..f2f632c9f
--- /dev/null
+++ b/src/com/android/gallery3d/app/OrientationManager.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2012 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.ContentResolver;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.provider.Settings;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.ui.OrientationSource;
+
+public class OrientationManager implements OrientationSource {
+ private static final String TAG = "OrientationManager";
+
+ // Orientation hysteresis amount used in rounding, in degrees
+ private static final int ORIENTATION_HYSTERESIS = 5;
+
+ private Activity mActivity;
+ private MyOrientationEventListener mOrientationListener;
+ // If the framework orientation is locked.
+ private boolean mOrientationLocked = false;
+
+ // This is true if "Settings -> Display -> Rotation Lock" is checked. We
+ // don't allow the orientation to be unlocked if the value is true.
+ private boolean mRotationLockedSetting = false;
+
+ public OrientationManager(Activity activity) {
+ mActivity = activity;
+ mOrientationListener = new MyOrientationEventListener(activity);
+ }
+
+ public void resume() {
+ ContentResolver resolver = mActivity.getContentResolver();
+ mRotationLockedSetting = Settings.System.getInt(
+ resolver, Settings.System.ACCELEROMETER_ROTATION, 0) != 1;
+ mOrientationListener.enable();
+ }
+
+ public void pause() {
+ mOrientationListener.disable();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Orientation handling
+ //
+ // We can choose to lock the framework orientation or not. If we lock the
+ // framework orientation, we calculate a a compensation value according to
+ // current device orientation and send it to listeners. If we don't lock
+ // the framework orientation, we always set the compensation value to 0.
+ ////////////////////////////////////////////////////////////////////////////
+
+ // Lock the framework orientation to the current device orientation
+ public void lockOrientation() {
+ if (mOrientationLocked) return;
+ mOrientationLocked = true;
+ if (ApiHelper.HAS_ORIENTATION_LOCK) {
+ mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
+ } else {
+ mActivity.setRequestedOrientation(calculateCurrentScreenOrientation());
+ }
+ }
+
+ // Unlock the framework orientation, so it can change when the device
+ // rotates.
+ public void unlockOrientation() {
+ if (!mOrientationLocked) return;
+ mOrientationLocked = false;
+ Log.d(TAG, "unlock orientation");
+ mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
+ }
+
+ private int calculateCurrentScreenOrientation() {
+ int displayRotation = getDisplayRotation();
+ // Display rotation >= 180 means we need to use the REVERSE landscape/portrait
+ boolean standard = displayRotation < 180;
+ if (mActivity.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE) {
+ return standard
+ ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ : ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+ } else {
+ if (displayRotation == 90 || displayRotation == 270) {
+ // If displayRotation = 90 or 270 then we are on a landscape
+ // device. On landscape devices, portrait is a 90 degree
+ // clockwise rotation from landscape, so we need
+ // to flip which portrait we pick as display rotation is counter clockwise
+ standard = !standard;
+ }
+ return standard
+ ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ : ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+ }
+ }
+
+ // This listens to the device orientation, so we can update the compensation.
+ private class MyOrientationEventListener extends OrientationEventListener {
+ public MyOrientationEventListener(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onOrientationChanged(int orientation) {
+ // We keep the last known orientation. So if the user first orient
+ // the camera then point the camera to floor or sky, we still have
+ // the correct orientation.
+ if (orientation == ORIENTATION_UNKNOWN) return;
+ orientation = roundOrientation(orientation, 0);
+ }
+ }
+
+ @Override
+ public int getDisplayRotation() {
+ return getDisplayRotation(mActivity);
+ }
+
+ @Override
+ public int getCompensation() {
+ return 0;
+ }
+
+ private static int roundOrientation(int orientation, int orientationHistory) {
+ boolean changeOrientation = false;
+ if (orientationHistory == OrientationEventListener.ORIENTATION_UNKNOWN) {
+ changeOrientation = true;
+ } else {
+ int dist = Math.abs(orientation - orientationHistory);
+ dist = Math.min(dist, 360 - dist);
+ changeOrientation = (dist >= 45 + ORIENTATION_HYSTERESIS);
+ }
+ if (changeOrientation) {
+ return ((orientation + 45) / 90 * 90) % 360;
+ }
+ return orientationHistory;
+ }
+
+ private static int getDisplayRotation(Activity activity) {
+ int rotation = activity.getWindowManager().getDefaultDisplay()
+ .getRotation();
+ switch (rotation) {
+ case Surface.ROTATION_0: return 0;
+ case Surface.ROTATION_90: return 90;
+ case Surface.ROTATION_180: return 180;
+ case Surface.ROTATION_270: return 270;
+ }
+ return 0;
+ }
+}
diff --git a/src/com/android/gallery3d/app/PackagesMonitor.java b/src/com/android/gallery3d/app/PackagesMonitor.java
new file mode 100644
index 000000000..9b2412f1b
--- /dev/null
+++ b/src/com/android/gallery3d/app/PackagesMonitor.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.app;
+
+import android.app.IntentService;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.util.LightCycleHelper;
+
+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(final Context context, final Intent intent) {
+ intent.setClass(context, AsyncService.class);
+ context.startService(intent);
+ }
+
+ public static class AsyncService extends IntentService {
+ public AsyncService() {
+ super("GalleryPackagesMonitorAsync");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ onReceiveAsync(this, intent);
+ }
+ }
+
+ // Runs in a background thread.
+ private static void onReceiveAsync(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);
+ } else if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+ PicasaSource.onPackageChanged(context, packageName);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/PanoramaMetadataSupport.java b/src/com/android/gallery3d/app/PanoramaMetadataSupport.java
new file mode 100644
index 000000000..ba0c9e71a
--- /dev/null
+++ b/src/com/android/gallery3d/app/PanoramaMetadataSupport.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2012 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.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.PanoramaMetadataJob;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
+
+import java.util.ArrayList;
+
+/**
+ * This class breaks out the off-thread panorama support checks so that the
+ * complexity can be shared between UriImage and LocalImage, which need to
+ * support panoramas.
+ */
+public class PanoramaMetadataSupport implements FutureListener<PanoramaMetadata> {
+ private Object mLock = new Object();
+ private Future<PanoramaMetadata> mGetPanoMetadataTask;
+ private PanoramaMetadata mPanoramaMetadata;
+ private ArrayList<PanoramaSupportCallback> mCallbacksWaiting;
+ private MediaObject mMediaObject;
+
+ public PanoramaMetadataSupport(MediaObject mediaObject) {
+ mMediaObject = mediaObject;
+ }
+
+ public void getPanoramaSupport(GalleryApp app, PanoramaSupportCallback callback) {
+ synchronized (mLock) {
+ if (mPanoramaMetadata != null) {
+ callback.panoramaInfoAvailable(mMediaObject, mPanoramaMetadata.mUsePanoramaViewer,
+ mPanoramaMetadata.mIsPanorama360);
+ } else {
+ if (mCallbacksWaiting == null) {
+ mCallbacksWaiting = new ArrayList<PanoramaSupportCallback>();
+ mGetPanoMetadataTask = app.getThreadPool().submit(
+ new PanoramaMetadataJob(app.getAndroidContext(),
+ mMediaObject.getContentUri()), this);
+
+ }
+ mCallbacksWaiting.add(callback);
+ }
+ }
+ }
+
+ public void clearCachedValues() {
+ synchronized (mLock) {
+ if (mPanoramaMetadata != null) {
+ mPanoramaMetadata = null;
+ } else if (mGetPanoMetadataTask != null) {
+ mGetPanoMetadataTask.cancel();
+ for (PanoramaSupportCallback cb : mCallbacksWaiting) {
+ cb.panoramaInfoAvailable(mMediaObject, false, false);
+ }
+ mGetPanoMetadataTask = null;
+ mCallbacksWaiting = null;
+ }
+ }
+ }
+
+ @Override
+ public void onFutureDone(Future<PanoramaMetadata> future) {
+ synchronized (mLock) {
+ mPanoramaMetadata = future.get();
+ if (mPanoramaMetadata == null) {
+ // Error getting panorama data from file. Treat as not panorama.
+ mPanoramaMetadata = LightCycleHelper.NOT_PANORAMA;
+ }
+ for (PanoramaSupportCallback cb : mCallbacksWaiting) {
+ cb.panoramaInfoAvailable(mMediaObject, mPanoramaMetadata.mUsePanoramaViewer,
+ mPanoramaMetadata.mIsPanorama360);
+ }
+ mGetPanoMetadataTask = null;
+ mCallbacksWaiting = null;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/PhotoDataAdapter.java b/src/com/android/gallery3d/app/PhotoDataAdapter.java
new file mode 100644
index 000000000..fd3a7cf73
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoDataAdapter.java
@@ -0,0 +1,1133 @@
+/*
+ * 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 android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.LocalMediaItem;
+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.glrenderer.TiledTexture;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.ui.TileImageViewAdapter;
+import com.android.gallery3d.ui.TiledScreenNail;
+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 com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+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 MSG_UPDATE_IMAGE_REQUESTS = 4;
+
+ private static final int MIN_LOAD_COUNT = 16;
+ private static final int DATA_CACHE_SIZE = 256;
+ private static final int SCREEN_NAIL_MAX = PhotoView.SCREEN_NAIL_MAX;
+ private static final int IMAGE_CACHE_SIZE = 2 * SCREEN_NAIL_MAX + 1;
+
+ private static final int BIT_SCREEN_NAIL = 1;
+ private static final int BIT_FULL_IMAGE = 2;
+
+ // 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 Path-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<Path, ImageEntry> mImageCache =
+ new HashMap<Path, ImageEntry>();
+ 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 images. If any
+ // of the version number changes, we notify the view. This is used after a
+ // database reload or mCurrentIndex changes.
+ private final long mChanges[] = new long[IMAGE_CACHE_SIZE];
+ // mPaths keeps the corresponding Path (of MediaItem) for the images. This
+ // is used to determine the item movement.
+ private final Path mPaths[] = new Path[IMAGE_CACHE_SIZE];
+
+ 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 int mCameraIndex;
+ private boolean mIsPanorama;
+ private boolean mIsStaticCamera;
+ private boolean mIsActive;
+ private boolean mNeedFullImage;
+ private int mFocusHintDirection = FOCUS_HINT_NEXT;
+ private Path mFocusHintPath = null;
+
+ public interface DataListener extends LoadingListener {
+ public void onPhotoChanged(int index, Path item);
+ }
+
+ private DataListener mDataListener;
+
+ private final SourceListener mSourceListener = new SourceListener();
+ private final TiledTexture.Uploader mUploader;
+
+ // 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. cameraIndex is the index of the camera
+ // preview. If cameraIndex < 0, there is no camera preview.
+ public PhotoDataAdapter(AbstractGalleryActivity activity, PhotoView view,
+ MediaSet mediaSet, Path itemPath, int indexHint, int cameraIndex,
+ boolean isPanorama, boolean isStaticCamera) {
+ mSource = Utils.checkNotNull(mediaSet);
+ mPhotoView = Utils.checkNotNull(view);
+ mItemPath = Utils.checkNotNull(itemPath);
+ mCurrentIndex = indexHint;
+ mCameraIndex = cameraIndex;
+ mIsPanorama = isPanorama;
+ mIsStaticCamera = isStaticCamera;
+ mThreadPool = activity.getThreadPool();
+ mNeedFullImage = true;
+
+ Arrays.fill(mChanges, MediaObject.INVALID_DATA_VERSION);
+
+ mUploader = new TiledTexture.Uploader(activity.getGLRoot());
+
+ 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(false);
+ }
+ return;
+ }
+ case MSG_UPDATE_IMAGE_REQUESTS: {
+ updateImageRequests();
+ return;
+ }
+ default: throw new AssertionError();
+ }
+ }
+ };
+
+ updateSlidingWindow();
+ }
+
+ private MediaItem getItemInternal(int index) {
+ if (index < 0 || index >= mSize) return null;
+ if (index >= mContentStart && index < mContentEnd) {
+ return mData[index % DATA_CACHE_SIZE];
+ }
+ return null;
+ }
+
+ private long getVersion(int index) {
+ MediaItem item = getItemInternal(index);
+ if (item == null) return MediaObject.INVALID_DATA_VERSION;
+ return item.getDataVersion();
+ }
+
+ private Path getPath(int index) {
+ MediaItem item = getItemInternal(index);
+ if (item == null) return null;
+ return item.getPath();
+ }
+
+ private void fireDataChange() {
+ // First check if data actually changed.
+ boolean changed = false;
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+ long newVersion = getVersion(mCurrentIndex + i);
+ if (mChanges[i + SCREEN_NAIL_MAX] != newVersion) {
+ mChanges[i + SCREEN_NAIL_MAX] = newVersion;
+ changed = true;
+ }
+ }
+
+ if (!changed) return;
+
+ // Now calculate the fromIndex array. fromIndex represents the item
+ // movement. It records the index where the picture come from. The
+ // special value Integer.MAX_VALUE means it's a new picture.
+ final int N = IMAGE_CACHE_SIZE;
+ int fromIndex[] = new int[N];
+
+ // Remember the old path array.
+ Path oldPaths[] = new Path[N];
+ System.arraycopy(mPaths, 0, oldPaths, 0, N);
+
+ // Update the mPaths array.
+ for (int i = 0; i < N; ++i) {
+ mPaths[i] = getPath(mCurrentIndex + i - SCREEN_NAIL_MAX);
+ }
+
+ // Calculate the fromIndex array.
+ for (int i = 0; i < N; i++) {
+ Path p = mPaths[i];
+ if (p == null) {
+ fromIndex[i] = Integer.MAX_VALUE;
+ continue;
+ }
+
+ // Try to find the same path in the old array
+ int j;
+ for (j = 0; j < N; j++) {
+ if (oldPaths[j] == p) {
+ break;
+ }
+ }
+ fromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE;
+ }
+
+ mPhotoView.notifyDataChange(fromIndex, -mCurrentIndex,
+ mSize - 1 - mCurrentIndex);
+ }
+
+ public void setDataListener(DataListener listener) {
+ mDataListener = listener;
+ }
+
+ private void updateScreenNail(Path path, Future<ScreenNail> future) {
+ ImageEntry entry = mImageCache.get(path);
+ ScreenNail screenNail = future.get();
+
+ if (entry == null || entry.screenNailTask != future) {
+ if (screenNail != null) screenNail.recycle();
+ return;
+ }
+
+ entry.screenNailTask = null;
+
+ // Combine the ScreenNails if we already have a BitmapScreenNail
+ if (entry.screenNail instanceof TiledScreenNail) {
+ TiledScreenNail original = (TiledScreenNail) entry.screenNail;
+ screenNail = original.combine(screenNail);
+ }
+
+ if (screenNail == null) {
+ entry.failToLoad = true;
+ } else {
+ entry.failToLoad = false;
+ entry.screenNail = screenNail;
+ }
+
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+ if (path == getPath(mCurrentIndex + i)) {
+ if (i == 0) updateTileProvider(entry);
+ mPhotoView.notifyImageChange(i);
+ break;
+ }
+ }
+ updateImageRequests();
+ updateScreenNailUploadQueue();
+ }
+
+ private void updateFullImage(Path path, Future<BitmapRegionDecoder> future) {
+ ImageEntry entry = mImageCache.get(path);
+ if (entry == null || entry.fullImageTask != future) {
+ BitmapRegionDecoder fullImage = future.get();
+ if (fullImage != null) fullImage.recycle();
+ return;
+ }
+
+ entry.fullImageTask = null;
+ entry.fullImage = future.get();
+ if (entry.fullImage != null) {
+ if (path == getPath(mCurrentIndex)) {
+ updateTileProvider(entry);
+ mPhotoView.notifyImageChange(0);
+ }
+ }
+ updateImageRequests();
+ }
+
+ @Override
+ public void resume() {
+ mIsActive = true;
+ TiledTexture.prepareResources();
+
+ mSource.addContentListener(mSourceListener);
+ updateImageCache();
+ updateImageRequests();
+
+ mReloadTask = new ReloadTask();
+ mReloadTask.start();
+
+ fireDataChange();
+ }
+
+ @Override
+ 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();
+ if (entry.screenNail != null) entry.screenNail.recycle();
+ }
+ mImageCache.clear();
+ mTileProvider.clear();
+
+ mUploader.clear();
+ TiledTexture.freeResources();
+ }
+
+ private MediaItem getItem(int index) {
+ if (index < 0 || index >= mSize || !mIsActive) return null;
+ Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+ if (index >= mContentStart && index < mContentEnd) {
+ return mData[index % DATA_CACHE_SIZE];
+ }
+ return null;
+ }
+
+ private void updateCurrentIndex(int index) {
+ if (mCurrentIndex == index) return;
+ mCurrentIndex = index;
+ updateSlidingWindow();
+
+ MediaItem item = mData[index % DATA_CACHE_SIZE];
+ mItemPath = item == null ? null : item.getPath();
+
+ updateImageCache();
+ updateImageRequests();
+ updateTileProvider();
+
+ if (mDataListener != null) {
+ mDataListener.onPhotoChanged(index, mItemPath);
+ }
+
+ fireDataChange();
+ }
+
+ private void uploadScreenNail(int offset) {
+ int index = mCurrentIndex + offset;
+ if (index < mActiveStart || index >= mActiveEnd) return;
+
+ MediaItem item = getItem(index);
+ if (item == null) return;
+
+ ImageEntry e = mImageCache.get(item.getPath());
+ if (e == null) return;
+
+ ScreenNail s = e.screenNail;
+ if (s instanceof TiledScreenNail) {
+ TiledTexture t = ((TiledScreenNail) s).getTexture();
+ if (t != null && !t.isReady()) mUploader.addTexture(t);
+ }
+ }
+
+ private void updateScreenNailUploadQueue() {
+ mUploader.clear();
+ uploadScreenNail(0);
+ for (int i = 1; i < IMAGE_CACHE_SIZE; ++i) {
+ uploadScreenNail(i);
+ uploadScreenNail(-i);
+ }
+ }
+
+ @Override
+ public void moveTo(int index) {
+ updateCurrentIndex(index);
+ }
+
+ @Override
+ public ScreenNail getScreenNail(int offset) {
+ int index = mCurrentIndex + offset;
+ if (index < 0 || index >= mSize || !mIsActive) return null;
+ Utils.assertTrue(index >= mActiveStart && index < mActiveEnd);
+
+ MediaItem item = getItem(index);
+ if (item == null) return null;
+
+ ImageEntry entry = mImageCache.get(item.getPath());
+ if (entry == null) return null;
+
+ // Create a default ScreenNail if the real one is not available yet,
+ // except for camera that a black screen is better than a gray tile.
+ if (entry.screenNail == null && !isCamera(offset)) {
+ entry.screenNail = newPlaceholderScreenNail(item);
+ if (offset == 0) updateTileProvider(entry);
+ }
+
+ return entry.screenNail;
+ }
+
+ @Override
+ public void getImageSize(int offset, PhotoView.Size size) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ if (item == null) {
+ size.width = 0;
+ size.height = 0;
+ } else {
+ size.width = item.getWidth();
+ size.height = item.getHeight();
+ }
+ }
+
+ @Override
+ public int getImageRotation(int offset) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ return (item == null) ? 0 : item.getFullImageRotation();
+ }
+
+ @Override
+ public void setNeedFullImage(boolean enabled) {
+ mNeedFullImage = enabled;
+ mMainHandler.sendEmptyMessage(MSG_UPDATE_IMAGE_REQUESTS);
+ }
+
+ @Override
+ public boolean isCamera(int offset) {
+ return mCurrentIndex + offset == mCameraIndex;
+ }
+
+ @Override
+ public boolean isPanorama(int offset) {
+ return isCamera(offset) && mIsPanorama;
+ }
+
+ @Override
+ public boolean isStaticCamera(int offset) {
+ return isCamera(offset) && mIsStaticCamera;
+ }
+
+ @Override
+ public boolean isVideo(int offset) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ return (item == null)
+ ? false
+ : item.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
+ }
+
+ @Override
+ public boolean isDeletable(int offset) {
+ MediaItem item = getItem(mCurrentIndex + offset);
+ return (item == null)
+ ? false
+ : (item.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0;
+ }
+
+ @Override
+ public int getLoadingState(int offset) {
+ ImageEntry entry = mImageCache.get(getPath(mCurrentIndex + offset));
+ if (entry == null) return LOADING_INIT;
+ if (entry.failToLoad) return LOADING_FAIL;
+ if (entry.screenNail != null) return LOADING_COMPLETE;
+ return LOADING_INIT;
+ }
+
+ @Override
+ public ScreenNail getScreenNail() {
+ return getScreenNail(0);
+ }
+
+ @Override
+ public int getImageHeight() {
+ return mTileProvider.getImageHeight();
+ }
+
+ @Override
+ public int getImageWidth() {
+ return mTileProvider.getImageWidth();
+ }
+
+ @Override
+ public int getLevelCount() {
+ return mTileProvider.getLevelCount();
+ }
+
+ @Override
+ public Bitmap getTile(int level, int x, int y, int tileSize) {
+ return mTileProvider.getTile(level, x, y, tileSize);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return mSize == 0;
+ }
+
+ @Override
+ public int getCurrentIndex() {
+ return mCurrentIndex;
+ }
+
+ @Override
+ public MediaItem getMediaItem(int offset) {
+ int index = mCurrentIndex + offset;
+ if (index >= mContentStart && index < mContentEnd) {
+ return mData[index % DATA_CACHE_SIZE];
+ }
+ return null;
+ }
+
+ @Override
+ public void setCurrentPhoto(Path path, int indexHint) {
+ if (mItemPath == path) return;
+ mItemPath = path;
+ mCurrentIndex = indexHint;
+ updateSlidingWindow();
+ updateImageCache();
+ fireDataChange();
+
+ // We need to reload content if the path doesn't match.
+ MediaItem item = getMediaItem(0);
+ if (item != null && item.getPath() != path) {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ @Override
+ public void setFocusHintDirection(int direction) {
+ mFocusHintDirection = direction;
+ }
+
+ @Override
+ public void setFocusHintPath(Path path) {
+ mFocusHintPath = path;
+ }
+
+ private void updateTileProvider() {
+ ImageEntry entry = mImageCache.get(getPath(mCurrentIndex));
+ if (entry == null) { // in loading
+ mTileProvider.clear();
+ } else {
+ updateTileProvider(entry);
+ }
+ }
+
+ private void updateTileProvider(ImageEntry entry) {
+ ScreenNail screenNail = entry.screenNail;
+ BitmapRegionDecoder fullImage = entry.fullImage;
+ if (screenNail != null) {
+ if (fullImage != null) {
+ mTileProvider.setScreenNail(screenNail,
+ fullImage.getWidth(), fullImage.getHeight());
+ mTileProvider.setRegionDecoder(fullImage);
+ } else {
+ int width = screenNail.getWidth();
+ int height = screenNail.getHeight();
+ mTileProvider.setScreenNail(screenNail, width, height);
+ }
+ } else {
+ mTileProvider.clear();
+ }
+ }
+
+ 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;
+ if (bit == BIT_FULL_IMAGE && !mNeedFullImage) continue;
+ 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.requestedScreenNail = MediaObject.INVALID_DATA_VERSION;
+ }
+ if (entry.fullImageTask != null && entry.fullImageTask != task) {
+ entry.fullImageTask.cancel();
+ entry.fullImageTask = null;
+ entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+ }
+ }
+ }
+
+ private class ScreenNailJob implements Job<ScreenNail> {
+ private MediaItem mItem;
+
+ public ScreenNailJob(MediaItem item) {
+ mItem = item;
+ }
+
+ @Override
+ public ScreenNail run(JobContext jc) {
+ // We try to get a ScreenNail first, if it fails, we fallback to get
+ // a Bitmap and then wrap it in a BitmapScreenNail instead.
+ ScreenNail s = mItem.getScreenNail();
+ if (s != null) return s;
+
+ // If this is a temporary item, don't try to get its bitmap because
+ // it won't be available. We will get its bitmap after a data reload.
+ if (isTemporaryItem(mItem)) {
+ return newPlaceholderScreenNail(mItem);
+ }
+
+ Bitmap bitmap = mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
+ if (jc.isCancelled()) return null;
+ if (bitmap != null) {
+ bitmap = BitmapUtils.rotateBitmap(bitmap,
+ mItem.getRotation() - mItem.getFullImageRotation(), true);
+ }
+ return bitmap == null ? null : new TiledScreenNail(bitmap);
+ }
+ }
+
+ private class FullImageJob implements Job<BitmapRegionDecoder> {
+ private MediaItem mItem;
+
+ public FullImageJob(MediaItem item) {
+ mItem = item;
+ }
+
+ @Override
+ public BitmapRegionDecoder run(JobContext jc) {
+ if (isTemporaryItem(mItem)) {
+ return null;
+ }
+ return mItem.requestLargeImage().run(jc);
+ }
+ }
+
+ // Returns true if we think this is a temporary item created by Camera. A
+ // temporary item is an image or a video whose data is still being
+ // processed, but an incomplete entry is created first in MediaProvider, so
+ // we can display them (in grey tile) even if they are not saved to disk
+ // yet. When the image or video data is actually saved, we will get
+ // notification from MediaProvider, reload data, and show the actual image
+ // or video data.
+ private boolean isTemporaryItem(MediaItem mediaItem) {
+ // Must have camera to create a temporary item.
+ if (mCameraIndex < 0) return false;
+ // Must be an item in camera roll.
+ if (!(mediaItem instanceof LocalMediaItem)) return false;
+ LocalMediaItem item = (LocalMediaItem) mediaItem;
+ if (item.getBucketId() != MediaSetUtils.CAMERA_BUCKET_ID) return false;
+ // Must have no size, but must have width and height information
+ if (item.getSize() != 0) return false;
+ if (item.getWidth() == 0) return false;
+ if (item.getHeight() == 0) return false;
+ // Must be created in the last 10 seconds.
+ if (item.getDateInMs() - System.currentTimeMillis() > 10000) return false;
+ return true;
+ }
+
+ // Create a default ScreenNail when a ScreenNail is needed, but we don't yet
+ // have one available (because the image data is still being saved, or the
+ // Bitmap is still being loaded.
+ private ScreenNail newPlaceholderScreenNail(MediaItem item) {
+ int width = item.getWidth();
+ int height = item.getHeight();
+ return new TiledScreenNail(width, height);
+ }
+
+ // 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(getPath(index));
+ if (entry == null) return null;
+ MediaItem item = mData[index % DATA_CACHE_SIZE];
+ Utils.assertTrue(item != null);
+ long version = item.getDataVersion();
+
+ if (which == BIT_SCREEN_NAIL && entry.screenNailTask != null
+ && entry.requestedScreenNail == version) {
+ return entry.screenNailTask;
+ } else if (which == BIT_FULL_IMAGE && entry.fullImageTask != null
+ && entry.requestedFullImage == version) {
+ return entry.fullImageTask;
+ }
+
+ if (which == BIT_SCREEN_NAIL && entry.requestedScreenNail != version) {
+ entry.requestedScreenNail = version;
+ entry.screenNailTask = mThreadPool.submit(
+ new ScreenNailJob(item),
+ new ScreenNailListener(item));
+ // request screen nail
+ return entry.screenNailTask;
+ }
+ if (which == BIT_FULL_IMAGE && entry.requestedFullImage != version
+ && (item.getSupportedOperations()
+ & MediaItem.SUPPORT_FULL_IMAGE) != 0) {
+ entry.requestedFullImage = version;
+ entry.fullImageTask = mThreadPool.submit(
+ new FullImageJob(item),
+ new FullImageListener(item));
+ // request full image
+ return entry.fullImageTask;
+ }
+ return null;
+ }
+
+ private void updateImageCache() {
+ HashSet<Path> toBeRemoved = new HashSet<Path>(mImageCache.keySet());
+ for (int i = mActiveStart; i < mActiveEnd; ++i) {
+ MediaItem item = mData[i % DATA_CACHE_SIZE];
+ if (item == null) continue;
+ Path path = item.getPath();
+ ImageEntry entry = mImageCache.get(path);
+ toBeRemoved.remove(path);
+ if (entry != null) {
+ if (Math.abs(i - mCurrentIndex) > 1) {
+ if (entry.fullImageTask != null) {
+ entry.fullImageTask.cancel();
+ entry.fullImageTask = null;
+ }
+ entry.fullImage = null;
+ entry.requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+ }
+ if (entry.requestedScreenNail != item.getDataVersion()) {
+ // This ScreenNail is outdated, we want to update it if it's
+ // still a placeholder.
+ if (entry.screenNail instanceof TiledScreenNail) {
+ TiledScreenNail s = (TiledScreenNail) entry.screenNail;
+ s.updatePlaceholderSize(
+ item.getWidth(), item.getHeight());
+ }
+ }
+ } else {
+ entry = new ImageEntry();
+ mImageCache.put(path, entry);
+ }
+ }
+
+ // Clear the data and requests for ImageEntries outside the new window.
+ for (Path path : toBeRemoved) {
+ ImageEntry entry = mImageCache.remove(path);
+ if (entry.fullImageTask != null) entry.fullImageTask.cancel();
+ if (entry.screenNailTask != null) entry.screenNailTask.cancel();
+ if (entry.screenNail != null) entry.screenNail.recycle();
+ }
+
+ updateScreenNailUploadQueue();
+ }
+
+ private class FullImageListener
+ implements Runnable, FutureListener<BitmapRegionDecoder> {
+ private final Path mPath;
+ private Future<BitmapRegionDecoder> mFuture;
+
+ public FullImageListener(MediaItem item) {
+ mPath = item.getPath();
+ }
+
+ @Override
+ public void onFutureDone(Future<BitmapRegionDecoder> future) {
+ mFuture = future;
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
+ }
+
+ @Override
+ public void run() {
+ updateFullImage(mPath, mFuture);
+ }
+ }
+
+ private class ScreenNailListener
+ implements Runnable, FutureListener<ScreenNail> {
+ private final Path mPath;
+ private Future<ScreenNail> mFuture;
+
+ public ScreenNailListener(MediaItem item) {
+ mPath = item.getPath();
+ }
+
+ @Override
+ public void onFutureDone(Future<ScreenNail> future) {
+ mFuture = future;
+ mMainHandler.sendMessage(
+ mMainHandler.obtainMessage(MSG_RUN_OBJECT, this));
+ }
+
+ @Override
+ public void run() {
+ updateScreenNail(mPath, mFuture);
+ }
+ }
+
+ private static class ImageEntry {
+ public BitmapRegionDecoder fullImage;
+ public ScreenNail screenNail;
+ public Future<ScreenNail> screenNailTask;
+ public Future<BitmapRegionDecoder> fullImageTask;
+ public long requestedScreenNail = MediaObject.INVALID_DATA_VERSION;
+ public long requestedFullImage = MediaObject.INVALID_DATA_VERSION;
+ public boolean failToLoad = false;
+ }
+
+ private class SourceListener implements ContentListener {
+ @Override
+ public void onContentDirty() {
+ if (mReloadTask != null) mReloadTask.notifyDirty();
+ }
+ }
+
+ private <T> T executeAndWait(Callable<T> callable) {
+ FutureTask<T> task = new FutureTask<T>(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<MediaItem> items;
+ }
+
+ private class GetUpdateInfo implements Callable<UpdateInfo> {
+
+ 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 {
+ // TODO: Try to load some data in first update
+ 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<Void> {
+ 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;
+ }
+
+ 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;
+ }
+ }
+
+ // update mItemPath
+ MediaItem current = mData[mCurrentIndex % DATA_CACHE_SIZE];
+ mItemPath = current == null ? null : current.getPath();
+
+ updateImageCache();
+ updateTileProvider();
+ updateImageRequests();
+
+ if (mDataListener != null) {
+ mDataListener.onPhotoChanged(mCurrentIndex, mItemPath);
+ }
+
+ fireDataChange();
+ return null;
+ }
+ }
+
+ 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());
+ 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);
+
+ int index = MediaSet.INDEX_NOT_FOUND;
+
+ // First try to focus on the given hint path if there is one.
+ if (mFocusHintPath != null) {
+ index = findIndexOfPathInCache(info, mFocusHintPath);
+ mFocusHintPath = null;
+ }
+
+ // Otherwise try to see if the currently focused item can be found.
+ if (index == MediaSet.INDEX_NOT_FOUND) {
+ MediaItem item = findCurrentMediaItem(info);
+ if (item != null && item.getPath() == info.target) {
+ index = info.indexHint;
+ } else {
+ index = findIndexOfTarget(info);
+ }
+ }
+
+ // The image has been deleted. Focus on the next image (keep
+ // mCurrentIndex unchanged) or the previous image (decrease
+ // mCurrentIndex by 1). In page mode we want to see the next
+ // image, so we focus on the next one. In film mode we want the
+ // later images to shift left to fill the empty space, so we
+ // focus on the previous image (so it will not move). In any
+ // case the index needs to be limited to [0, mSize).
+ if (index == MediaSet.INDEX_NOT_FOUND) {
+ index = info.indexHint;
+ int focusHintDirection = mFocusHintDirection;
+ if (index == (mCameraIndex + 1)) {
+ focusHintDirection = FOCUS_HINT_NEXT;
+ }
+ if (focusHintDirection == FOCUS_HINT_PREVIOUS
+ && index > 0) {
+ index--;
+ }
+ }
+
+ // Don't change index if mSize == 0
+ if (mSize > 0) {
+ if (index >= mSize) index = mSize - 1;
+ }
+
+ info.indexHint = index;
+
+ executeAndWait(new UpdateContent(info));
+ }
+ }
+
+ public synchronized void notifyDirty() {
+ mDirty = true;
+ notifyAll();
+ }
+
+ public synchronized void terminate() {
+ mActive = false;
+ notifyAll();
+ }
+
+ private MediaItem findCurrentMediaItem(UpdateInfo info) {
+ ArrayList<MediaItem> 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<MediaItem> items = info.items;
+
+ // First, try to find the item in the data just loaded
+ if (items != null) {
+ int i = findIndexOfPathInCache(info, info.target);
+ if (i != MediaSet.INDEX_NOT_FOUND) return i;
+ }
+
+ // Not found, find it in mSource.
+ return mSource.getIndexOfItem(info.target, info.indexHint);
+ }
+
+ private int findIndexOfPathInCache(UpdateInfo info, Path path) {
+ ArrayList<MediaItem> items = info.items;
+ for (int i = 0, n = items.size(); i < n; ++i) {
+ MediaItem item = items.get(i);
+ if (item != null && item.getPath() == path) {
+ return i + info.contentStart;
+ }
+ }
+ return MediaSet.INDEX_NOT_FOUND;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/PhotoPage.java b/src/com/android/gallery3d/app/PhotoPage.java
new file mode 100644
index 000000000..7a71e9109
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoPage.java
@@ -0,0 +1,1571 @@
+/*
+ * 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 android.annotation.TargetApi;
+import android.app.ActionBar.OnMenuVisibilityListener;
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcAdapter.CreateBeamUrisCallback;
+import android.nfc.NfcEvent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.RelativeLayout;
+import android.widget.ShareActionProvider;
+import android.widget.Toast;
+
+import com.android.camera.CameraActivity;
+import com.android.camera.ProxyLauncher;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.ComboAlbum;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.FilterDeleteSet;
+import com.android.gallery3d.data.FilterSource;
+import com.android.gallery3d.data.LocalImage;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.data.SecureAlbum;
+import com.android.gallery3d.data.SecureSource;
+import com.android.gallery3d.data.SnailAlbum;
+import com.android.gallery3d.data.SnailItem;
+import com.android.gallery3d.data.SnailSource;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.picasasource.PicasaSource;
+import com.android.gallery3d.ui.DetailsHelper;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
+import com.android.gallery3d.ui.GLView;
+import com.android.gallery3d.ui.MenuExecutor;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.SelectionManager;
+import com.android.gallery3d.ui.SynchronizedHandler;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.UsageStatistics;
+
+public abstract class PhotoPage extends ActivityState implements
+ PhotoView.Listener, AppBridge.Server, ShareActionProvider.OnShareTargetSelectedListener,
+ PhotoPageBottomControls.Delegate, GalleryActionBar.OnAlbumModeSelectedListener {
+ private static final String TAG = "PhotoPage";
+
+ private static final int MSG_HIDE_BARS = 1;
+ private static final int MSG_ON_FULL_SCREEN_CHANGED = 4;
+ private static final int MSG_UPDATE_ACTION_BAR = 5;
+ private static final int MSG_UNFREEZE_GLROOT = 6;
+ private static final int MSG_WANT_BARS = 7;
+ private static final int MSG_REFRESH_BOTTOM_CONTROLS = 8;
+ private static final int MSG_ON_CAMERA_CENTER = 9;
+ private static final int MSG_ON_PICTURE_CENTER = 10;
+ private static final int MSG_REFRESH_IMAGE = 11;
+ private static final int MSG_UPDATE_PHOTO_UI = 12;
+ private static final int MSG_UPDATE_PROGRESS = 13;
+ private static final int MSG_UPDATE_DEFERRED = 14;
+ private static final int MSG_UPDATE_SHARE_URI = 15;
+ private static final int MSG_UPDATE_PANORAMA_UI = 16;
+
+ private static final int HIDE_BARS_TIMEOUT = 3500;
+ private static final int UNFREEZE_GLROOT_TIMEOUT = 250;
+
+ private static final int REQUEST_SLIDESHOW = 1;
+ private static final int REQUEST_CROP = 2;
+ private static final int REQUEST_CROP_PICASA = 3;
+ private static final int REQUEST_EDIT = 4;
+ private static final int REQUEST_PLAY_VIDEO = 5;
+ private static final int REQUEST_TRIM = 6;
+
+ 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";
+ public static final String KEY_OPEN_ANIMATION_RECT = "open-animation-rect";
+ public static final String KEY_APP_BRIDGE = "app-bridge";
+ public static final String KEY_TREAT_BACK_AS_UP = "treat-back-as-up";
+ public static final String KEY_START_IN_FILMSTRIP = "start-in-filmstrip";
+ public static final String KEY_RETURN_INDEX_HINT = "return-index-hint";
+ public static final String KEY_SHOW_WHEN_LOCKED = "show_when_locked";
+ public static final String KEY_IN_CAMERA_ROLL = "in_camera_roll";
+
+ public static final String KEY_ALBUMPAGE_TRANSITION = "albumpage-transition";
+ public static final int MSG_ALBUMPAGE_NONE = 0;
+ public static final int MSG_ALBUMPAGE_STARTED = 1;
+ public static final int MSG_ALBUMPAGE_RESUMED = 2;
+ public static final int MSG_ALBUMPAGE_PICKED = 4;
+
+ public static final String ACTION_NEXTGEN_EDIT = "action_nextgen_edit";
+ public static final String ACTION_SIMPLE_EDIT = "action_simple_edit";
+
+ private GalleryApp mApplication;
+ private SelectionManager mSelectionManager;
+
+ private PhotoView mPhotoView;
+ private PhotoPage.Model mModel;
+ private DetailsHelper mDetailsHelper;
+ 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 FilterDeleteSet mMediaSet;
+
+ // The mediaset used by camera launched from secure lock screen.
+ private SecureAlbum mSecureAlbum;
+
+ private int mCurrentIndex = 0;
+ private Handler mHandler;
+ private boolean mShowBars = true;
+ private volatile boolean mActionBarAllowed = true;
+ private GalleryActionBar mActionBar;
+ private boolean mIsMenuVisible;
+ private boolean mHaveImageEditor;
+ private PhotoPageBottomControls mBottomControls;
+ private PhotoPageProgressBar mProgressBar;
+ private MediaItem mCurrentPhoto = null;
+ private MenuExecutor mMenuExecutor;
+ private boolean mIsActive;
+ private boolean mShowSpinner;
+ private String mSetPathString;
+ // This is the original mSetPathString before adding the camera preview item.
+ private String mOriginalSetPathString;
+ private AppBridge mAppBridge;
+ private SnailItem mScreenNailItem;
+ private SnailAlbum mScreenNailSet;
+ private OrientationManager mOrientationManager;
+ private boolean mTreatBackAsUp;
+ private boolean mStartInFilmstrip;
+ private boolean mHasCameraScreennailOrPlaceholder = false;
+ private boolean mRecenterCameraOnResume = true;
+
+ // These are only valid after the panorama callback
+ private boolean mIsPanorama;
+ private boolean mIsPanorama360;
+
+ private long mCameraSwitchCutoff = 0;
+ private boolean mSkipUpdateCurrentPhoto = false;
+ private static final long CAMERA_SWITCH_CUTOFF_THRESHOLD_MS = 300;
+
+ private static final long DEFERRED_UPDATE_MS = 250;
+ private boolean mDeferredUpdateWaiting = false;
+ private long mDeferUpdateUntil = Long.MAX_VALUE;
+
+ // The item that is deleted (but it can still be undeleted before commiting)
+ private Path mDeletePath;
+ private boolean mDeleteIsFocus; // whether the deleted item was in focus
+
+ private Uri[] mNfcPushUris = new Uri[1];
+
+ private final MyMenuVisibilityListener mMenuVisibilityListener =
+ new MyMenuVisibilityListener();
+ private UpdateProgressListener mProgressListener;
+
+ private final PanoramaSupportCallback mUpdatePanoramaMenuItemsCallback = new PanoramaSupportCallback() {
+ @Override
+ public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+ boolean isPanorama360) {
+ if (mediaObject == mCurrentPhoto) {
+ mHandler.obtainMessage(MSG_UPDATE_PANORAMA_UI, isPanorama360 ? 1 : 0, 0,
+ mediaObject).sendToTarget();
+ }
+ }
+ };
+
+ private final PanoramaSupportCallback mRefreshBottomControlsCallback = new PanoramaSupportCallback() {
+ @Override
+ public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+ boolean isPanorama360) {
+ if (mediaObject == mCurrentPhoto) {
+ mHandler.obtainMessage(MSG_REFRESH_BOTTOM_CONTROLS, isPanorama ? 1 : 0, isPanorama360 ? 1 : 0,
+ mediaObject).sendToTarget();
+ }
+ }
+ };
+
+ private final PanoramaSupportCallback mUpdateShareURICallback = new PanoramaSupportCallback() {
+ @Override
+ public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+ boolean isPanorama360) {
+ if (mediaObject == mCurrentPhoto) {
+ mHandler.obtainMessage(MSG_UPDATE_SHARE_URI, isPanorama360 ? 1 : 0, 0, mediaObject)
+ .sendToTarget();
+ }
+ }
+ };
+
+ public static interface Model extends PhotoView.Model {
+ public void resume();
+ public void pause();
+ public boolean isEmpty();
+ public void setCurrentPhoto(Path path, int indexHint);
+ }
+
+ private class MyMenuVisibilityListener implements OnMenuVisibilityListener {
+ @Override
+ public void onMenuVisibilityChanged(boolean isVisible) {
+ mIsMenuVisible = isVisible;
+ refreshHidingMessage();
+ }
+ }
+
+ private class UpdateProgressListener implements StitchingChangeListener {
+
+ @Override
+ public void onStitchingResult(Uri uri) {
+ sendUpdate(uri, MSG_REFRESH_IMAGE);
+ }
+
+ @Override
+ public void onStitchingQueued(Uri uri) {
+ sendUpdate(uri, MSG_UPDATE_PROGRESS);
+ }
+
+ @Override
+ public void onStitchingProgress(Uri uri, final int progress) {
+ sendUpdate(uri, MSG_UPDATE_PROGRESS);
+ }
+
+ private void sendUpdate(Uri uri, int message) {
+ MediaObject currentPhoto = mCurrentPhoto;
+ boolean isCurrentPhoto = currentPhoto instanceof LocalImage
+ && currentPhoto.getContentUri().equals(uri);
+ if (isCurrentPhoto) {
+ mHandler.sendEmptyMessage(message);
+ }
+ }
+ };
+
+ @Override
+ protected int getBackgroundColorId() {
+ return R.color.photo_background;
+ }
+
+ private final GLView mRootPane = new GLView() {
+ @Override
+ protected void onLayout(
+ boolean changed, int left, int top, int right, int bottom) {
+ mPhotoView.layout(0, 0, right - left, bottom - top);
+ if (mShowDetails) {
+ mDetailsHelper.layout(left, mActionBar.getHeight(), right, bottom);
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ super.onCreate(data, restoreState);
+ mActionBar = mActivity.getGalleryActionBar();
+ mSelectionManager = new SelectionManager(mActivity, false);
+ mMenuExecutor = new MenuExecutor(mActivity, mSelectionManager);
+
+ mPhotoView = new PhotoView(mActivity);
+ mPhotoView.setListener(this);
+ mRootPane.addComponent(mPhotoView);
+ mApplication = (GalleryApp) ((Activity) mActivity).getApplication();
+ mOrientationManager = mActivity.getOrientationManager();
+ mActivity.getGLRoot().setOrientationSource(mOrientationManager);
+
+ mHandler = new SynchronizedHandler(mActivity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_HIDE_BARS: {
+ hideBars();
+ break;
+ }
+ case MSG_REFRESH_BOTTOM_CONTROLS: {
+ if (mCurrentPhoto == message.obj && mBottomControls != null) {
+ mIsPanorama = message.arg1 == 1;
+ mIsPanorama360 = message.arg2 == 1;
+ mBottomControls.refresh();
+ }
+ break;
+ }
+ case MSG_ON_FULL_SCREEN_CHANGED: {
+ if (mAppBridge != null) {
+ mAppBridge.onFullScreenChanged(message.arg1 == 1);
+ }
+ break;
+ }
+ case MSG_UPDATE_ACTION_BAR: {
+ updateBars();
+ break;
+ }
+ case MSG_WANT_BARS: {
+ wantBars();
+ break;
+ }
+ case MSG_UNFREEZE_GLROOT: {
+ mActivity.getGLRoot().unfreeze();
+ break;
+ }
+ case MSG_UPDATE_DEFERRED: {
+ long nextUpdate = mDeferUpdateUntil - SystemClock.uptimeMillis();
+ if (nextUpdate <= 0) {
+ mDeferredUpdateWaiting = false;
+ updateUIForCurrentPhoto();
+ } else {
+ mHandler.sendEmptyMessageDelayed(MSG_UPDATE_DEFERRED, nextUpdate);
+ }
+ break;
+ }
+ case MSG_ON_CAMERA_CENTER: {
+ mSkipUpdateCurrentPhoto = false;
+ boolean stayedOnCamera = false;
+ if (!mPhotoView.getFilmMode()) {
+ stayedOnCamera = true;
+ } else if (SystemClock.uptimeMillis() < mCameraSwitchCutoff &&
+ mMediaSet.getMediaItemCount() > 1) {
+ mPhotoView.switchToImage(1);
+ } else {
+ if (mAppBridge != null) mPhotoView.setFilmMode(false);
+ stayedOnCamera = true;
+ }
+
+ if (stayedOnCamera) {
+ if (mAppBridge == null && mMediaSet.getTotalMediaItemCount() > 1) {
+ launchCamera();
+ /* We got here by swiping from photo 1 to the
+ placeholder, so make it be the thing that
+ is in focus when the user presses back from
+ the camera app */
+ mPhotoView.switchToImage(1);
+ } else {
+ updateBars();
+ updateCurrentPhoto(mModel.getMediaItem(0));
+ }
+ }
+ break;
+ }
+ case MSG_ON_PICTURE_CENTER: {
+ if (!mPhotoView.getFilmMode() && mCurrentPhoto != null
+ && (mCurrentPhoto.getSupportedOperations() & MediaObject.SUPPORT_ACTION) != 0) {
+ mPhotoView.setFilmMode(true);
+ }
+ break;
+ }
+ case MSG_REFRESH_IMAGE: {
+ final MediaItem photo = mCurrentPhoto;
+ mCurrentPhoto = null;
+ updateCurrentPhoto(photo);
+ break;
+ }
+ case MSG_UPDATE_PHOTO_UI: {
+ updateUIForCurrentPhoto();
+ break;
+ }
+ case MSG_UPDATE_PROGRESS: {
+ updateProgressBar();
+ break;
+ }
+ case MSG_UPDATE_SHARE_URI: {
+ if (mCurrentPhoto == message.obj) {
+ boolean isPanorama360 = message.arg1 != 0;
+ Uri contentUri = mCurrentPhoto.getContentUri();
+ Intent panoramaIntent = null;
+ if (isPanorama360) {
+ panoramaIntent = createSharePanoramaIntent(contentUri);
+ }
+ Intent shareIntent = createShareIntent(mCurrentPhoto);
+
+ mActionBar.setShareIntents(panoramaIntent, shareIntent, PhotoPage.this);
+ setNfcBeamPushUri(contentUri);
+ }
+ break;
+ }
+ case MSG_UPDATE_PANORAMA_UI: {
+ if (mCurrentPhoto == message.obj) {
+ boolean isPanorama360 = message.arg1 != 0;
+ updatePanoramaUI(isPanorama360);
+ }
+ break;
+ }
+ default: throw new AssertionError(message.what);
+ }
+ }
+ };
+
+ mSetPathString = data.getString(KEY_MEDIA_SET_PATH);
+ mOriginalSetPathString = mSetPathString;
+ setupNfcBeamPush();
+ String itemPathString = data.getString(KEY_MEDIA_ITEM_PATH);
+ Path itemPath = itemPathString != null ?
+ Path.fromString(data.getString(KEY_MEDIA_ITEM_PATH)) :
+ null;
+ mTreatBackAsUp = data.getBoolean(KEY_TREAT_BACK_AS_UP, false);
+ mStartInFilmstrip = data.getBoolean(KEY_START_IN_FILMSTRIP, false);
+ boolean inCameraRoll = data.getBoolean(KEY_IN_CAMERA_ROLL, false);
+ mCurrentIndex = data.getInt(KEY_INDEX_HINT, 0);
+ if (mSetPathString != null) {
+ mShowSpinner = true;
+ mAppBridge = (AppBridge) data.getParcelable(KEY_APP_BRIDGE);
+ if (mAppBridge != null) {
+ mShowBars = false;
+ mHasCameraScreennailOrPlaceholder = true;
+ mAppBridge.setServer(this);
+
+ // Get the ScreenNail from AppBridge and register it.
+ int id = SnailSource.newId();
+ Path screenNailSetPath = SnailSource.getSetPath(id);
+ Path screenNailItemPath = SnailSource.getItemPath(id);
+ mScreenNailSet = (SnailAlbum) mActivity.getDataManager()
+ .getMediaObject(screenNailSetPath);
+ mScreenNailItem = (SnailItem) mActivity.getDataManager()
+ .getMediaObject(screenNailItemPath);
+ mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
+
+ if (data.getBoolean(KEY_SHOW_WHEN_LOCKED, false)) {
+ // Set the flag to be on top of the lock screen.
+ mFlags |= FLAG_SHOW_WHEN_LOCKED;
+ }
+
+ // Don't display "empty album" action item for capture intents.
+ if (!mSetPathString.equals("/local/all/0")) {
+ // Check if the path is a secure album.
+ if (SecureSource.isSecurePath(mSetPathString)) {
+ mSecureAlbum = (SecureAlbum) mActivity.getDataManager()
+ .getMediaSet(mSetPathString);
+ mShowSpinner = false;
+ }
+ mSetPathString = "/filter/empty/{"+mSetPathString+"}";
+ }
+
+ // Combine the original MediaSet with the one for ScreenNail
+ // from AppBridge.
+ mSetPathString = "/combo/item/{" + screenNailSetPath +
+ "," + mSetPathString + "}";
+
+ // Start from the screen nail.
+ itemPath = screenNailItemPath;
+ } else if (inCameraRoll && GalleryUtils.isCameraAvailable(mActivity)) {
+ mSetPathString = "/combo/item/{" + FilterSource.FILTER_CAMERA_SHORTCUT +
+ "," + mSetPathString + "}";
+ mCurrentIndex++;
+ mHasCameraScreennailOrPlaceholder = true;
+ }
+
+ MediaSet originalSet = mActivity.getDataManager()
+ .getMediaSet(mSetPathString);
+ if (mHasCameraScreennailOrPlaceholder && originalSet instanceof ComboAlbum) {
+ // Use the name of the camera album rather than the default
+ // ComboAlbum behavior
+ ((ComboAlbum) originalSet).useNameOfChild(1);
+ }
+ mSelectionManager.setSourceMediaSet(originalSet);
+ mSetPathString = "/filter/delete/{" + mSetPathString + "}";
+ mMediaSet = (FilterDeleteSet) mActivity.getDataManager()
+ .getMediaSet(mSetPathString);
+ if (mMediaSet == null) {
+ Log.w(TAG, "failed to restore " + mSetPathString);
+ }
+ if (itemPath == null) {
+ int mediaItemCount = mMediaSet.getMediaItemCount();
+ if (mediaItemCount > 0) {
+ if (mCurrentIndex >= mediaItemCount) mCurrentIndex = 0;
+ itemPath = mMediaSet.getMediaItem(mCurrentIndex, 1)
+ .get(0).getPath();
+ } else {
+ // Bail out, PhotoPage can't load on an empty album
+ return;
+ }
+ }
+ PhotoDataAdapter pda = new PhotoDataAdapter(
+ mActivity, mPhotoView, mMediaSet, itemPath, mCurrentIndex,
+ mAppBridge == null ? -1 : 0,
+ mAppBridge == null ? false : mAppBridge.isPanorama(),
+ mAppBridge == null ? false : mAppBridge.isStaticCamera());
+ mModel = pda;
+ mPhotoView.setModel(mModel);
+
+ pda.setDataListener(new PhotoDataAdapter.DataListener() {
+
+ @Override
+ public void onPhotoChanged(int index, Path item) {
+ int oldIndex = mCurrentIndex;
+ mCurrentIndex = index;
+
+ if (mHasCameraScreennailOrPlaceholder) {
+ if (mCurrentIndex > 0) {
+ mSkipUpdateCurrentPhoto = false;
+ }
+
+ if (oldIndex == 0 && mCurrentIndex > 0
+ && !mPhotoView.getFilmMode()) {
+ mPhotoView.setFilmMode(true);
+ if (mAppBridge != null) {
+ UsageStatistics.onEvent("CameraToFilmstrip",
+ UsageStatistics.TRANSITION_SWIPE, null);
+ }
+ } else if (oldIndex == 2 && mCurrentIndex == 1) {
+ mCameraSwitchCutoff = SystemClock.uptimeMillis() +
+ CAMERA_SWITCH_CUTOFF_THRESHOLD_MS;
+ mPhotoView.stopScrolling();
+ } else if (oldIndex >= 1 && mCurrentIndex == 0) {
+ mPhotoView.setWantPictureCenterCallbacks(true);
+ mSkipUpdateCurrentPhoto = true;
+ }
+ }
+ if (!mSkipUpdateCurrentPhoto) {
+ if (item != null) {
+ MediaItem photo = mModel.getMediaItem(0);
+ if (photo != null) updateCurrentPhoto(photo);
+ }
+ updateBars();
+ }
+ // Reset the timeout for the bars after a swipe
+ refreshHidingMessage();
+ }
+
+ @Override
+ public void onLoadingFinished(boolean loadingFailed) {
+ if (!mModel.isEmpty()) {
+ MediaItem photo = mModel.getMediaItem(0);
+ if (photo != null) updateCurrentPhoto(photo);
+ } else if (mIsActive) {
+ // We only want to finish the PhotoPage if there is no
+ // deletion that the user can undo.
+ if (mMediaSet.getNumberOfDeletions() == 0) {
+ mActivity.getStateManager().finishState(
+ PhotoPage.this);
+ }
+ }
+ }
+
+ @Override
+ public void onLoadingStarted() {
+ }
+ });
+ } 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);
+ mShowSpinner = false;
+ }
+
+ mPhotoView.setFilmMode(mStartInFilmstrip && mMediaSet.getMediaItemCount() > 1);
+ RelativeLayout galleryRoot = (RelativeLayout) ((Activity) mActivity)
+ .findViewById(mAppBridge != null ? R.id.content : R.id.gallery_root);
+ if (galleryRoot != null) {
+ if (mSecureAlbum == null) {
+ mBottomControls = new PhotoPageBottomControls(this, mActivity, galleryRoot);
+ }
+ StitchingProgressManager progressManager = mApplication.getStitchingProgressManager();
+ if (progressManager != null) {
+ mProgressBar = new PhotoPageProgressBar(mActivity, galleryRoot);
+ mProgressListener = new UpdateProgressListener();
+ progressManager.addChangeListener(mProgressListener);
+ if (mSecureAlbum != null) {
+ progressManager.addChangeListener(mSecureAlbum);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onPictureCenter(boolean isCamera) {
+ isCamera = isCamera || (mHasCameraScreennailOrPlaceholder && mAppBridge == null);
+ mPhotoView.setWantPictureCenterCallbacks(false);
+ mHandler.removeMessages(MSG_ON_CAMERA_CENTER);
+ mHandler.removeMessages(MSG_ON_PICTURE_CENTER);
+ mHandler.sendEmptyMessage(isCamera ? MSG_ON_CAMERA_CENTER : MSG_ON_PICTURE_CENTER);
+ }
+
+ @Override
+ public boolean canDisplayBottomControls() {
+ return mIsActive && !mPhotoView.canUndo();
+ }
+
+ @Override
+ public boolean canDisplayBottomControl(int control) {
+ if (mCurrentPhoto == null) {
+ return false;
+ }
+ switch(control) {
+ case R.id.photopage_bottom_control_edit:
+ return mHaveImageEditor && mShowBars
+ && !mPhotoView.getFilmMode()
+ && (mCurrentPhoto.getSupportedOperations() & MediaItem.SUPPORT_EDIT) != 0
+ && mCurrentPhoto.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE;
+ case R.id.photopage_bottom_control_panorama:
+ return mIsPanorama;
+ case R.id.photopage_bottom_control_tiny_planet:
+ return mHaveImageEditor && mShowBars
+ && mIsPanorama360 && !mPhotoView.getFilmMode();
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void onBottomControlClicked(int control) {
+ switch(control) {
+ case R.id.photopage_bottom_control_edit:
+ launchPhotoEditor();
+ return;
+ case R.id.photopage_bottom_control_panorama:
+ mActivity.getPanoramaViewHelper()
+ .showPanorama(mCurrentPhoto.getContentUri());
+ return;
+ case R.id.photopage_bottom_control_tiny_planet:
+ launchTinyPlanet();
+ return;
+ default:
+ return;
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ private void setupNfcBeamPush() {
+ if (!ApiHelper.HAS_SET_BEAM_PUSH_URIS) return;
+
+ NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mActivity);
+ if (adapter != null) {
+ adapter.setBeamPushUris(null, mActivity);
+ adapter.setBeamPushUrisCallback(new CreateBeamUrisCallback() {
+ @Override
+ public Uri[] createBeamUris(NfcEvent event) {
+ return mNfcPushUris;
+ }
+ }, mActivity);
+ }
+ }
+
+ private void setNfcBeamPushUri(Uri uri) {
+ mNfcPushUris[0] = uri;
+ }
+
+ private static Intent createShareIntent(MediaObject mediaObject) {
+ int type = mediaObject.getMediaType();
+ return new Intent(Intent.ACTION_SEND)
+ .setType(MenuExecutor.getMimeType(type))
+ .putExtra(Intent.EXTRA_STREAM, mediaObject.getContentUri())
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+
+ private static Intent createSharePanoramaIntent(Uri contentUri) {
+ return new Intent(Intent.ACTION_SEND)
+ .setType(GalleryUtils.MIME_TYPE_PANORAMA360)
+ .putExtra(Intent.EXTRA_STREAM, contentUri)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+
+ private void overrideTransitionToEditor() {
+ ((Activity) mActivity).overridePendingTransition(android.R.anim.fade_in,
+ android.R.anim.fade_out);
+ }
+
+ private void launchTinyPlanet() {
+ // Deep link into tiny planet
+ MediaItem current = mModel.getMediaItem(0);
+ Intent intent = new Intent(FilterShowActivity.TINY_PLANET_ACTION);
+ intent.setClass(mActivity, FilterShowActivity.class);
+ intent.setDataAndType(current.getContentUri(), current.getMimeType())
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.putExtra(FilterShowActivity.LAUNCH_FULLSCREEN,
+ mActivity.isFullscreen());
+ mActivity.startActivityForResult(intent, REQUEST_EDIT);
+ overrideTransitionToEditor();
+ }
+
+ private void launchCamera() {
+ Intent intent = new Intent(mActivity, CameraActivity.class)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mRecenterCameraOnResume = false;
+ mActivity.startActivity(intent);
+ }
+
+ private void launchPhotoEditor() {
+ MediaItem current = mModel.getMediaItem(0);
+ if (current == null || (current.getSupportedOperations()
+ & MediaObject.SUPPORT_EDIT) == 0) {
+ return;
+ }
+
+ Intent intent = new Intent(ACTION_NEXTGEN_EDIT);
+
+ intent.setDataAndType(current.getContentUri(), current.getMimeType())
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ if (mActivity.getPackageManager()
+ .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() == 0) {
+ intent.setAction(Intent.ACTION_EDIT);
+ }
+ intent.putExtra(FilterShowActivity.LAUNCH_FULLSCREEN,
+ mActivity.isFullscreen());
+ ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null),
+ REQUEST_EDIT);
+ overrideTransitionToEditor();
+ }
+
+ private void launchSimpleEditor() {
+ MediaItem current = mModel.getMediaItem(0);
+ if (current == null || (current.getSupportedOperations()
+ & MediaObject.SUPPORT_EDIT) == 0) {
+ return;
+ }
+
+ Intent intent = new Intent(ACTION_SIMPLE_EDIT);
+
+ intent.setDataAndType(current.getContentUri(), current.getMimeType())
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ if (mActivity.getPackageManager()
+ .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() == 0) {
+ intent.setAction(Intent.ACTION_EDIT);
+ }
+ intent.putExtra(FilterShowActivity.LAUNCH_FULLSCREEN,
+ mActivity.isFullscreen());
+ ((Activity) mActivity).startActivityForResult(Intent.createChooser(intent, null),
+ REQUEST_EDIT);
+ overrideTransitionToEditor();
+ }
+
+ private void requestDeferredUpdate() {
+ mDeferUpdateUntil = SystemClock.uptimeMillis() + DEFERRED_UPDATE_MS;
+ if (!mDeferredUpdateWaiting) {
+ mDeferredUpdateWaiting = true;
+ mHandler.sendEmptyMessageDelayed(MSG_UPDATE_DEFERRED, DEFERRED_UPDATE_MS);
+ }
+ }
+
+ private void updateUIForCurrentPhoto() {
+ if (mCurrentPhoto == null) return;
+
+ // If by swiping or deletion the user ends up on an action item
+ // and zoomed in, zoom out so that the context of the action is
+ // more clear
+ if ((mCurrentPhoto.getSupportedOperations() & MediaObject.SUPPORT_ACTION) != 0
+ && !mPhotoView.getFilmMode()) {
+ mPhotoView.setWantPictureCenterCallbacks(true);
+ }
+
+ updateMenuOperations();
+ refreshBottomControlsWhenReady();
+ if (mShowDetails) {
+ mDetailsHelper.reloadDetails();
+ }
+ if ((mSecureAlbum == null)
+ && (mCurrentPhoto.getSupportedOperations() & MediaItem.SUPPORT_SHARE) != 0) {
+ mCurrentPhoto.getPanoramaSupport(mUpdateShareURICallback);
+ }
+ updateProgressBar();
+ }
+
+ private void updateCurrentPhoto(MediaItem photo) {
+ if (mCurrentPhoto == photo) return;
+ mCurrentPhoto = photo;
+ if (mPhotoView.getFilmMode()) {
+ requestDeferredUpdate();
+ } else {
+ updateUIForCurrentPhoto();
+ }
+ }
+
+ private void updateProgressBar() {
+ if (mProgressBar != null) {
+ mProgressBar.hideProgress();
+ StitchingProgressManager progressManager = mApplication.getStitchingProgressManager();
+ if (progressManager != null && mCurrentPhoto instanceof LocalImage) {
+ Integer progress = progressManager.getProgress(mCurrentPhoto.getContentUri());
+ if (progress != null) {
+ mProgressBar.setProgress(progress);
+ }
+ }
+ }
+ }
+
+ private void updateMenuOperations() {
+ Menu menu = mActionBar.getMenu();
+
+ // it could be null if onCreateActionBar has not been called yet
+ if (menu == null) return;
+
+ MenuItem item = menu.findItem(R.id.action_slideshow);
+ if (item != null) {
+ item.setVisible((mSecureAlbum == null) && canDoSlideShow());
+ }
+ if (mCurrentPhoto == null) return;
+
+ int supportedOperations = mCurrentPhoto.getSupportedOperations();
+ if (mSecureAlbum != null) {
+ supportedOperations &= MediaObject.SUPPORT_DELETE;
+ } else {
+ mCurrentPhoto.getPanoramaSupport(mUpdatePanoramaMenuItemsCallback);
+ if (!mHaveImageEditor) {
+ supportedOperations &= ~MediaObject.SUPPORT_EDIT;
+ }
+ }
+ MenuExecutor.updateMenuOperation(menu, supportedOperations);
+ }
+
+ private boolean canDoSlideShow() {
+ if (mMediaSet == null || mCurrentPhoto == null) {
+ return false;
+ }
+ if (mCurrentPhoto.getMediaType() != MediaObject.MEDIA_TYPE_IMAGE) {
+ return false;
+ }
+ return true;
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // Action Bar show/hide management
+ //////////////////////////////////////////////////////////////////////////
+
+ private void showBars() {
+ if (mShowBars) return;
+ mShowBars = true;
+ mOrientationManager.unlockOrientation();
+ mActionBar.show();
+ mActivity.getGLRoot().setLightsOutMode(false);
+ refreshHidingMessage();
+ refreshBottomControlsWhenReady();
+ }
+
+ private void hideBars() {
+ if (!mShowBars) return;
+ mShowBars = false;
+ mActionBar.hide();
+ mActivity.getGLRoot().setLightsOutMode(true);
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ refreshBottomControlsWhenReady();
+ }
+
+ private void refreshHidingMessage() {
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ if (!mIsMenuVisible && !mPhotoView.getFilmMode()) {
+ mHandler.sendEmptyMessageDelayed(MSG_HIDE_BARS, HIDE_BARS_TIMEOUT);
+ }
+ }
+
+ private boolean canShowBars() {
+ // No bars if we are showing camera preview.
+ if (mAppBridge != null && mCurrentIndex == 0
+ && !mPhotoView.getFilmMode()) return false;
+
+ // No bars if it's not allowed.
+ if (!mActionBarAllowed) return false;
+
+ Configuration config = mActivity.getResources().getConfiguration();
+ if (config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void wantBars() {
+ if (canShowBars()) showBars();
+ }
+
+ private void toggleBars() {
+ if (mShowBars) {
+ hideBars();
+ } else {
+ if (canShowBars()) showBars();
+ }
+ }
+
+ private void updateBars() {
+ if (!canShowBars()) {
+ hideBars();
+ }
+ }
+
+ @Override
+ protected void onBackPressed() {
+ if (mShowDetails) {
+ hideDetails();
+ } else if (mAppBridge == null || !switchWithCaptureAnimation(-1)) {
+ // We are leaving this page. Set the result now.
+ setResult();
+ if (mStartInFilmstrip && !mPhotoView.getFilmMode()) {
+ mPhotoView.setFilmMode(true);
+ } else if (mTreatBackAsUp) {
+ onUpPressed();
+ } else {
+ super.onBackPressed();
+ }
+ }
+ }
+
+ private void onUpPressed() {
+ if ((mStartInFilmstrip || mAppBridge != null)
+ && !mPhotoView.getFilmMode()) {
+ mPhotoView.setFilmMode(true);
+ return;
+ }
+
+ if (mActivity.getStateManager().getStateCount() > 1) {
+ setResult();
+ super.onBackPressed();
+ return;
+ }
+
+ if (mOriginalSetPathString == null) return;
+
+ if (mAppBridge == null) {
+ // We're in view mode so set up the stacks on our own.
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
+ data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
+ mActivity.getDataManager().getTopSetPath(
+ DataManager.INCLUDE_ALL));
+ mActivity.getStateManager().switchState(this, AlbumPage.class, data);
+ } else {
+ GalleryUtils.startGalleryActivity(mActivity);
+ }
+ }
+
+ private void setResult() {
+ Intent result = null;
+ result = new Intent();
+ result.putExtra(KEY_RETURN_INDEX_HINT, mCurrentIndex);
+ setStateResult(Activity.RESULT_OK, result);
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // AppBridge.Server interface
+ //////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void setCameraRelativeFrame(Rect frame) {
+ mPhotoView.setCameraRelativeFrame(frame);
+ }
+
+ @Override
+ public boolean switchWithCaptureAnimation(int offset) {
+ return mPhotoView.switchWithCaptureAnimation(offset);
+ }
+
+ @Override
+ public void setSwipingEnabled(boolean enabled) {
+ mPhotoView.setSwipingEnabled(enabled);
+ }
+
+ @Override
+ public void notifyScreenNailChanged() {
+ mScreenNailItem.setScreenNail(mAppBridge.attachScreenNail());
+ mScreenNailSet.notifyChange();
+ }
+
+ @Override
+ public void addSecureAlbumItem(boolean isVideo, int id) {
+ mSecureAlbum.addMediaItem(isVideo, id);
+ }
+
+ @Override
+ protected boolean onCreateActionBar(Menu menu) {
+ mActionBar.createActionBarMenu(R.menu.photo, menu);
+ mHaveImageEditor = GalleryUtils.isEditorAvailable(mActivity, "image/*");
+ updateMenuOperations();
+ mActionBar.setTitle(mMediaSet != null ? mMediaSet.getName() : "");
+ return true;
+ }
+
+ private MenuExecutor.ProgressListener mConfirmDialogListener =
+ new MenuExecutor.ProgressListener() {
+ @Override
+ public void onProgressUpdate(int index) {}
+
+ @Override
+ public void onProgressComplete(int result) {}
+
+ @Override
+ public void onConfirmDialogShown() {
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ }
+
+ @Override
+ public void onConfirmDialogDismissed(boolean confirmed) {
+ refreshHidingMessage();
+ }
+
+ @Override
+ public void onProgressStart() {}
+ };
+
+ private void switchToGrid() {
+ if (mActivity.getStateManager().hasStateClass(AlbumPage.class)) {
+ onUpPressed();
+ } else {
+ if (mOriginalSetPathString == null) return;
+ if (mProgressBar != null) {
+ updateCurrentPhoto(null);
+ mProgressBar.hideProgress();
+ }
+ Bundle data = new Bundle(getData());
+ data.putString(AlbumPage.KEY_MEDIA_PATH, mOriginalSetPathString);
+ data.putString(AlbumPage.KEY_PARENT_MEDIA_PATH,
+ mActivity.getDataManager().getTopSetPath(
+ DataManager.INCLUDE_ALL));
+
+ // We only show cluster menu in the first AlbumPage in stack
+ // TODO: Enable this when running from the camera app
+ boolean inAlbum = mActivity.getStateManager().hasStateClass(AlbumPage.class);
+ data.putBoolean(AlbumPage.KEY_SHOW_CLUSTER_MENU, !inAlbum
+ && mAppBridge == null);
+
+ data.putBoolean(PhotoPage.KEY_APP_BRIDGE, mAppBridge != null);
+
+ // Account for live preview being first item
+ mActivity.getTransitionStore().put(KEY_RETURN_INDEX_HINT,
+ mAppBridge != null ? mCurrentIndex - 1 : mCurrentIndex);
+
+ if (mHasCameraScreennailOrPlaceholder && mAppBridge != null) {
+ mActivity.getStateManager().startState(AlbumPage.class, data);
+ } else {
+ mActivity.getStateManager().switchState(this, AlbumPage.class, data);
+ }
+ }
+ }
+
+ @Override
+ protected boolean onItemSelected(MenuItem item) {
+ if (mModel == null) return true;
+ refreshHidingMessage();
+ MediaItem current = mModel.getMediaItem(0);
+
+ // This is a shield for monkey when it clicks the action bar
+ // menu when transitioning from filmstrip to camera
+ if (current instanceof SnailItem) return true;
+ // TODO: We should check the current photo against the MediaItem
+ // that the menu was initially created for. We need to fix this
+ // after PhotoPage being refactored.
+ 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();
+ String confirmMsg = null;
+ switch (action) {
+ case android.R.id.home: {
+ onUpPressed();
+ return true;
+ }
+ case R.id.action_slideshow: {
+ Bundle data = new Bundle();
+ data.putString(SlideshowPage.KEY_SET_PATH, mMediaSet.getPath().toString());
+ data.putString(SlideshowPage.KEY_ITEM_PATH, path.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 = mActivity;
+ Intent intent = new Intent(CropActivity.CROP_ACTION);
+ intent.setClass(activity, CropActivity.class);
+ intent.setDataAndType(manager.getContentUri(path), current.getMimeType())
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ activity.startActivityForResult(intent, PicasaSource.isPicasaImage(current)
+ ? REQUEST_CROP_PICASA
+ : REQUEST_CROP);
+ return true;
+ }
+ case R.id.action_trim: {
+ Intent intent = new Intent(mActivity, TrimVideo.class);
+ intent.setData(manager.getContentUri(path));
+ // We need the file path to wrap this into a RandomAccessFile.
+ intent.putExtra(KEY_MEDIA_ITEM_PATH, current.getFilePath());
+ mActivity.startActivityForResult(intent, REQUEST_TRIM);
+ return true;
+ }
+ case R.id.action_mute: {
+ MuteVideo muteVideo = new MuteVideo(current.getFilePath(),
+ manager.getContentUri(path), mActivity);
+ muteVideo.muteInBackground();
+ return true;
+ }
+ case R.id.action_edit: {
+ launchPhotoEditor();
+ return true;
+ }
+ case R.id.action_simple_edit: {
+ launchSimpleEditor();
+ return true;
+ }
+ case R.id.action_details: {
+ if (mShowDetails) {
+ hideDetails();
+ } else {
+ showDetails();
+ }
+ return true;
+ }
+ case R.id.action_delete:
+ confirmMsg = mActivity.getResources().getQuantityString(
+ R.plurals.delete_selection, 1);
+ case R.id.action_setas:
+ case R.id.action_rotate_ccw:
+ case R.id.action_rotate_cw:
+ case R.id.action_show_on_map:
+ mSelectionManager.deSelectAll();
+ mSelectionManager.toggle(path);
+ mMenuExecutor.onMenuClicked(item, confirmMsg, mConfirmDialogListener);
+ return true;
+ default :
+ return false;
+ }
+ }
+
+ private void hideDetails() {
+ mShowDetails = false;
+ mDetailsHelper.hide();
+ }
+
+ private void showDetails() {
+ mShowDetails = true;
+ if (mDetailsHelper == null) {
+ mDetailsHelper = new DetailsHelper(mActivity, mRootPane, new MyDetailsSource());
+ mDetailsHelper.setCloseListener(new CloseListener() {
+ @Override
+ public void onClose() {
+ hideDetails();
+ }
+ });
+ }
+ mDetailsHelper.show();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Callbacks from PhotoView
+ ////////////////////////////////////////////////////////////////////////////
+ @Override
+ public void onSingleTapUp(int x, int y) {
+ if (mAppBridge != null) {
+ if (mAppBridge.onSingleTapUp(x, y)) return;
+ }
+
+ MediaItem item = mModel.getMediaItem(0);
+ if (item == null || item == mScreenNailItem) {
+ // item is not ready or it is camera preview, ignore
+ return;
+ }
+
+ int supported = item.getSupportedOperations();
+ boolean playVideo = ((supported & MediaItem.SUPPORT_PLAY) != 0);
+ boolean unlock = ((supported & MediaItem.SUPPORT_UNLOCK) != 0);
+ boolean goBack = ((supported & MediaItem.SUPPORT_BACK) != 0);
+ boolean launchCamera = ((supported & MediaItem.SUPPORT_CAMERA_SHORTCUT) != 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) {
+ if (mSecureAlbum == null) {
+ playVideo(mActivity, item.getPlayUri(), item.getName());
+ } else {
+ mActivity.getStateManager().finishState(this);
+ }
+ } else if (goBack) {
+ onBackPressed();
+ } else if (unlock) {
+ Intent intent = new Intent(mActivity, Gallery.class);
+ intent.putExtra(Gallery.KEY_DISMISS_KEYGUARD, true);
+ mActivity.startActivity(intent);
+ } else if (launchCamera) {
+ launchCamera();
+ } else {
+ toggleBars();
+ }
+ }
+
+ @Override
+ public void onActionBarAllowed(boolean allowed) {
+ mActionBarAllowed = allowed;
+ mHandler.sendEmptyMessage(MSG_UPDATE_ACTION_BAR);
+ }
+
+ @Override
+ public void onActionBarWanted() {
+ mHandler.sendEmptyMessage(MSG_WANT_BARS);
+ }
+
+ @Override
+ public void onFullScreenChanged(boolean full) {
+ Message m = mHandler.obtainMessage(
+ MSG_ON_FULL_SCREEN_CHANGED, full ? 1 : 0, 0);
+ m.sendToTarget();
+ }
+
+ // How we do delete/undo:
+ //
+ // When the user choose to delete a media item, we just tell the
+ // FilterDeleteSet to hide that item. If the user choose to undo it, we
+ // again tell FilterDeleteSet not to hide it. If the user choose to commit
+ // the deletion, we then actually delete the media item.
+ @Override
+ public void onDeleteImage(Path path, int offset) {
+ onCommitDeleteImage(); // commit the previous deletion
+ mDeletePath = path;
+ mDeleteIsFocus = (offset == 0);
+ mMediaSet.addDeletion(path, mCurrentIndex + offset);
+ }
+
+ @Override
+ public void onUndoDeleteImage() {
+ if (mDeletePath == null) return;
+ // If the deletion was done on the focused item, we want the model to
+ // focus on it when it is undeleted.
+ if (mDeleteIsFocus) mModel.setFocusHintPath(mDeletePath);
+ mMediaSet.removeDeletion(mDeletePath);
+ mDeletePath = null;
+ }
+
+ @Override
+ public void onCommitDeleteImage() {
+ if (mDeletePath == null) return;
+ mMenuExecutor.startSingleItemAction(R.id.action_delete, mDeletePath);
+ mDeletePath = null;
+ }
+
+ public void playVideo(Activity activity, Uri uri, String title) {
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW)
+ .setDataAndType(uri, "video/*")
+ .putExtra(Intent.EXTRA_TITLE, title)
+ .putExtra(MovieActivity.KEY_TREAT_UP_AS_BACK, true);
+ activity.startActivityForResult(intent, REQUEST_PLAY_VIDEO);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(activity, activity.getString(R.string.video_err),
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void setCurrentPhotoByIntent(Intent intent) {
+ if (intent == null) return;
+ Path path = mApplication.getDataManager()
+ .findPathByUri(intent.getData(), intent.getType());
+ if (path != null) {
+ Path albumPath = mApplication.getDataManager().getDefaultSetOf(path);
+ if (!albumPath.equalsIgnoreCase(mOriginalSetPathString)) {
+ // If the edited image is stored in a different album, we need
+ // to start a new activity state to show the new image
+ Bundle data = new Bundle(getData());
+ data.putString(KEY_MEDIA_SET_PATH, albumPath.toString());
+ data.putString(PhotoPage.KEY_MEDIA_ITEM_PATH, path.toString());
+ mActivity.getStateManager().startState(SinglePhotoPage.class, data);
+ return;
+ }
+ mModel.setCurrentPhoto(path, mCurrentIndex);
+ }
+ }
+
+ @Override
+ protected void onStateResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == Activity.RESULT_CANCELED) {
+ // This is a reset, not a canceled
+ return;
+ }
+ if (resultCode == ProxyLauncher.RESULT_USER_CANCELED) {
+ // Unmap reset vs. canceled
+ resultCode = Activity.RESULT_CANCELED;
+ }
+ mRecenterCameraOnResume = false;
+ switch (requestCode) {
+ case REQUEST_EDIT:
+ setCurrentPhotoByIntent(data);
+ break;
+ case REQUEST_CROP:
+ if (resultCode == Activity.RESULT_OK) {
+ setCurrentPhotoByIntent(data);
+ }
+ break;
+ case REQUEST_CROP_PICASA: {
+ if (resultCode == Activity.RESULT_OK) {
+ Context context = mActivity.getAndroidContext();
+ String message = context.getString(R.string.crop_saved,
+ context.getString(R.string.folder_edited_online_photos));
+ Toast.makeText(context, 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;
+
+ mActivity.getGLRoot().unfreeze();
+ mHandler.removeMessages(MSG_UNFREEZE_GLROOT);
+
+ DetailsHelper.pause();
+ // Hide the detail dialog on exit
+ if (mShowDetails) hideDetails();
+ if (mModel != null) {
+ mModel.pause();
+ }
+ mPhotoView.pause();
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ mHandler.removeMessages(MSG_REFRESH_BOTTOM_CONTROLS);
+ refreshBottomControlsWhenReady();
+ mActionBar.removeOnMenuVisibilityListener(mMenuVisibilityListener);
+ if (mShowSpinner) {
+ mActionBar.disableAlbumModeMenu(true);
+ }
+ onCommitDeleteImage();
+ mMenuExecutor.pause();
+ if (mMediaSet != null) mMediaSet.clearDeletion();
+ }
+
+ @Override
+ public void onCurrentImageUpdated() {
+ mActivity.getGLRoot().unfreeze();
+ }
+
+ @Override
+ public void onFilmModeChanged(boolean enabled) {
+ refreshBottomControlsWhenReady();
+ if (mShowSpinner) {
+ if (enabled) {
+ mActionBar.enableAlbumModeMenu(
+ GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED, this);
+ } else {
+ mActionBar.disableAlbumModeMenu(true);
+ }
+ }
+ if (enabled) {
+ mHandler.removeMessages(MSG_HIDE_BARS);
+ UsageStatistics.onContentViewChanged(
+ UsageStatistics.COMPONENT_GALLERY, "FilmstripPage");
+ } else {
+ refreshHidingMessage();
+ if (mAppBridge == null || mCurrentIndex > 0) {
+ UsageStatistics.onContentViewChanged(
+ UsageStatistics.COMPONENT_GALLERY, "SinglePhotoPage");
+ } else {
+ UsageStatistics.onContentViewChanged(
+ UsageStatistics.COMPONENT_CAMERA, "Unknown"); // TODO
+ }
+ }
+ }
+
+ private void transitionFromAlbumPageIfNeeded() {
+ TransitionStore transitions = mActivity.getTransitionStore();
+
+ int albumPageTransition = transitions.get(
+ KEY_ALBUMPAGE_TRANSITION, MSG_ALBUMPAGE_NONE);
+
+ if (albumPageTransition == MSG_ALBUMPAGE_NONE && mAppBridge != null
+ && mRecenterCameraOnResume) {
+ // Generally, resuming the PhotoPage when in Camera should
+ // reset to the capture mode to allow quick photo taking
+ mCurrentIndex = 0;
+ mPhotoView.resetToFirstPicture();
+ } else {
+ int resumeIndex = transitions.get(KEY_INDEX_HINT, -1);
+ if (resumeIndex >= 0) {
+ if (mHasCameraScreennailOrPlaceholder) {
+ // Account for preview/placeholder being the first item
+ resumeIndex++;
+ }
+ if (resumeIndex < mMediaSet.getMediaItemCount()) {
+ mCurrentIndex = resumeIndex;
+ mModel.moveTo(mCurrentIndex);
+ }
+ }
+ }
+
+ if (albumPageTransition == MSG_ALBUMPAGE_RESUMED) {
+ mPhotoView.setFilmMode(mStartInFilmstrip || mAppBridge != null);
+ } else if (albumPageTransition == MSG_ALBUMPAGE_PICKED) {
+ mPhotoView.setFilmMode(false);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (mModel == null) {
+ mActivity.getStateManager().finishState(this);
+ return;
+ }
+ transitionFromAlbumPageIfNeeded();
+
+ mActivity.getGLRoot().freeze();
+ mIsActive = true;
+ setContentPane(mRootPane);
+
+ mModel.resume();
+ mPhotoView.resume();
+ mActionBar.setDisplayOptions(
+ ((mSecureAlbum == null) && (mSetPathString != null)), false);
+ mActionBar.addOnMenuVisibilityListener(mMenuVisibilityListener);
+ refreshBottomControlsWhenReady();
+ if (mShowSpinner && mPhotoView.getFilmMode()) {
+ mActionBar.enableAlbumModeMenu(
+ GalleryActionBar.ALBUM_FILMSTRIP_MODE_SELECTED, this);
+ }
+ if (!mShowBars) {
+ mActionBar.hide();
+ mActivity.getGLRoot().setLightsOutMode(true);
+ }
+ boolean haveImageEditor = GalleryUtils.isEditorAvailable(mActivity, "image/*");
+ if (haveImageEditor != mHaveImageEditor) {
+ mHaveImageEditor = haveImageEditor;
+ updateMenuOperations();
+ }
+
+ mRecenterCameraOnResume = true;
+ mHandler.sendEmptyMessageDelayed(MSG_UNFREEZE_GLROOT, UNFREEZE_GLROOT_TIMEOUT);
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mAppBridge != null) {
+ mAppBridge.setServer(null);
+ mScreenNailItem.setScreenNail(null);
+ mAppBridge.detachScreenNail();
+ mAppBridge = null;
+ mScreenNailSet = null;
+ mScreenNailItem = null;
+ }
+ mActivity.getGLRoot().setOrientationSource(null);
+ if (mBottomControls != null) mBottomControls.cleanup();
+
+ // Remove all pending messages.
+ mHandler.removeCallbacksAndMessages(null);
+ super.onDestroy();
+ }
+
+ private class MyDetailsSource implements DetailsSource {
+
+ @Override
+ public MediaDetails getDetails() {
+ return mModel.getMediaItem(0).getDetails();
+ }
+
+ @Override
+ public int size() {
+ return mMediaSet != null ? mMediaSet.getMediaItemCount() : 1;
+ }
+
+ @Override
+ public int setIndex() {
+ return mModel.getCurrentIndex();
+ }
+ }
+
+ @Override
+ public void onAlbumModeSelected(int mode) {
+ if (mode == GalleryActionBar.ALBUM_GRID_MODE_SELECTED) {
+ switchToGrid();
+ }
+ }
+
+ @Override
+ public void refreshBottomControlsWhenReady() {
+ if (mBottomControls == null) {
+ return;
+ }
+ MediaObject currentPhoto = mCurrentPhoto;
+ if (currentPhoto == null) {
+ mHandler.obtainMessage(MSG_REFRESH_BOTTOM_CONTROLS, 0, 0, currentPhoto).sendToTarget();
+ } else {
+ currentPhoto.getPanoramaSupport(mRefreshBottomControlsCallback);
+ }
+ }
+
+ private void updatePanoramaUI(boolean isPanorama360) {
+ Menu menu = mActionBar.getMenu();
+
+ // it could be null if onCreateActionBar has not been called yet
+ if (menu == null) {
+ return;
+ }
+
+ MenuExecutor.updateMenuForPanorama(menu, isPanorama360, isPanorama360);
+
+ if (isPanorama360) {
+ MenuItem item = menu.findItem(R.id.action_share);
+ if (item != null) {
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ item.setTitle(mActivity.getResources().getString(R.string.share_as_photo));
+ }
+ } else if ((mCurrentPhoto.getSupportedOperations() & MediaObject.SUPPORT_SHARE) != 0) {
+ MenuItem item = menu.findItem(R.id.action_share);
+ if (item != null) {
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ item.setTitle(mActivity.getResources().getString(R.string.share));
+ }
+ }
+ }
+
+ @Override
+ public void onUndoBarVisibilityChanged(boolean visible) {
+ refreshBottomControlsWhenReady();
+ }
+
+ @Override
+ public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) {
+ final long timestampMillis = mCurrentPhoto.getDateInMs();
+ final String mediaType = getMediaTypeString(mCurrentPhoto);
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_GALLERY,
+ UsageStatistics.ACTION_SHARE,
+ mediaType,
+ timestampMillis > 0
+ ? System.currentTimeMillis() - timestampMillis
+ : -1);
+ return false;
+ }
+
+ private static String getMediaTypeString(MediaItem item) {
+ if (item.getMediaType() == MediaObject.MEDIA_TYPE_VIDEO) {
+ return "Video";
+ } else if (item.getMediaType() == MediaObject.MEDIA_TYPE_IMAGE) {
+ return "Photo";
+ } else {
+ return "Unknown:" + item.getMediaType();
+ }
+ }
+
+}
diff --git a/src/com/android/gallery3d/app/PhotoPageBottomControls.java b/src/com/android/gallery3d/app/PhotoPageBottomControls.java
new file mode 100644
index 000000000..24b8ceb7e
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoPageBottomControls.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012 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.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.RelativeLayout;
+
+import com.android.gallery3d.R;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class PhotoPageBottomControls implements OnClickListener {
+ public interface Delegate {
+ public boolean canDisplayBottomControls();
+ public boolean canDisplayBottomControl(int control);
+ public void onBottomControlClicked(int control);
+ public void refreshBottomControlsWhenReady();
+ }
+
+ private Delegate mDelegate;
+ private ViewGroup mParentLayout;
+ private ViewGroup mContainer;
+
+ private boolean mContainerVisible = false;
+ private Map<View, Boolean> mControlsVisible = new HashMap<View, Boolean>();
+
+ private Animation mContainerAnimIn = new AlphaAnimation(0f, 1f);
+ private Animation mContainerAnimOut = new AlphaAnimation(1f, 0f);
+ private static final int CONTAINER_ANIM_DURATION_MS = 200;
+
+ private static final int CONTROL_ANIM_DURATION_MS = 150;
+ private static Animation getControlAnimForVisibility(boolean visible) {
+ Animation anim = visible ? new AlphaAnimation(0f, 1f)
+ : new AlphaAnimation(1f, 0f);
+ anim.setDuration(CONTROL_ANIM_DURATION_MS);
+ return anim;
+ }
+
+ public PhotoPageBottomControls(Delegate delegate, Context context, RelativeLayout layout) {
+ mDelegate = delegate;
+ mParentLayout = layout;
+
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mContainer = (ViewGroup) inflater
+ .inflate(R.layout.photopage_bottom_controls, mParentLayout, false);
+ mParentLayout.addView(mContainer);
+
+ for (int i = mContainer.getChildCount() - 1; i >= 0; i--) {
+ View child = mContainer.getChildAt(i);
+ child.setOnClickListener(this);
+ mControlsVisible.put(child, false);
+ }
+
+ mContainerAnimIn.setDuration(CONTAINER_ANIM_DURATION_MS);
+ mContainerAnimOut.setDuration(CONTAINER_ANIM_DURATION_MS);
+
+ mDelegate.refreshBottomControlsWhenReady();
+ }
+
+ private void hide() {
+ mContainer.clearAnimation();
+ mContainerAnimOut.reset();
+ mContainer.startAnimation(mContainerAnimOut);
+ mContainer.setVisibility(View.INVISIBLE);
+ }
+
+ private void show() {
+ mContainer.clearAnimation();
+ mContainerAnimIn.reset();
+ mContainer.startAnimation(mContainerAnimIn);
+ mContainer.setVisibility(View.VISIBLE);
+ }
+
+ public void refresh() {
+ boolean visible = mDelegate.canDisplayBottomControls();
+ boolean containerVisibilityChanged = (visible != mContainerVisible);
+ if (containerVisibilityChanged) {
+ if (visible) {
+ show();
+ } else {
+ hide();
+ }
+ mContainerVisible = visible;
+ }
+ if (!mContainerVisible) {
+ return;
+ }
+ for (View control : mControlsVisible.keySet()) {
+ Boolean prevVisibility = mControlsVisible.get(control);
+ boolean curVisibility = mDelegate.canDisplayBottomControl(control.getId());
+ if (prevVisibility.booleanValue() != curVisibility) {
+ if (!containerVisibilityChanged) {
+ control.clearAnimation();
+ control.startAnimation(getControlAnimForVisibility(curVisibility));
+ }
+ control.setVisibility(curVisibility ? View.VISIBLE : View.INVISIBLE);
+ mControlsVisible.put(control, curVisibility);
+ }
+ }
+ // Force a layout change
+ mContainer.requestLayout(); // Kick framework to draw the control.
+ }
+
+ public void cleanup() {
+ mParentLayout.removeView(mContainer);
+ mControlsVisible.clear();
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (mContainerVisible && mControlsVisible.get(view).booleanValue()) {
+ mDelegate.onBottomControlClicked(view.getId());
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/app/PhotoPageProgressBar.java b/src/com/android/gallery3d/app/PhotoPageProgressBar.java
new file mode 100644
index 000000000..141fea698
--- /dev/null
+++ b/src/com/android/gallery3d/app/PhotoPageProgressBar.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 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.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.RelativeLayout;
+
+import com.android.gallery3d.R;
+
+public class PhotoPageProgressBar {
+ private ViewGroup mContainer;
+ private View mProgress;
+
+ public PhotoPageProgressBar(Context context, RelativeLayout parentLayout) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mContainer = (ViewGroup) inflater.inflate(R.layout.photopage_progress_bar, parentLayout,
+ false);
+ parentLayout.addView(mContainer);
+ mProgress = mContainer.findViewById(R.id.photopage_progress_foreground);
+ }
+
+ public void setProgress(int progressPercent) {
+ mContainer.setVisibility(View.VISIBLE);
+ LayoutParams layoutParams = mProgress.getLayoutParams();
+ layoutParams.width = mContainer.getWidth() * progressPercent / 100;
+ mProgress.setLayoutParams(layoutParams);
+ }
+
+ public void hideProgress() {
+ mContainer.setVisibility(View.INVISIBLE);
+ }
+}
diff --git a/src/com/android/gallery3d/app/PickerActivity.java b/src/com/android/gallery3d/app/PickerActivity.java
new file mode 100644
index 000000000..d5bb218ea
--- /dev/null
+++ b/src/com/android/gallery3d/app/PickerActivity.java
@@ -0,0 +1,83 @@
+/*
+ * 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.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.Window;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.GLRootView;
+
+public class PickerActivity extends AbstractGalleryActivity
+ implements OnClickListener {
+
+ public static final String KEY_ALBUM_PATH = "album-path";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // We show the picker in two ways. One smaller screen we use a full
+ // screen window with an action bar. On larger screen we use a dialog.
+ boolean isDialog = getResources().getBoolean(R.bool.picker_is_dialog);
+
+ if (!isDialog) {
+ requestWindowFeature(Window.FEATURE_ACTION_BAR);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+ }
+
+ setContentView(R.layout.dialog_picker);
+
+ if (isDialog) {
+ // In dialog mode, we don't have the action bar to show the
+ // "cancel" action, so we show an additional "cancel" button.
+ View view = findViewById(R.id.cancel);
+ view.setOnClickListener(this);
+ view.setVisibility(View.VISIBLE);
+
+ // We need this, otherwise the view will be dimmed because it
+ // is "behind" the dialog.
+ ((GLRootView) findViewById(R.id.gl_root_view)).setZOrderOnTop(true);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.pickup, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.action_cancel) {
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.cancel) finish();
+ }
+}
diff --git a/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.java
new file mode 100644
index 000000000..00f2fe78f
--- /dev/null
+++ b/src/com/android/gallery3d/app/SinglePhotoDataAdapter.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.app;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+
+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.BitmapScreenNail;
+import com.android.gallery3d.ui.PhotoView;
+import com.android.gallery3d.ui.ScreenNail;
+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;
+
+public class SinglePhotoDataAdapter extends TileImageViewAdapter
+ implements PhotoPage.Model {
+
+ private static final String TAG = "SinglePhotoDataAdapter";
+ private static final int SIZE_BACKUP = 1024;
+ private static final int MSG_UPDATE_IMAGE = 1;
+
+ private MediaItem mItem;
+ private boolean mHasFullImage;
+ private Future<?> mTask;
+ private Handler mHandler;
+
+ private PhotoView mPhotoView;
+ private ThreadPool mThreadPool;
+ private int mLoadingState = LOADING_INIT;
+ private BitmapScreenNail mBitmapScreenNail;
+
+ public SinglePhotoDataAdapter(
+ AbstractGalleryActivity 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((ImageBundle) message.obj);
+ } else {
+ onDecodeThumbComplete((Future<Bitmap>) message.obj);
+ }
+ }
+ };
+ mThreadPool = activity.getThreadPool();
+ }
+
+ private static class ImageBundle {
+ public final BitmapRegionDecoder decoder;
+ public final Bitmap backupImage;
+
+ public ImageBundle(BitmapRegionDecoder decoder, Bitmap backupImage) {
+ this.decoder = decoder;
+ this.backupImage = backupImage;
+ }
+ }
+
+ private FutureListener<BitmapRegionDecoder> mLargeListener =
+ new FutureListener<BitmapRegionDecoder>() {
+ @Override
+ public void onFutureDone(Future<BitmapRegionDecoder> future) {
+ BitmapRegionDecoder decoder = future.get();
+ if (decoder == null) return;
+ int width = decoder.getWidth();
+ int height = decoder.getHeight();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = BitmapUtils.computeSampleSize(
+ (float) SIZE_BACKUP / Math.max(width, height));
+ Bitmap bitmap = decoder.decodeRegion(new Rect(0, 0, width, height), options);
+ mHandler.sendMessage(mHandler.obtainMessage(
+ MSG_UPDATE_IMAGE, new ImageBundle(decoder, bitmap)));
+ }
+ };
+
+ private FutureListener<Bitmap> mThumbListener =
+ new FutureListener<Bitmap>() {
+ @Override
+ public void onFutureDone(Future<Bitmap> future) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_UPDATE_IMAGE, future));
+ }
+ };
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ private void setScreenNail(Bitmap bitmap, int width, int height) {
+ mBitmapScreenNail = new BitmapScreenNail(bitmap);
+ setScreenNail(mBitmapScreenNail, width, height);
+ }
+
+ private void onDecodeLargeComplete(ImageBundle bundle) {
+ try {
+ setScreenNail(bundle.backupImage,
+ bundle.decoder.getWidth(), bundle.decoder.getHeight());
+ setRegionDecoder(bundle.decoder);
+ mPhotoView.notifyImageChange(0);
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode large", t);
+ }
+ }
+
+ private void onDecodeThumbComplete(Future<Bitmap> future) {
+ try {
+ Bitmap backup = future.get();
+ if (backup == null) {
+ mLoadingState = LOADING_FAIL;
+ return;
+ } else {
+ mLoadingState = LOADING_COMPLETE;
+ }
+ setScreenNail(backup, backup.getWidth(), backup.getHeight());
+ mPhotoView.notifyImageChange(0);
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode thumb", t);
+ }
+ }
+
+ @Override
+ public void resume() {
+ if (mTask == null) {
+ if (mHasFullImage) {
+ mTask = mThreadPool.submit(
+ mItem.requestLargeImage(), mLargeListener);
+ } else {
+ mTask = mThreadPool.submit(
+ mItem.requestImage(MediaItem.TYPE_THUMBNAIL),
+ mThumbListener);
+ }
+ }
+ }
+
+ @Override
+ public void pause() {
+ Future<?> task = mTask;
+ task.cancel();
+ task.waitDone();
+ if (task.get() == null) {
+ mTask = null;
+ }
+ if (mBitmapScreenNail != null) {
+ mBitmapScreenNail.recycle();
+ mBitmapScreenNail = null;
+ }
+ }
+
+ @Override
+ public void moveTo(int index) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void getImageSize(int offset, PhotoView.Size size) {
+ if (offset == 0) {
+ size.width = mItem.getWidth();
+ size.height = mItem.getHeight();
+ } else {
+ size.width = 0;
+ size.height = 0;
+ }
+ }
+
+ @Override
+ public int getImageRotation(int offset) {
+ return (offset == 0) ? mItem.getFullImageRotation() : 0;
+ }
+
+ @Override
+ public ScreenNail getScreenNail(int offset) {
+ return (offset == 0) ? getScreenNail() : null;
+ }
+
+ @Override
+ public void setNeedFullImage(boolean enabled) {
+ // currently not necessary.
+ }
+
+ @Override
+ public boolean isCamera(int offset) {
+ return false;
+ }
+
+ @Override
+ public boolean isPanorama(int offset) {
+ return false;
+ }
+
+ @Override
+ public boolean isStaticCamera(int offset) {
+ return false;
+ }
+
+ @Override
+ public boolean isVideo(int offset) {
+ return mItem.getMediaType() == MediaItem.MEDIA_TYPE_VIDEO;
+ }
+
+ @Override
+ public boolean isDeletable(int offset) {
+ return (mItem.getSupportedOperations() & MediaItem.SUPPORT_DELETE) != 0;
+ }
+
+ @Override
+ public MediaItem getMediaItem(int offset) {
+ return offset == 0 ? mItem : null;
+ }
+
+ @Override
+ public int getCurrentIndex() {
+ return 0;
+ }
+
+ @Override
+ public void setCurrentPhoto(Path path, int indexHint) {
+ // ignore
+ }
+
+ @Override
+ public void setFocusHintDirection(int direction) {
+ // ignore
+ }
+
+ @Override
+ public void setFocusHintPath(Path path) {
+ // ignore
+ }
+
+ @Override
+ public int getLoadingState(int offset) {
+ return mLoadingState;
+ }
+}
diff --git a/src/com/android/gallery3d/app/SinglePhotoPage.java b/src/com/android/gallery3d/app/SinglePhotoPage.java
new file mode 100644
index 000000000..beb87d358
--- /dev/null
+++ b/src/com/android/gallery3d/app/SinglePhotoPage.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2012 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 SinglePhotoPage extends PhotoPage {
+
+}
diff --git a/src/com/android/gallery3d/app/SlideshowDataAdapter.java b/src/com/android/gallery3d/app/SlideshowDataAdapter.java
new file mode 100644
index 000000000..7a0fba5fb
--- /dev/null
+++ b/src/com/android/gallery3d/app/SlideshowDataAdapter.java
@@ -0,0 +1,204 @@
+/*
+ * 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 android.graphics.Bitmap;
+
+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.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 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);
+ public int findItemIndex(Path path, int hint);
+ }
+
+ private final SlideshowSource mSource;
+
+ private int mLoadIndex = 0;
+ private int mNextOutput = 0;
+ private boolean mIsActive = false;
+ private boolean mNeedReset;
+ private boolean mDataReady;
+ private Path mInitialPath;
+
+ private final LinkedList<Slide> mImageQueue = new LinkedList<Slide>();
+
+ private Future<Void> 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();
+
+ // The index is just a hint if initialPath is set
+ public SlideshowDataAdapter(GalleryContext context, SlideshowSource source, int index,
+ Path initialPath) {
+ mSource = source;
+ mInitialPath = initialPath;
+ mLoadIndex = index;
+ mNextOutput = index;
+ mThreadPool = context.getThreadPool();
+ }
+
+ private MediaItem loadItem() {
+ if (mNeedReload.compareAndSet(true, false)) {
+ long v = mSource.reload();
+ if (v != mDataVersion) {
+ mDataVersion = v;
+ mNeedReset = true;
+ return null;
+ }
+ }
+ int index = mLoadIndex;
+ if (mInitialPath != null) {
+ index = mSource.findItemIndex(mInitialPath, index);
+ mInitialPath = null;
+ }
+ return mSource.getMediaItem(index);
+ }
+
+ private class ReloadTask implements Job<Void> {
+ @Override
+ 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 {
+ @Override
+ 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();
+ }
+
+ @Override
+ public Future<Slide> nextSlide(FutureListener<Slide> listener) {
+ return mThreadPool.submit(new Job<Slide>() {
+ @Override
+ public Slide run(JobContext jc) {
+ jc.setMode(ThreadPool.MODE_NONE);
+ return innerNextBitmap();
+ }
+ }, listener);
+ }
+
+ @Override
+ public void pause() {
+ synchronized (this) {
+ mIsActive = false;
+ notifyAll();
+ }
+ mSource.removeContentListener(mSourceListener);
+ mReloadTask.cancel();
+ mReloadTask.waitDone();
+ mReloadTask = null;
+ }
+
+ @Override
+ 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/SlideshowPage.java b/src/com/android/gallery3d/app/SlideshowPage.java
new file mode 100644
index 000000000..174058dc8
--- /dev/null
+++ b/src/com/android/gallery3d/app/SlideshowPage.java
@@ -0,0 +1,366 @@
+/*
+ * 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 android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.R;
+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.data.Path;
+import com.android.gallery3d.glrenderer.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 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";
+ public static final String KEY_DREAM = "dream";
+
+ 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<Slide> nextSlide(FutureListener<Slide> 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 final Intent mResultIntent = new Intent();
+
+ @Override
+ protected int getBackgroundColorId() {
+ return R.color.slideshow_background;
+ }
+
+ private final 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(getBackgroundColor());
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle data, Bundle restoreState) {
+ super.onCreate(data, restoreState);
+ mFlags |= (FLAG_HIDE_ACTION_BAR | FLAG_HIDE_STATUS_BAR);
+ if (data.getBoolean(KEY_DREAM)) {
+ // Dream screensaver only keeps screen on for plugged devices.
+ mFlags |= FLAG_SCREEN_ON_WHEN_PLUGGED | FLAG_SHOW_WHEN_LOCKED;
+ } else {
+ // User-initiated slideshow would always keep screen on.
+ mFlags |= FLAG_SCREEN_ON_ALWAYS;
+ }
+
+ 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<Slide>() {
+ @Override
+ public void onFutureDone(Future<Slide> 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();
+ mIsActive = false;
+ mModel.pause();
+ mSlideshowView.release();
+
+ mHandler.removeMessages(MSG_LOAD_NEXT_BITMAP);
+ mHandler.removeMessages(MSG_SHOW_PENDING_BITMAP);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mIsActive = true;
+ mModel.resume();
+
+ if (mPendingSlide != null) {
+ showPendingBitmap();
+ } else {
+ loadNextBitmap();
+ }
+ }
+
+ private void initializeData(Bundle data) {
+ boolean random = data.getBoolean(KEY_RANDOM_ORDER, false);
+
+ // We only want to show slideshow for images only, not videos.
+ String mediaPath = data.getString(KEY_SET_PATH);
+ mediaPath = FilterUtils.newFilterPath(mediaPath, FilterUtils.FILTER_IMAGE_ONLY);
+ MediaSet mediaSet = mActivity.getDataManager().getMediaSet(mediaPath);
+
+ if (random) {
+ boolean repeat = data.getBoolean(KEY_REPEAT);
+ mModel = new SlideshowDataAdapter(mActivity,
+ new ShuffleSource(mediaSet, repeat), 0, null);
+ setStateResult(Activity.RESULT_OK, mResultIntent.putExtra(KEY_PHOTO_INDEX, 0));
+ } else {
+ int index = data.getInt(KEY_PHOTO_INDEX);
+ String itemPath = data.getString(KEY_ITEM_PATH);
+ Path path = itemPath != null ? Path.fromString(itemPath) : null;
+ boolean repeat = data.getBoolean(KEY_REPEAT);
+ mModel = new SlideshowDataAdapter(mActivity, new SequentialSource(mediaSet, repeat),
+ index, path);
+ 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<MediaItem> 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 final 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;
+ }
+
+ @Override
+ public int findItemIndex(Path path, int hint) {
+ return hint;
+ }
+
+ @Override
+ public MediaItem getMediaItem(int index) {
+ if (!mRepeat && index >= mOrder.length) return null;
+ if (mOrder.length == 0) 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;
+ }
+
+ @Override
+ 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);
+ }
+ }
+
+ @Override
+ public void addContentListener(ContentListener listener) {
+ mMediaSet.addContentListener(listener);
+ }
+
+ @Override
+ public void removeContentListener(ContentListener listener) {
+ mMediaSet.removeContentListener(listener);
+ }
+ }
+
+ private static class SequentialSource implements SlideshowDataAdapter.SlideshowSource {
+ private static final int DATA_SIZE = 32;
+
+ private ArrayList<MediaItem> mData = new ArrayList<MediaItem>();
+ 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;
+ }
+
+ @Override
+ public int findItemIndex(Path path, int hint) {
+ return mMediaSet.getIndexOfItem(path, hint);
+ }
+
+ @Override
+ public MediaItem getMediaItem(int index) {
+ int dataEnd = mDataStart + mData.size();
+
+ if (mRepeat) {
+ int count = mMediaSet.getMediaItemCount();
+ if (count == 0) return null;
+ index = index % count;
+ }
+ 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);
+ }
+
+ @Override
+ public long reload() {
+ long version = mMediaSet.reload();
+ if (version != mDataVersion) {
+ mDataVersion = version;
+ mData.clear();
+ }
+ return mDataVersion;
+ }
+
+ @Override
+ public void addContentListener(ContentListener listener) {
+ mMediaSet.addContentListener(listener);
+ }
+
+ @Override
+ 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..53c3fc228
--- /dev/null
+++ b/src/com/android/gallery3d/app/StateManager.java
@@ -0,0 +1,339 @@
+/*
+ * 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 android.app.Activity;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.camera.CameraActivity;
+import com.android.gallery3d.anim.StateTransitionAnimation;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.UsageStatistics;
+
+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 AbstractGalleryActivity mActivity;
+ private Stack<StateEntry> mStack = new Stack<StateEntry>();
+ private ActivityState.ResultEntry mResult;
+
+ public StateManager(AbstractGalleryActivity activity) {
+ mActivity = activity;
+ }
+
+ public void startState(Class<? extends ActivityState> 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();
+ top.transitionOnNextPause(top.getClass(), klass,
+ StateTransitionAnimation.Transition.Incoming);
+ if (mIsResumed) top.onPause();
+ }
+
+ UsageStatistics.onContentViewChanged(
+ UsageStatistics.COMPONENT_GALLERY,
+ klass.getSimpleName());
+ state.initialize(mActivity, data);
+
+ mStack.push(new StateEntry(data, state));
+ state.onCreate(data, null);
+ if (mIsResumed) state.resume();
+ }
+
+ public void startStateForResult(Class<? extends ActivityState> 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(mActivity, data);
+ state.mResult = new ActivityState.ResultEntry();
+ state.mResult.requestCode = requestCode;
+
+ if (!mStack.isEmpty()) {
+ ActivityState as = getTopState();
+ as.transitionOnNextPause(as.getClass(), klass,
+ StateTransitionAnimation.Transition.Incoming);
+ as.mReceivedResults = state.mResult;
+ if (mIsResumed) as.onPause();
+ } else {
+ mResult = state.mResult;
+ }
+ UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY,
+ klass.getSimpleName());
+ mStack.push(new StateEntry(data, state));
+ state.onCreate(data, null);
+ if (mIsResumed) state.resume();
+ }
+
+ public boolean createOptionsMenu(Menu menu) {
+ if (mStack.isEmpty()) {
+ return false;
+ } else {
+ return getTopState().onCreateActionBar(menu);
+ }
+ }
+
+ public void onConfigurationChange(Configuration config) {
+ for (StateEntry entry : mStack) {
+ entry.activityState.onConfigurationChanged(config);
+ }
+ }
+
+ 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 void clearActivityResult() {
+ if (!mStack.isEmpty()) {
+ getTopState().clearStateResult();
+ }
+ }
+
+ public int getStateCount() {
+ return mStack.size();
+ }
+
+ public boolean itemSelected(MenuItem item) {
+ if (!mStack.isEmpty()) {
+ if (getTopState().onItemSelected(item)) return true;
+ if (item.getItemId() == android.R.id.home) {
+ if (mStack.size() > 1) {
+ getTopState().onBackPressed();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void onBackPressed() {
+ if (!mStack.isEmpty()) {
+ getTopState().onBackPressed();
+ }
+ }
+
+ void finishState(ActivityState state) {
+ finishState(state, true);
+ }
+
+ public void clearTasks() {
+ // Remove all the states that are on top of the bottom PhotoPage state
+ while (mStack.size() > 1) {
+ mStack.pop().activityState.onDestroy();
+ }
+ }
+
+ void finishState(ActivityState state, boolean fireOnPause) {
+ // The finish() request could be rejected (only happens under Monkey),
+ // If it is rejected, we won't close the last page.
+ if (mStack.size() == 1) {
+ Activity activity = (Activity) mActivity.getAndroidContext();
+ if (mResult != null) {
+ activity.setResult(mResult.resultCode, mResult.resultData);
+ }
+ activity.finish();
+ if (!activity.isFinishing()) {
+ Log.w(TAG, "finish is rejected, keep the last state");
+ return;
+ }
+ Log.v(TAG, "no more state, finish activity");
+ }
+
+ Log.v(TAG, "finishState " + state);
+ if (state != mStack.peek().activityState) {
+ if (state.isDestroyed()) {
+ Log.d(TAG, "The state is already destroyed");
+ return;
+ } else {
+ 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();
+ state.mIsFinishing = true;
+ ActivityState top = !mStack.isEmpty() ? mStack.peek().activityState : null;
+ if (mIsResumed && fireOnPause) {
+ if (top != null) {
+ state.transitionOnNextPause(state.getClass(), top.getClass(),
+ StateTransitionAnimation.Transition.Outgoing);
+ }
+ state.onPause();
+ }
+ mActivity.getGLRoot().setContentPane(null);
+ state.onDestroy();
+
+ if (top != null && mIsResumed) top.resume();
+ if (top != null) {
+ UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY,
+ top.getClass().getSimpleName());
+ }
+ }
+
+ public void switchState(ActivityState oldState,
+ Class<? extends ActivityState> 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 (!data.containsKey(PhotoPage.KEY_APP_BRIDGE)) {
+ // Do not do the fade out stuff when we are switching camera modes
+ oldState.transitionOnNextPause(oldState.getClass(), klass,
+ StateTransitionAnimation.Transition.Incoming);
+ }
+ 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(mActivity, data);
+ mStack.push(new StateEntry(data, state));
+ state.onCreate(data, null);
+ if (mIsResumed) state.resume();
+ UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY,
+ klass.getSimpleName());
+ }
+
+ 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);
+ ActivityState topState = null;
+ for (Parcelable parcelable : list) {
+ Bundle bundle = (Bundle) parcelable;
+ Class<? extends ActivityState> klass =
+ (Class<? extends ActivityState>) 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(mActivity, data);
+ activityState.onCreate(data, state);
+ mStack.push(new StateEntry(data, activityState));
+ topState = activityState;
+ }
+ if (topState != null) {
+ UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_GALLERY,
+ topState.getClass().getSimpleName());
+ }
+ }
+
+ 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<? extends ActivityState> 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/StitchingChangeListener.java b/src/com/android/gallery3d/app/StitchingChangeListener.java
new file mode 100644
index 000000000..0b8c2d6d6
--- /dev/null
+++ b/src/com/android/gallery3d/app/StitchingChangeListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2012 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.net.Uri;
+
+public interface StitchingChangeListener {
+ public void onStitchingQueued(Uri uri);
+
+ public void onStitchingResult(Uri uri);
+
+ public void onStitchingProgress(Uri uri, int progress);
+}
diff --git a/src/com/android/gallery3d/app/TimeBar.java b/src/com/android/gallery3d/app/TimeBar.java
new file mode 100644
index 000000000..246346a56
--- /dev/null
+++ b/src/com/android/gallery3d/app/TimeBar.java
@@ -0,0 +1,266 @@
+/*
+ * 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.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+
+/**
+ * The time bar view, which includes the current and total time, the progress
+ * bar, and the scrubber.
+ */
+public class TimeBar extends View {
+
+ public interface Listener {
+ void onScrubbingStart();
+
+ void onScrubbingMove(int time);
+
+ void onScrubbingEnd(int time, int start, int end);
+ }
+
+ // Padding around the scrubber to increase its touch target
+ private static final int SCRUBBER_PADDING_IN_DP = 10;
+
+ // The total padding, top plus bottom
+ private static final int V_PADDING_IN_DP = 30;
+
+ private static final int TEXT_SIZE_IN_DP = 14;
+
+ protected final Listener mListener;
+
+ // the bars we use for displaying the progress
+ protected final Rect mProgressBar;
+ protected final Rect mPlayedBar;
+
+ protected final Paint mProgressPaint;
+ protected final Paint mPlayedPaint;
+ protected final Paint mTimeTextPaint;
+
+ protected final Bitmap mScrubber;
+ protected int mScrubberPadding; // adds some touch tolerance around the
+ // scrubber
+
+ protected int mScrubberLeft;
+ protected int mScrubberTop;
+ protected int mScrubberCorrection;
+ protected boolean mScrubbing;
+ protected boolean mShowTimes;
+ protected boolean mShowScrubber;
+
+ protected int mTotalTime;
+ protected int mCurrentTime;
+
+ protected final Rect mTimeBounds;
+
+ protected int mVPaddingInPx;
+
+ public TimeBar(Context context, Listener listener) {
+ super(context);
+ mListener = Utils.checkNotNull(listener);
+
+ mShowTimes = true;
+ mShowScrubber = true;
+
+ mProgressBar = new Rect();
+ mPlayedBar = new Rect();
+
+ mProgressPaint = new Paint();
+ mProgressPaint.setColor(0xFF808080);
+ mPlayedPaint = new Paint();
+ mPlayedPaint.setColor(0xFFFFFFFF);
+
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ float textSizeInPx = metrics.density * TEXT_SIZE_IN_DP;
+ mTimeTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ mTimeTextPaint.setColor(0xFFCECECE);
+ mTimeTextPaint.setTextSize(textSizeInPx);
+ mTimeTextPaint.setTextAlign(Paint.Align.CENTER);
+
+ mTimeBounds = new Rect();
+ mTimeTextPaint.getTextBounds("0:00:00", 0, 7, mTimeBounds);
+
+ mScrubber = BitmapFactory.decodeResource(getResources(), R.drawable.scrubber_knob);
+ mScrubberPadding = (int) (metrics.density * SCRUBBER_PADDING_IN_DP);
+
+ mVPaddingInPx = (int) (metrics.density * V_PADDING_IN_DP);
+ }
+
+ private void update() {
+ mPlayedBar.set(mProgressBar);
+
+ if (mTotalTime > 0) {
+ mPlayedBar.right =
+ mPlayedBar.left + (int) ((mProgressBar.width() * (long) mCurrentTime) / mTotalTime);
+ } else {
+ mPlayedBar.right = mProgressBar.left;
+ }
+
+ if (!mScrubbing) {
+ mScrubberLeft = mPlayedBar.right - mScrubber.getWidth() / 2;
+ }
+ invalidate();
+ }
+
+ /**
+ * @return the preferred height of this view, including invisible padding
+ */
+ public int getPreferredHeight() {
+ return mTimeBounds.height() + mVPaddingInPx + mScrubberPadding;
+ }
+
+ /**
+ * @return the height of the time bar, excluding invisible padding
+ */
+ public int getBarHeight() {
+ return mTimeBounds.height() + mVPaddingInPx;
+ }
+
+ public void setTime(int currentTime, int totalTime,
+ int trimStartTime, int trimEndTime) {
+ if (mCurrentTime == currentTime && mTotalTime == totalTime) {
+ return;
+ }
+ mCurrentTime = currentTime;
+ mTotalTime = totalTime;
+ update();
+ }
+
+ private boolean inScrubber(float x, float y) {
+ int scrubberRight = mScrubberLeft + mScrubber.getWidth();
+ int scrubberBottom = mScrubberTop + mScrubber.getHeight();
+ return mScrubberLeft - mScrubberPadding < x && x < scrubberRight + mScrubberPadding
+ && mScrubberTop - mScrubberPadding < y && y < scrubberBottom + mScrubberPadding;
+ }
+
+ private void clampScrubber() {
+ int half = mScrubber.getWidth() / 2;
+ int max = mProgressBar.right - half;
+ int min = mProgressBar.left - half;
+ mScrubberLeft = Math.min(max, Math.max(min, mScrubberLeft));
+ }
+
+ private int getScrubberTime() {
+ return (int) ((long) (mScrubberLeft + mScrubber.getWidth() / 2 - mProgressBar.left)
+ * mTotalTime / mProgressBar.width());
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int w = r - l;
+ int h = b - t;
+ if (!mShowTimes && !mShowScrubber) {
+ mProgressBar.set(0, 0, w, h);
+ } else {
+ int margin = mScrubber.getWidth() / 3;
+ if (mShowTimes) {
+ margin += mTimeBounds.width();
+ }
+ int progressY = (h + mScrubberPadding) / 2;
+ mScrubberTop = progressY - mScrubber.getHeight() / 2 + 1;
+ mProgressBar.set(
+ getPaddingLeft() + margin, progressY,
+ w - getPaddingRight() - margin, progressY + 4);
+ }
+ update();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // draw progress bars
+ canvas.drawRect(mProgressBar, mProgressPaint);
+ canvas.drawRect(mPlayedBar, mPlayedPaint);
+
+ // draw scrubber and timers
+ if (mShowScrubber) {
+ canvas.drawBitmap(mScrubber, mScrubberLeft, mScrubberTop, null);
+ }
+ if (mShowTimes) {
+ canvas.drawText(
+ stringForTime(mCurrentTime),
+ mTimeBounds.width() / 2 + getPaddingLeft(),
+ mTimeBounds.height() + mVPaddingInPx / 2 + mScrubberPadding + 1,
+ mTimeTextPaint);
+ canvas.drawText(
+ stringForTime(mTotalTime),
+ getWidth() - getPaddingRight() - mTimeBounds.width() / 2,
+ mTimeBounds.height() + mVPaddingInPx / 2 + mScrubberPadding + 1,
+ mTimeTextPaint);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mShowScrubber) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ mScrubberCorrection = inScrubber(x, y)
+ ? x - mScrubberLeft
+ : mScrubber.getWidth() / 2;
+ mScrubbing = true;
+ mListener.onScrubbingStart();
+ }
+ // fall-through
+ case MotionEvent.ACTION_MOVE: {
+ mScrubberLeft = x - mScrubberCorrection;
+ clampScrubber();
+ mCurrentTime = getScrubberTime();
+ mListener.onScrubbingMove(mCurrentTime);
+ invalidate();
+ return true;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ mListener.onScrubbingEnd(getScrubberTime(), 0, 0);
+ mScrubbing = false;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ protected String stringForTime(long millis) {
+ int totalSeconds = (int) millis / 1000;
+ int seconds = totalSeconds % 60;
+ int minutes = (totalSeconds / 60) % 60;
+ int hours = totalSeconds / 3600;
+ if (hours > 0) {
+ return String.format("%d:%02d:%02d", hours, minutes, seconds).toString();
+ } else {
+ return String.format("%02d:%02d", minutes, seconds).toString();
+ }
+ }
+
+ public void setSeekable(boolean canSeek) {
+ mShowScrubber = canSeek;
+ }
+
+}
diff --git a/src/com/android/gallery3d/app/TransitionStore.java b/src/com/android/gallery3d/app/TransitionStore.java
new file mode 100644
index 000000000..aa38ed77e
--- /dev/null
+++ b/src/com/android/gallery3d/app/TransitionStore.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 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.HashMap;
+
+public class TransitionStore {
+ private HashMap<Object, Object> mStorage = new HashMap<Object, Object>();
+
+ public void put(Object key, Object value) {
+ mStorage.put(key, value);
+ }
+
+ public <T> void putIfNotPresent(Object key, T valueIfNull) {
+ mStorage.put(key, get(key, valueIfNull));
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> T get(Object key) {
+ return (T) mStorage.get(key);
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> T get(Object key, T valueIfNull) {
+ T value = (T) mStorage.get(key);
+ return value == null ? valueIfNull : value;
+ }
+
+ public void clear() {
+ mStorage.clear();
+ }
+}
diff --git a/src/com/android/gallery3d/app/TrimControllerOverlay.java b/src/com/android/gallery3d/app/TrimControllerOverlay.java
new file mode 100644
index 000000000..cae016626
--- /dev/null
+++ b/src/com/android/gallery3d/app/TrimControllerOverlay.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2012 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.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.common.ApiHelper;
+
+/**
+ * The controller for the Trimming Video.
+ */
+public class TrimControllerOverlay extends CommonControllerOverlay {
+
+ public TrimControllerOverlay(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void createTimeBar(Context context) {
+ mTimeBar = new TrimTimeBar(context, this);
+ }
+
+ private void hidePlayButtonIfPlaying() {
+ if (mState == State.PLAYING) {
+ mPlayPauseReplayView.setVisibility(View.INVISIBLE);
+ }
+ if (ApiHelper.HAS_OBJECT_ANIMATION) {
+ mPlayPauseReplayView.setAlpha(1f);
+ }
+ }
+
+ @Override
+ public void showPlaying() {
+ super.showPlaying();
+ if (ApiHelper.HAS_OBJECT_ANIMATION) {
+ // Add animation to hide the play button while playing.
+ ObjectAnimator anim = ObjectAnimator.ofFloat(mPlayPauseReplayView, "alpha", 1f, 0f);
+ anim.setDuration(200);
+ anim.start();
+ anim.addListener(new AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ hidePlayButtonIfPlaying();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ hidePlayButtonIfPlaying();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+ } else {
+ hidePlayButtonIfPlaying();
+ }
+ }
+
+ @Override
+ public void setTimes(int currentTime, int totalTime, int trimStartTime, int trimEndTime) {
+ mTimeBar.setTime(currentTime, totalTime, trimStartTime, trimEndTime);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (super.onTouchEvent(event)) {
+ return true;
+ }
+
+ // The special thing here is that the State.ENDED include both cases of
+ // the video completed and current == trimEnd. Both request a replay.
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (mState == State.PLAYING || mState == State.PAUSED) {
+ mListener.onPlayPause();
+ } else if (mState == State.ENDED) {
+ if (mCanReplay) {
+ mListener.onReplay();
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ break;
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/app/TrimTimeBar.java b/src/com/android/gallery3d/app/TrimTimeBar.java
new file mode 100644
index 000000000..f8dbc749e
--- /dev/null
+++ b/src/com/android/gallery3d/app/TrimTimeBar.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2012 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.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.R;
+
+/**
+ * The trim time bar view, which includes the current and total time, the progress
+ * bar, and the scrubbers for current time, start and end time for trimming.
+ */
+public class TrimTimeBar extends TimeBar {
+
+ public static final int SCRUBBER_NONE = 0;
+ public static final int SCRUBBER_START = 1;
+ public static final int SCRUBBER_CURRENT = 2;
+ public static final int SCRUBBER_END = 3;
+
+ private int mPressedThumb = SCRUBBER_NONE;
+
+ // On touch event, the setting order is Scrubber Position -> Time ->
+ // PlayedBar. At the setTimes(), activity can update the Time directly, then
+ // PlayedBar will be updated too.
+ private int mTrimStartScrubberLeft;
+ private int mTrimEndScrubberLeft;
+
+ private int mTrimStartScrubberTop;
+ private int mTrimEndScrubberTop;
+
+ private int mTrimStartTime;
+ private int mTrimEndTime;
+
+ private final Bitmap mTrimStartScrubber;
+ private final Bitmap mTrimEndScrubber;
+ public TrimTimeBar(Context context, Listener listener) {
+ super(context, listener);
+
+ mTrimStartTime = 0;
+ mTrimEndTime = 0;
+ mTrimStartScrubberLeft = 0;
+ mTrimEndScrubberLeft = 0;
+ mTrimStartScrubberTop = 0;
+ mTrimEndScrubberTop = 0;
+
+ mTrimStartScrubber = BitmapFactory.decodeResource(getResources(),
+ R.drawable.text_select_handle_left);
+ mTrimEndScrubber = BitmapFactory.decodeResource(getResources(),
+ R.drawable.text_select_handle_right);
+ // Increase the size of this trimTimeBar, but minimize the scrubber
+ // touch padding since we have 3 scrubbers now.
+ mScrubberPadding = 0;
+ mVPaddingInPx = mVPaddingInPx * 3 / 2;
+ }
+
+ private int getBarPosFromTime(int time) {
+ return mProgressBar.left +
+ (int) ((mProgressBar.width() * (long) time) / mTotalTime);
+ }
+
+ private int trimStartScrubberTipOffset() {
+ return mTrimStartScrubber.getWidth() * 3 / 4;
+ }
+
+ private int trimEndScrubberTipOffset() {
+ return mTrimEndScrubber.getWidth() / 4;
+ }
+
+ // Based on all the time info (current, total, trimStart, trimEnd), we
+ // decide the playedBar size.
+ private void updatePlayedBarAndScrubberFromTime() {
+ // According to the Time, update the Played Bar
+ mPlayedBar.set(mProgressBar);
+ if (mTotalTime > 0) {
+ // set playedBar according to the trim time.
+ mPlayedBar.left = getBarPosFromTime(mTrimStartTime);
+ mPlayedBar.right = getBarPosFromTime(mCurrentTime);
+ if (!mScrubbing) {
+ mScrubberLeft = mPlayedBar.right - mScrubber.getWidth() / 2;
+ mTrimStartScrubberLeft = mPlayedBar.left - trimStartScrubberTipOffset();
+ mTrimEndScrubberLeft = getBarPosFromTime(mTrimEndTime)
+ - trimEndScrubberTipOffset();
+ }
+ } else {
+ // If the video is not prepared, just show the scrubber at the end
+ // of progressBar
+ mPlayedBar.right = mProgressBar.left;
+ mScrubberLeft = mProgressBar.left - mScrubber.getWidth() / 2;
+ mTrimStartScrubberLeft = mProgressBar.left - trimStartScrubberTipOffset();
+ mTrimEndScrubberLeft = mProgressBar.right - trimEndScrubberTipOffset();
+ }
+ }
+
+ private void initTrimTimeIfNeeded() {
+ if (mTotalTime > 0 && mTrimEndTime == 0) {
+ mTrimEndTime = mTotalTime;
+ }
+ }
+
+ private void update() {
+ initTrimTimeIfNeeded();
+ updatePlayedBarAndScrubberFromTime();
+ invalidate();
+ }
+
+ @Override
+ public void setTime(int currentTime, int totalTime,
+ int trimStartTime, int trimEndTime) {
+ if (mCurrentTime == currentTime && mTotalTime == totalTime
+ && mTrimStartTime == trimStartTime && mTrimEndTime == trimEndTime) {
+ return;
+ }
+ mCurrentTime = currentTime;
+ mTotalTime = totalTime;
+ mTrimStartTime = trimStartTime;
+ mTrimEndTime = trimEndTime;
+ update();
+ }
+
+ private int whichScrubber(float x, float y) {
+ if (inScrubber(x, y, mTrimStartScrubberLeft, mTrimStartScrubberTop, mTrimStartScrubber)) {
+ return SCRUBBER_START;
+ } else if (inScrubber(x, y, mTrimEndScrubberLeft, mTrimEndScrubberTop, mTrimEndScrubber)) {
+ return SCRUBBER_END;
+ } else if (inScrubber(x, y, mScrubberLeft, mScrubberTop, mScrubber)) {
+ return SCRUBBER_CURRENT;
+ }
+ return SCRUBBER_NONE;
+ }
+
+ private boolean inScrubber(float x, float y, int startX, int startY, Bitmap scrubber) {
+ int scrubberRight = startX + scrubber.getWidth();
+ int scrubberBottom = startY + scrubber.getHeight();
+ return startX < x && x < scrubberRight && startY < y && y < scrubberBottom;
+ }
+
+ private int clampScrubber(int scrubberLeft, int offset, int lowerBound, int upperBound) {
+ int max = upperBound - offset;
+ int min = lowerBound - offset;
+ return Math.min(max, Math.max(min, scrubberLeft));
+ }
+
+ private int getScrubberTime(int scrubberLeft, int offset) {
+ return (int) ((long) (scrubberLeft + offset - mProgressBar.left)
+ * mTotalTime / mProgressBar.width());
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ int w = r - l;
+ int h = b - t;
+ if (!mShowTimes && !mShowScrubber) {
+ mProgressBar.set(0, 0, w, h);
+ } else {
+ int margin = mScrubber.getWidth() / 3;
+ if (mShowTimes) {
+ margin += mTimeBounds.width();
+ }
+ int progressY = h / 4;
+ int scrubberY = progressY - mScrubber.getHeight() / 2 + 1;
+ mScrubberTop = scrubberY;
+ mTrimStartScrubberTop = progressY;
+ mTrimEndScrubberTop = progressY;
+ mProgressBar.set(
+ getPaddingLeft() + margin, progressY,
+ w - getPaddingRight() - margin, progressY + 4);
+ }
+ update();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // draw progress bars
+ canvas.drawRect(mProgressBar, mProgressPaint);
+ canvas.drawRect(mPlayedBar, mPlayedPaint);
+
+ if (mShowTimes) {
+ canvas.drawText(
+ stringForTime(mCurrentTime),
+ mTimeBounds.width() / 2 + getPaddingLeft(),
+ mTimeBounds.height() / 2 + mTrimStartScrubberTop,
+ mTimeTextPaint);
+ canvas.drawText(
+ stringForTime(mTotalTime),
+ getWidth() - getPaddingRight() - mTimeBounds.width() / 2,
+ mTimeBounds.height() / 2 + mTrimStartScrubberTop,
+ mTimeTextPaint);
+ }
+
+ // draw extra scrubbers
+ if (mShowScrubber) {
+ canvas.drawBitmap(mScrubber, mScrubberLeft, mScrubberTop, null);
+ canvas.drawBitmap(mTrimStartScrubber, mTrimStartScrubberLeft,
+ mTrimStartScrubberTop, null);
+ canvas.drawBitmap(mTrimEndScrubber, mTrimEndScrubberLeft,
+ mTrimEndScrubberTop, null);
+ }
+ }
+
+ private void updateTimeFromPos() {
+ mCurrentTime = getScrubberTime(mScrubberLeft, mScrubber.getWidth() / 2);
+ mTrimStartTime = getScrubberTime(mTrimStartScrubberLeft, trimStartScrubberTipOffset());
+ mTrimEndTime = getScrubberTime(mTrimEndScrubberLeft, trimEndScrubberTipOffset());
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mShowScrubber) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mPressedThumb = whichScrubber(x, y);
+ switch (mPressedThumb) {
+ case SCRUBBER_NONE:
+ break;
+ case SCRUBBER_CURRENT:
+ mScrubbing = true;
+ mScrubberCorrection = x - mScrubberLeft;
+ break;
+ case SCRUBBER_START:
+ mScrubbing = true;
+ mScrubberCorrection = x - mTrimStartScrubberLeft;
+ break;
+ case SCRUBBER_END:
+ mScrubbing = true;
+ mScrubberCorrection = x - mTrimEndScrubberLeft;
+ break;
+ }
+ if (mScrubbing == true) {
+ mListener.onScrubbingStart();
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (mScrubbing) {
+ int seekToTime = -1;
+ int lowerBound = mTrimStartScrubberLeft + trimStartScrubberTipOffset();
+ int upperBound = mTrimEndScrubberLeft + trimEndScrubberTipOffset();
+ switch (mPressedThumb) {
+ case SCRUBBER_CURRENT:
+ mScrubberLeft = x - mScrubberCorrection;
+ mScrubberLeft =
+ clampScrubber(mScrubberLeft,
+ mScrubber.getWidth() / 2,
+ lowerBound, upperBound);
+ seekToTime = getScrubberTime(mScrubberLeft,
+ mScrubber.getWidth() / 2);
+ break;
+ case SCRUBBER_START:
+ mTrimStartScrubberLeft = x - mScrubberCorrection;
+ // Limit start <= end
+ if (mTrimStartScrubberLeft > mTrimEndScrubberLeft) {
+ mTrimStartScrubberLeft = mTrimEndScrubberLeft;
+ }
+ lowerBound = mProgressBar.left;
+ mTrimStartScrubberLeft =
+ clampScrubber(mTrimStartScrubberLeft,
+ trimStartScrubberTipOffset(),
+ lowerBound, upperBound);
+ seekToTime = getScrubberTime(mTrimStartScrubberLeft,
+ trimStartScrubberTipOffset());
+ break;
+ case SCRUBBER_END:
+ mTrimEndScrubberLeft = x - mScrubberCorrection;
+ upperBound = mProgressBar.right;
+ mTrimEndScrubberLeft =
+ clampScrubber(mTrimEndScrubberLeft,
+ trimEndScrubberTipOffset(),
+ lowerBound, upperBound);
+ seekToTime = getScrubberTime(mTrimEndScrubberLeft,
+ trimEndScrubberTipOffset());
+ break;
+ }
+ updateTimeFromPos();
+ updatePlayedBarAndScrubberFromTime();
+ if (seekToTime != -1) {
+ mListener.onScrubbingMove(seekToTime);
+ }
+ invalidate();
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ if (mScrubbing) {
+ int seekToTime = 0;
+ switch (mPressedThumb) {
+ case SCRUBBER_CURRENT:
+ seekToTime = getScrubberTime(mScrubberLeft,
+ mScrubber.getWidth() / 2);
+ break;
+ case SCRUBBER_START:
+ seekToTime = getScrubberTime(mTrimStartScrubberLeft,
+ trimStartScrubberTipOffset());
+ mScrubberLeft = mTrimStartScrubberLeft +
+ trimStartScrubberTipOffset() - mScrubber.getWidth() / 2;
+ break;
+ case SCRUBBER_END:
+ seekToTime = getScrubberTime(mTrimEndScrubberLeft,
+ trimEndScrubberTipOffset());
+ mScrubberLeft = mTrimEndScrubberLeft +
+ trimEndScrubberTipOffset() - mScrubber.getWidth() / 2;
+ break;
+ }
+ updateTimeFromPos();
+ mListener.onScrubbingEnd(seekToTime,
+ getScrubberTime(mTrimStartScrubberLeft,
+ trimStartScrubberTipOffset()),
+ getScrubberTime(mTrimEndScrubberLeft, trimEndScrubberTipOffset()));
+ mScrubbing = false;
+ mPressedThumb = SCRUBBER_NONE;
+ return true;
+ }
+ break;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/app/TrimVideo.java b/src/com/android/gallery3d/app/TrimVideo.java
new file mode 100644
index 000000000..1e7728162
--- /dev/null
+++ b/src/com/android/gallery3d/app/TrimVideo.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2012 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.ActionBar;
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.VideoView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.SaveVideoFileInfo;
+import com.android.gallery3d.util.SaveVideoFileUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+public class TrimVideo extends Activity implements
+ MediaPlayer.OnErrorListener,
+ MediaPlayer.OnCompletionListener,
+ ControllerOverlay.Listener {
+
+ private VideoView mVideoView;
+ private TextView mSaveVideoTextView;
+ private TrimControllerOverlay mController;
+ private Context mContext;
+ private Uri mUri;
+ private final Handler mHandler = new Handler();
+ public static final String TRIM_ACTION = "com.android.camera.action.TRIM";
+
+ public ProgressDialog mProgress;
+
+ private int mTrimStartTime = 0;
+ private int mTrimEndTime = 0;
+ private int mVideoPosition = 0;
+ public static final String KEY_TRIM_START = "trim_start";
+ public static final String KEY_TRIM_END = "trim_end";
+ public static final String KEY_VIDEO_POSITION = "video_pos";
+ private boolean mHasPaused = false;
+
+ private String mSrcVideoPath = null;
+ private static final String TIME_STAMP_NAME = "'TRIM'_yyyyMMdd_HHmmss";
+ private SaveVideoFileInfo mDstFileInfo = null;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mContext = getApplicationContext();
+ super.onCreate(savedInstanceState);
+
+ requestWindowFeature(Window.FEATURE_ACTION_BAR);
+ requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
+
+ ActionBar actionBar = getActionBar();
+ int displayOptions = ActionBar.DISPLAY_SHOW_HOME;
+ actionBar.setDisplayOptions(0, displayOptions);
+ displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM;
+ actionBar.setDisplayOptions(displayOptions, displayOptions);
+ actionBar.setCustomView(R.layout.trim_menu);
+
+ mSaveVideoTextView = (TextView) findViewById(R.id.start_trim);
+ mSaveVideoTextView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ trimVideo();
+ }
+ });
+ mSaveVideoTextView.setEnabled(false);
+
+ Intent intent = getIntent();
+ mUri = intent.getData();
+ mSrcVideoPath = intent.getStringExtra(PhotoPage.KEY_MEDIA_ITEM_PATH);
+ setContentView(R.layout.trim_view);
+ View rootView = findViewById(R.id.trim_view_root);
+
+ mVideoView = (VideoView) rootView.findViewById(R.id.surface_view);
+
+ mController = new TrimControllerOverlay(mContext);
+ ((ViewGroup) rootView).addView(mController.getView());
+ mController.setListener(this);
+ mController.setCanReplay(true);
+
+ mVideoView.setOnErrorListener(this);
+ mVideoView.setOnCompletionListener(this);
+ mVideoView.setVideoURI(mUri);
+
+ playVideo();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mHasPaused) {
+ mVideoView.seekTo(mVideoPosition);
+ mVideoView.resume();
+ mHasPaused = false;
+ }
+ mHandler.post(mProgressChecker);
+ }
+
+ @Override
+ public void onPause() {
+ mHasPaused = true;
+ mHandler.removeCallbacksAndMessages(null);
+ mVideoPosition = mVideoView.getCurrentPosition();
+ mVideoView.suspend();
+ super.onPause();
+ }
+
+ @Override
+ public void onStop() {
+ if (mProgress != null) {
+ mProgress.dismiss();
+ mProgress = null;
+ }
+ super.onStop();
+ }
+
+ @Override
+ public void onDestroy() {
+ mVideoView.stopPlayback();
+ super.onDestroy();
+ }
+
+ private final Runnable mProgressChecker = new Runnable() {
+ @Override
+ public void run() {
+ int pos = setProgress();
+ mHandler.postDelayed(mProgressChecker, 200 - (pos % 200));
+ }
+ };
+
+ @Override
+ public void onSaveInstanceState(Bundle savedInstanceState) {
+ savedInstanceState.putInt(KEY_TRIM_START, mTrimStartTime);
+ savedInstanceState.putInt(KEY_TRIM_END, mTrimEndTime);
+ savedInstanceState.putInt(KEY_VIDEO_POSITION, mVideoPosition);
+ super.onSaveInstanceState(savedInstanceState);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mTrimStartTime = savedInstanceState.getInt(KEY_TRIM_START, 0);
+ mTrimEndTime = savedInstanceState.getInt(KEY_TRIM_END, 0);
+ mVideoPosition = savedInstanceState.getInt(KEY_VIDEO_POSITION, 0);
+ }
+
+ // This updates the time bar display (if necessary). It is called by
+ // mProgressChecker and also from places where the time bar needs
+ // to be updated immediately.
+ private int setProgress() {
+ mVideoPosition = mVideoView.getCurrentPosition();
+ // If the video position is smaller than the starting point of trimming,
+ // correct it.
+ if (mVideoPosition < mTrimStartTime) {
+ mVideoView.seekTo(mTrimStartTime);
+ mVideoPosition = mTrimStartTime;
+ }
+ // If the position is bigger than the end point of trimming, show the
+ // replay button and pause.
+ if (mVideoPosition >= mTrimEndTime && mTrimEndTime > 0) {
+ if (mVideoPosition > mTrimEndTime) {
+ mVideoView.seekTo(mTrimEndTime);
+ mVideoPosition = mTrimEndTime;
+ }
+ mController.showEnded();
+ mVideoView.pause();
+ }
+
+ int duration = mVideoView.getDuration();
+ if (duration > 0 && mTrimEndTime == 0) {
+ mTrimEndTime = duration;
+ }
+ mController.setTimes(mVideoPosition, duration, mTrimStartTime, mTrimEndTime);
+ return mVideoPosition;
+ }
+
+ private void playVideo() {
+ mVideoView.start();
+ mController.showPlaying();
+ setProgress();
+ }
+
+ private void pauseVideo() {
+ mVideoView.pause();
+ mController.showPaused();
+ }
+
+
+ private boolean isModified() {
+ int delta = mTrimEndTime - mTrimStartTime;
+
+ // Considering that we only trim at sync frame, we don't want to trim
+ // when the time interval is too short or too close to the origin.
+ if (delta < 100 || Math.abs(mVideoView.getDuration() - delta) < 100) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ private void trimVideo() {
+
+ mDstFileInfo = SaveVideoFileUtils.getDstMp4FileInfo(TIME_STAMP_NAME,
+ getContentResolver(), mUri, getString(R.string.folder_download));
+ final File mSrcFile = new File(mSrcVideoPath);
+
+ showProgressDialog();
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ VideoUtils.startTrim(mSrcFile, mDstFileInfo.mFile,
+ mTrimStartTime, mTrimEndTime);
+ // Update the database for adding a new video file.
+ SaveVideoFileUtils.insertContent(mDstFileInfo,
+ getContentResolver(), mUri);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ // After trimming is done, trigger the UI changed.
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(getApplicationContext(),
+ getString(R.string.save_into, mDstFileInfo.mFolderName),
+ Toast.LENGTH_SHORT)
+ .show();
+ // TODO: change trimming into a service to avoid
+ // this progressDialog and add notification properly.
+ if (mProgress != null) {
+ mProgress.dismiss();
+ mProgress = null;
+ // Show the result only when the activity not stopped.
+ Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.fromFile(mDstFileInfo.mFile), "video/*");
+ intent.putExtra(MediaStore.EXTRA_FINISH_ON_COMPLETION, false);
+ startActivity(intent);
+ finish();
+ }
+ }
+ });
+ }
+ }).start();
+ }
+
+ private void showProgressDialog() {
+ // create a background thread to trim the video.
+ // and show the progress.
+ mProgress = new ProgressDialog(this);
+ mProgress.setTitle(getString(R.string.trimming));
+ mProgress.setMessage(getString(R.string.please_wait));
+ // TODO: make this cancelable.
+ mProgress.setCancelable(false);
+ mProgress.setCanceledOnTouchOutside(false);
+ mProgress.show();
+ }
+
+ @Override
+ public void onPlayPause() {
+ if (mVideoView.isPlaying()) {
+ pauseVideo();
+ } else {
+ playVideo();
+ }
+ }
+
+ @Override
+ public void onSeekStart() {
+ pauseVideo();
+ }
+
+ @Override
+ public void onSeekMove(int time) {
+ mVideoView.seekTo(time);
+ }
+
+ @Override
+ public void onSeekEnd(int time, int start, int end) {
+ mVideoView.seekTo(time);
+ mTrimStartTime = start;
+ mTrimEndTime = end;
+ setProgress();
+ // Enable save if there's modifications
+ mSaveVideoTextView.setEnabled(isModified());
+ }
+
+ @Override
+ public void onShown() {
+ }
+
+ @Override
+ public void onHidden() {
+ }
+
+ @Override
+ public void onReplay() {
+ mVideoView.seekTo(mTrimStartTime);
+ playVideo();
+ }
+
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ mController.showEnded();
+ }
+
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/app/VideoUtils.java b/src/com/android/gallery3d/app/VideoUtils.java
new file mode 100644
index 000000000..a3c3ef273
--- /dev/null
+++ b/src/com/android/gallery3d/app/VideoUtils.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+// Modified example based on mp4parser google code open source project.
+// http://code.google.com/p/mp4parser/source/browse/trunk/examples/src/main/java/com/googlecode/mp4parser/ShortenExample.java
+
+package com.android.gallery3d.app;
+
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.media.MediaMetadataRetriever;
+import android.media.MediaMuxer;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.util.SaveVideoFileInfo;
+import com.coremedia.iso.IsoFile;
+import com.coremedia.iso.boxes.TimeToSampleBox;
+import com.googlecode.mp4parser.authoring.Movie;
+import com.googlecode.mp4parser.authoring.Track;
+import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
+import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
+import com.googlecode.mp4parser.authoring.tracks.CroppedTrack;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+public class VideoUtils {
+ private static final String LOGTAG = "VideoUtils";
+ private static final int DEFAULT_BUFFER_SIZE = 1 * 1024 * 1024;
+
+ /**
+ * Remove the sound track.
+ */
+ public static void startMute(String filePath, SaveVideoFileInfo dstFileInfo)
+ throws IOException {
+ if (ApiHelper.HAS_MEDIA_MUXER) {
+ genVideoUsingMuxer(filePath, dstFileInfo.mFile.getPath(), -1, -1,
+ false, true);
+ } else {
+ startMuteUsingMp4Parser(filePath, dstFileInfo);
+ }
+ }
+
+ /**
+ * Shortens/Crops tracks
+ */
+ public static void startTrim(File src, File dst, int startMs, int endMs)
+ throws IOException {
+ if (ApiHelper.HAS_MEDIA_MUXER) {
+ genVideoUsingMuxer(src.getPath(), dst.getPath(), startMs, endMs,
+ true, true);
+ } else {
+ trimUsingMp4Parser(src, dst, startMs, endMs);
+ }
+ }
+
+ private static void startMuteUsingMp4Parser(String filePath,
+ SaveVideoFileInfo dstFileInfo) throws FileNotFoundException, IOException {
+ File dst = dstFileInfo.mFile;
+ File src = new File(filePath);
+ RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");
+ Movie movie = MovieCreator.build(randomAccessFile.getChannel());
+
+ // remove all tracks we will create new tracks from the old
+ List<Track> tracks = movie.getTracks();
+ movie.setTracks(new LinkedList<Track>());
+
+ for (Track track : tracks) {
+ if (track.getHandler().equals("vide")) {
+ movie.addTrack(track);
+ }
+ }
+ writeMovieIntoFile(dst, movie);
+ randomAccessFile.close();
+ }
+
+ private static void writeMovieIntoFile(File dst, Movie movie)
+ throws IOException {
+ if (!dst.exists()) {
+ dst.createNewFile();
+ }
+
+ IsoFile out = new DefaultMp4Builder().build(movie);
+ FileOutputStream fos = new FileOutputStream(dst);
+ FileChannel fc = fos.getChannel();
+ out.getBox(fc); // This one build up the memory.
+
+ fc.close();
+ fos.close();
+ }
+
+ /**
+ * @param srcPath the path of source video file.
+ * @param dstPath the path of destination video file.
+ * @param startMs starting time in milliseconds for trimming. Set to
+ * negative if starting from beginning.
+ * @param endMs end time for trimming in milliseconds. Set to negative if
+ * no trimming at the end.
+ * @param useAudio true if keep the audio track from the source.
+ * @param useVideo true if keep the video track from the source.
+ * @throws IOException
+ */
+ private static void genVideoUsingMuxer(String srcPath, String dstPath,
+ int startMs, int endMs, boolean useAudio, boolean useVideo)
+ throws IOException {
+ // Set up MediaExtractor to read from the source.
+ MediaExtractor extractor = new MediaExtractor();
+ extractor.setDataSource(srcPath);
+
+ int trackCount = extractor.getTrackCount();
+
+ // Set up MediaMuxer for the destination.
+ MediaMuxer muxer;
+ muxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
+
+ // Set up the tracks and retrieve the max buffer size for selected
+ // tracks.
+ HashMap<Integer, Integer> indexMap = new HashMap<Integer,
+ Integer>(trackCount);
+ int bufferSize = -1;
+ for (int i = 0; i < trackCount; i++) {
+ MediaFormat format = extractor.getTrackFormat(i);
+ String mime = format.getString(MediaFormat.KEY_MIME);
+
+ boolean selectCurrentTrack = false;
+
+ if (mime.startsWith("audio/") && useAudio) {
+ selectCurrentTrack = true;
+ } else if (mime.startsWith("video/") && useVideo) {
+ selectCurrentTrack = true;
+ }
+
+ if (selectCurrentTrack) {
+ extractor.selectTrack(i);
+ int dstIndex = muxer.addTrack(format);
+ indexMap.put(i, dstIndex);
+ if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) {
+ int newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
+ bufferSize = newSize > bufferSize ? newSize : bufferSize;
+ }
+ }
+ }
+
+ if (bufferSize < 0) {
+ bufferSize = DEFAULT_BUFFER_SIZE;
+ }
+
+ // Set up the orientation and starting time for extractor.
+ MediaMetadataRetriever retrieverSrc = new MediaMetadataRetriever();
+ retrieverSrc.setDataSource(srcPath);
+ String degreesString = retrieverSrc.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
+ if (degreesString != null) {
+ int degrees = Integer.parseInt(degreesString);
+ if (degrees >= 0) {
+ muxer.setOrientationHint(degrees);
+ }
+ }
+
+ if (startMs > 0) {
+ extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
+ }
+
+ // Copy the samples from MediaExtractor to MediaMuxer. We will loop
+ // for copying each sample and stop when we get to the end of the source
+ // file or exceed the end time of the trimming.
+ int offset = 0;
+ int trackIndex = -1;
+ ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);
+ BufferInfo bufferInfo = new BufferInfo();
+
+ muxer.start();
+ while (true) {
+ bufferInfo.offset = offset;
+ bufferInfo.size = extractor.readSampleData(dstBuf, offset);
+ if (bufferInfo.size < 0) {
+ Log.d(LOGTAG, "Saw input EOS.");
+ bufferInfo.size = 0;
+ break;
+ } else {
+ bufferInfo.presentationTimeUs = extractor.getSampleTime();
+ if (endMs > 0 && bufferInfo.presentationTimeUs > (endMs * 1000)) {
+ Log.d(LOGTAG, "The current sample is over the trim end time.");
+ break;
+ } else {
+ bufferInfo.flags = extractor.getSampleFlags();
+ trackIndex = extractor.getSampleTrackIndex();
+
+ muxer.writeSampleData(indexMap.get(trackIndex), dstBuf,
+ bufferInfo);
+ extractor.advance();
+ }
+ }
+ }
+
+ muxer.stop();
+ muxer.release();
+ return;
+ }
+
+ private static void trimUsingMp4Parser(File src, File dst, int startMs, int endMs)
+ throws FileNotFoundException, IOException {
+ RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");
+ Movie movie = MovieCreator.build(randomAccessFile.getChannel());
+
+ // remove all tracks we will create new tracks from the old
+ List<Track> tracks = movie.getTracks();
+ movie.setTracks(new LinkedList<Track>());
+
+ double startTime = startMs / 1000;
+ double endTime = endMs / 1000;
+
+ boolean timeCorrected = false;
+
+ // Here we try to find a track that has sync samples. Since we can only
+ // start decoding at such a sample we SHOULD make sure that the start of
+ // the new fragment is exactly such a frame.
+ for (Track track : tracks) {
+ if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
+ if (timeCorrected) {
+ // This exception here could be a false positive in case we
+ // have multiple tracks with sync samples at exactly the
+ // same positions. E.g. a single movie containing multiple
+ // qualities of the same video (Microsoft Smooth Streaming
+ // file)
+ throw new RuntimeException(
+ "The startTime has already been corrected by" +
+ " another track with SyncSample. Not Supported.");
+ }
+ startTime = correctTimeToSyncSample(track, startTime, false);
+ endTime = correctTimeToSyncSample(track, endTime, true);
+ timeCorrected = true;
+ }
+ }
+
+ for (Track track : tracks) {
+ long currentSample = 0;
+ double currentTime = 0;
+ long startSample = -1;
+ long endSample = -1;
+
+ for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
+ TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
+ for (int j = 0; j < entry.getCount(); j++) {
+ // entry.getDelta() is the amount of time the current sample
+ // covers.
+
+ if (currentTime <= startTime) {
+ // current sample is still before the new starttime
+ startSample = currentSample;
+ }
+ if (currentTime <= endTime) {
+ // current sample is after the new start time and still
+ // before the new endtime
+ endSample = currentSample;
+ } else {
+ // current sample is after the end of the cropped video
+ break;
+ }
+ currentTime += (double) entry.getDelta()
+ / (double) track.getTrackMetaData().getTimescale();
+ currentSample++;
+ }
+ }
+ movie.addTrack(new CroppedTrack(track, startSample, endSample));
+ }
+ writeMovieIntoFile(dst, movie);
+ randomAccessFile.close();
+ }
+
+ private static double correctTimeToSyncSample(Track track, double cutHere,
+ boolean next) {
+ double[] timeOfSyncSamples = new double[track.getSyncSamples().length];
+ long currentSample = 0;
+ double currentTime = 0;
+ for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) {
+ TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i);
+ for (int j = 0; j < entry.getCount(); j++) {
+ if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) {
+ // samples always start with 1 but we start with zero
+ // therefore +1
+ timeOfSyncSamples[Arrays.binarySearch(
+ track.getSyncSamples(), currentSample + 1)] = currentTime;
+ }
+ currentTime += (double) entry.getDelta()
+ / (double) track.getTrackMetaData().getTimescale();
+ currentSample++;
+ }
+ }
+ double previous = 0;
+ for (double timeOfSyncSample : timeOfSyncSamples) {
+ if (timeOfSyncSample > cutHere) {
+ if (next) {
+ return timeOfSyncSample;
+ } else {
+ return previous;
+ }
+ }
+ previous = timeOfSyncSample;
+ }
+ return timeOfSyncSamples[timeOfSyncSamples.length - 1];
+ }
+
+}
diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java
new file mode 100644
index 000000000..b0a26c236
--- /dev/null
+++ b/src/com/android/gallery3d/app/Wallpaper.java
@@ -0,0 +1,135 @@
+/*
+ * 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.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.Display;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+
+/**
+ * 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("deprecation")
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
+ private Point getDefaultDisplaySize(Point size) {
+ Display d = getWindowManager().getDefaultDisplay();
+ if (Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.HONEYCOMB_MR2) {
+ d.getSize(size);
+ } else {
+ size.set(d.getWidth(), d.getHeight());
+ }
+ return size;
+ }
+
+ @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();
+ Point size = getDefaultDisplaySize(new Point());
+ float spotlightX = (float) size.x / width;
+ float spotlightY = (float) size.y / height;
+ Intent request = new Intent(CropActivity.CROP_ACTION)
+ .setDataAndType(mPickedItem, IMAGE_TYPE)
+ .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT)
+ .putExtra(CropExtras.KEY_OUTPUT_X, width)
+ .putExtra(CropExtras.KEY_OUTPUT_Y, height)
+ .putExtra(CropExtras.KEY_ASPECT_X, width)
+ .putExtra(CropExtras.KEY_ASPECT_Y, height)
+ .putExtra(CropExtras.KEY_SPOTLIGHT_X, spotlightX)
+ .putExtra(CropExtras.KEY_SPOTLIGHT_Y, spotlightY)
+ .putExtra(CropExtras.KEY_SCALE, true)
+ .putExtra(CropExtras.KEY_SCALE_UP_IF_NEEDED, true)
+ .putExtra(CropExtras.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/ActionImage.java b/src/com/android/gallery3d/data/ActionImage.java
new file mode 100644
index 000000000..58e30b146
--- /dev/null
+++ b/src/com/android/gallery3d/data/ActionImage.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2012 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.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class ActionImage extends MediaItem {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ActionImage";
+ private GalleryApp mApplication;
+ private int mResourceId;
+
+ public ActionImage(Path path, GalleryApp application, int resourceId) {
+ super(path, nextVersionNumber());
+ mApplication = Utils.checkNotNull(application);
+ mResourceId = resourceId;
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new BitmapJob(type);
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> requestLargeImage() {
+ return null;
+ }
+
+ private class BitmapJob implements Job<Bitmap> {
+ private int mType;
+
+ protected BitmapJob(int type) {
+ mType = type;
+ }
+
+ @Override
+ public Bitmap run(JobContext jc) {
+ int targetSize = MediaItem.getTargetSize(mType);
+ Bitmap bitmap = BitmapFactory.decodeResource(mApplication.getResources(),
+ mResourceId);
+
+ if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+ bitmap = BitmapUtils.resizeAndCropCenter(bitmap, targetSize, true);
+ } else {
+ bitmap = BitmapUtils.resizeDownBySideLength(bitmap, targetSize, true);
+ }
+ return bitmap;
+ }
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return SUPPORT_ACTION;
+ }
+
+ @Override
+ public int getMediaType() {
+ return MEDIA_TYPE_UNKNOWN;
+ }
+
+ @Override
+ public Uri getContentUri() {
+ return null;
+ }
+
+ @Override
+ public String getMimeType() {
+ return "";
+ }
+
+ @Override
+ public int getWidth() {
+ return 0;
+ }
+
+ @Override
+ public int getHeight() {
+ return 0;
+ }
+}
diff --git a/src/com/android/gallery3d/data/BucketHelper.java b/src/com/android/gallery3d/data/BucketHelper.java
new file mode 100644
index 000000000..3418dafb7
--- /dev/null
+++ b/src/com/android/gallery3d/data/BucketHelper.java
@@ -0,0 +1,241 @@
+package com.android.gallery3d.data;
+
+import android.annotation.TargetApi;
+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 android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+
+class BucketHelper {
+
+ private static final String TAG = "BucketHelper";
+ private static final String EXTERNAL_MEDIA = "external";
+
+ // BUCKET_DISPLAY_NAME is a string like "Camera" which is the directory
+ // name of where an image or video is in. BUCKET_ID is a hash of the path
+ // name of that directory (see computeBucketValues() in MediaProvider for
+ // details). MEDIA_TYPE is video, image, audio, etc.
+ //
+ // The "albums" are not explicitly recorded in the database, but each image
+ // or video has the two columns (BUCKET_ID, MEDIA_TYPE). We define an
+ // "album" to be the collection of images/videos which have the same value
+ // for the two columns.
+ //
+ // The goal of the query (used in loadSubMediaSetsFromFilesTable()) is to
+ // find all albums, that is, all unique values for (BUCKET_ID, MEDIA_TYPE).
+ // In the meantime sort them by the timestamp of the latest image/video in
+ // each of the album.
+ //
+ // The order of columns below is important: it must match to the index in
+ // MediaStore.
+ private static final String[] PROJECTION_BUCKET = {
+ ImageColumns.BUCKET_ID,
+ FileColumns.MEDIA_TYPE,
+ ImageColumns.BUCKET_DISPLAY_NAME};
+
+ // The indices should match the above 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;
+
+ // We want to order the albums by reverse chronological order. We abuse the
+ // "WHERE" parameter to insert a "GROUP BY" clause into the SQL statement.
+ // The template for "WHERE" parameter is like:
+ // SELECT ... FROM ... WHERE (%s)
+ // and we make it look like:
+ // SELECT ... FROM ... WHERE (1) GROUP BY 1,(2)
+ // The "(1)" means true. The "1,(2)" means the first two columns specified
+ // after SELECT. Note that because there is a ")" in the template, we use
+ // "(2" to match it.
+ private static final String BUCKET_GROUP_BY = "1) GROUP BY 1,(2";
+
+ private static final String BUCKET_ORDER_BY = "MAX(datetaken) DESC";
+
+ // Before HoneyComb there is no Files table. Thus, we need to query the
+ // bucket info from the Images and Video tables and then merge them
+ // together.
+ //
+ // A bucket can exist in both tables. In this case, we need to find the
+ // latest timestamp from the two tables and sort ourselves. So we add the
+ // MAX(date_taken) to the projection and remove the media_type since we
+ // already know the media type from the table we query from.
+ private static final String[] PROJECTION_BUCKET_IN_ONE_TABLE = {
+ ImageColumns.BUCKET_ID,
+ "MAX(datetaken)",
+ ImageColumns.BUCKET_DISPLAY_NAME};
+
+ // We keep the INDEX_BUCKET_ID and INDEX_BUCKET_NAME the same as
+ // PROJECTION_BUCKET so we can reuse the values defined before.
+ private static final int INDEX_DATE_TAKEN = 1;
+
+ // When query from the Images or Video tables, we only need to group by BUCKET_ID.
+ private static final String BUCKET_GROUP_BY_IN_ONE_TABLE = "1) GROUP BY (1";
+
+ public static BucketEntry[] loadBucketEntries(
+ JobContext jc, ContentResolver resolver, int type) {
+ if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
+ return loadBucketEntriesFromFilesTable(jc, resolver, type);
+ } else {
+ return loadBucketEntriesFromImagesAndVideoTable(jc, resolver, type);
+ }
+ }
+
+ private static void updateBucketEntriesFromTable(JobContext jc,
+ ContentResolver resolver, Uri tableUri, HashMap<Integer, BucketEntry> buckets) {
+ Cursor cursor = resolver.query(tableUri, PROJECTION_BUCKET_IN_ONE_TABLE,
+ BUCKET_GROUP_BY_IN_ONE_TABLE, null, null);
+ if (cursor == null) {
+ Log.w(TAG, "cannot open media database: " + tableUri);
+ return;
+ }
+ try {
+ while (cursor.moveToNext()) {
+ int bucketId = cursor.getInt(INDEX_BUCKET_ID);
+ int dateTaken = cursor.getInt(INDEX_DATE_TAKEN);
+ BucketEntry entry = buckets.get(bucketId);
+ if (entry == null) {
+ entry = new BucketEntry(bucketId, cursor.getString(INDEX_BUCKET_NAME));
+ buckets.put(bucketId, entry);
+ entry.dateTaken = dateTaken;
+ } else {
+ entry.dateTaken = Math.max(entry.dateTaken, dateTaken);
+ }
+ }
+ } finally {
+ Utils.closeSilently(cursor);
+ }
+ }
+
+ private static BucketEntry[] loadBucketEntriesFromImagesAndVideoTable(
+ JobContext jc, ContentResolver resolver, int type) {
+ HashMap<Integer, BucketEntry> buckets = new HashMap<Integer, BucketEntry>(64);
+ if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) {
+ updateBucketEntriesFromTable(
+ jc, resolver, Images.Media.EXTERNAL_CONTENT_URI, buckets);
+ }
+ if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) {
+ updateBucketEntriesFromTable(
+ jc, resolver, Video.Media.EXTERNAL_CONTENT_URI, buckets);
+ }
+ BucketEntry[] entries = buckets.values().toArray(new BucketEntry[buckets.size()]);
+ Arrays.sort(entries, new Comparator<BucketEntry>() {
+ @Override
+ public int compare(BucketEntry a, BucketEntry b) {
+ // sorted by dateTaken in descending order
+ return b.dateTaken - a.dateTaken;
+ }
+ });
+ return entries;
+ }
+
+ private static BucketEntry[] loadBucketEntriesFromFilesTable(
+ JobContext jc, ContentResolver resolver, int type) {
+ Uri uri = getFilesContentUri();
+
+ Cursor cursor = resolver.query(uri,
+ PROJECTION_BUCKET, BUCKET_GROUP_BY,
+ null, BUCKET_ORDER_BY);
+ if (cursor == null) {
+ Log.w(TAG, "cannot open local database: " + uri);
+ return new BucketEntry[0];
+ }
+ ArrayList<BucketEntry> buffer = new ArrayList<BucketEntry>();
+ int typeBits = 0;
+ if ((type & MediaObject.MEDIA_TYPE_IMAGE) != 0) {
+ typeBits |= (1 << FileColumns.MEDIA_TYPE_IMAGE);
+ }
+ if ((type & MediaObject.MEDIA_TYPE_VIDEO) != 0) {
+ typeBits |= (1 << FileColumns.MEDIA_TYPE_VIDEO);
+ }
+ try {
+ while (cursor.moveToNext()) {
+ if ((typeBits & (1 << cursor.getInt(INDEX_MEDIA_TYPE))) != 0) {
+ BucketEntry entry = new BucketEntry(
+ cursor.getInt(INDEX_BUCKET_ID),
+ cursor.getString(INDEX_BUCKET_NAME));
+ if (!buffer.contains(entry)) {
+ buffer.add(entry);
+ }
+ }
+ if (jc.isCancelled()) return null;
+ }
+ } finally {
+ Utils.closeSilently(cursor);
+ }
+ return buffer.toArray(new BucketEntry[buffer.size()]);
+ }
+
+ private static String getBucketNameInTable(
+ ContentResolver resolver, Uri tableUri, int bucketId) {
+ String selectionArgs[] = new String[] {String.valueOf(bucketId)};
+ Uri uri = tableUri.buildUpon()
+ .appendQueryParameter("limit", "1")
+ .build();
+ Cursor cursor = resolver.query(uri, PROJECTION_BUCKET_IN_ONE_TABLE,
+ "bucket_id = ?", selectionArgs, null);
+ try {
+ if (cursor != null && cursor.moveToNext()) {
+ return cursor.getString(INDEX_BUCKET_NAME);
+ }
+ } finally {
+ Utils.closeSilently(cursor);
+ }
+ return null;
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ private static Uri getFilesContentUri() {
+ return Files.getContentUri(EXTERNAL_MEDIA);
+ }
+
+ public static String getBucketName(ContentResolver resolver, int bucketId) {
+ if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
+ String result = getBucketNameInTable(resolver, getFilesContentUri(), bucketId);
+ return result == null ? "" : result;
+ } else {
+ String result = getBucketNameInTable(
+ resolver, Images.Media.EXTERNAL_CONTENT_URI, bucketId);
+ if (result != null) return result;
+ result = getBucketNameInTable(
+ resolver, Video.Media.EXTERNAL_CONTENT_URI, bucketId);
+ return result == null ? "" : result;
+ }
+ }
+
+ public static class BucketEntry {
+ public String bucketName;
+ public int bucketId;
+ public int dateTaken;
+
+ 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/BytesBufferPool.java b/src/com/android/gallery3d/data/BytesBufferPool.java
new file mode 100644
index 000000000..d2da323fc
--- /dev/null
+++ b/src/com/android/gallery3d/data/BytesBufferPool.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2012 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.JobContext;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class BytesBufferPool {
+
+ private static final int READ_STEP = 4096;
+
+ public static class BytesBuffer {
+ public byte[] data;
+ public int offset;
+ public int length;
+
+ private BytesBuffer(int capacity) {
+ this.data = new byte[capacity];
+ }
+
+ // an helper function to read content from FileDescriptor
+ public void readFrom(JobContext jc, FileDescriptor fd) throws IOException {
+ FileInputStream fis = new FileInputStream(fd);
+ length = 0;
+ try {
+ int capacity = data.length;
+ while (true) {
+ int step = Math.min(READ_STEP, capacity - length);
+ int rc = fis.read(data, length, step);
+ if (rc < 0 || jc.isCancelled()) return;
+ length += rc;
+
+ if (length == capacity) {
+ byte[] newData = new byte[data.length * 2];
+ System.arraycopy(data, 0, newData, 0, data.length);
+ data = newData;
+ capacity = data.length;
+ }
+ }
+ } finally {
+ fis.close();
+ }
+ }
+ }
+
+ private final int mPoolSize;
+ private final int mBufferSize;
+ private final ArrayList<BytesBuffer> mList;
+
+ public BytesBufferPool(int poolSize, int bufferSize) {
+ mList = new ArrayList<BytesBuffer>(poolSize);
+ mPoolSize = poolSize;
+ mBufferSize = bufferSize;
+ }
+
+ public synchronized BytesBuffer get() {
+ int n = mList.size();
+ return n > 0 ? mList.remove(n - 1) : new BytesBuffer(mBufferSize);
+ }
+
+ public synchronized void recycle(BytesBuffer buffer) {
+ if (buffer.data.length != mBufferSize) return;
+ if (mList.size() < mPoolSize) {
+ buffer.offset = 0;
+ buffer.length = 0;
+ mList.add(buffer);
+ }
+ }
+
+ public synchronized void clear() {
+ mList.clear();
+ }
+}
diff --git a/src/com/android/gallery3d/data/CameraShortcutImage.java b/src/com/android/gallery3d/data/CameraShortcutImage.java
new file mode 100644
index 000000000..865270b4c
--- /dev/null
+++ b/src/com/android/gallery3d/data/CameraShortcutImage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012 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;
+
+public class CameraShortcutImage extends ActionImage {
+ @SuppressWarnings("unused")
+ private static final String TAG = "CameraShortcutImage";
+
+ public CameraShortcutImage(Path path, GalleryApp application) {
+ super(path, application, R.drawable.placeholder_camera);
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return super.getSupportedOperations() | SUPPORT_CAMERA_SHORTCUT;
+ }
+}
diff --git a/src/com/android/gallery3d/data/ChangeNotifier.java b/src/com/android/gallery3d/data/ChangeNotifier.java
new file mode 100644
index 000000000..558a8648e
--- /dev/null
+++ b/src/com/android/gallery3d/data/ChangeNotifier.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.data;
+
+import android.net.Uri;
+
+import com.android.gallery3d.app.GalleryApp;
+
+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);
+ }
+
+ public ChangeNotifier(MediaSet set, Uri[] uris, GalleryApp application) {
+ mMediaSet = set;
+ for (int i = 0; i < uris.length; i++) {
+ application.getDataManager().registerChangeNotifier(uris[i], this);
+ }
+ }
+
+ // Returns the dirty flag and clear it.
+ public boolean isDirty() {
+ return mContentDirty.compareAndSet(true, false);
+ }
+
+ public void fakeChange() {
+ onChange(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..8681952bf
--- /dev/null
+++ b/src/com/android/gallery3d/data/ClusterAlbum.java
@@ -0,0 +1,143 @@
+/*
+ * 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 {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ClusterAlbum";
+ private ArrayList<Path> mPaths = new ArrayList<Path>();
+ private String mName = "";
+ private DataManager mDataManager;
+ private MediaSet mClusterAlbumSet;
+ private MediaItem mCover;
+
+ public ClusterAlbum(Path path, DataManager dataManager,
+ MediaSet clusterAlbumSet) {
+ super(path, nextVersionNumber());
+ mDataManager = dataManager;
+ mClusterAlbumSet = clusterAlbumSet;
+ mClusterAlbumSet.addContentListener(this);
+ }
+
+ public void setCoverMediaItem(MediaItem cover) {
+ mCover = cover;
+ }
+
+ @Override
+ public MediaItem getCoverMediaItem() {
+ return mCover != null ? mCover : super.getCoverMediaItem();
+ }
+
+ void setMediaItems(ArrayList<Path> paths) {
+ mPaths = paths;
+ }
+
+ ArrayList<Path> 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<MediaItem> getMediaItem(int start, int count) {
+ return getMediaItemFromPath(mPaths, start, count, mDataManager);
+ }
+
+ public static ArrayList<MediaItem> getMediaItemFromPath(
+ ArrayList<Path> paths, int start, int count,
+ DataManager dataManager) {
+ if (start >= paths.size()) {
+ return new ArrayList<MediaItem>();
+ }
+ int end = Math.min(start + count, paths.size());
+ ArrayList<Path> subset = new ArrayList<Path>(paths.subList(start, end));
+ final MediaItem[] buf = new MediaItem[end - start];
+ ItemConsumer consumer = new ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ buf[index] = item;
+ }
+ };
+ dataManager.mapMediaItems(subset, consumer, 0);
+ ArrayList<MediaItem> result = new ArrayList<MediaItem>(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;
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return SUPPORT_SHARE | SUPPORT_DELETE | SUPPORT_INFO;
+ }
+
+ @Override
+ public void delete() {
+ ItemConsumer consumer = new ItemConsumer() {
+ @Override
+ 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..cb212ba36
--- /dev/null
+++ b/src/com/android/gallery3d/data/ClusterAlbumSet.java
@@ -0,0 +1,159 @@
+/*
+ * 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.content.Context;
+import android.net.Uri;
+
+import com.android.gallery3d.app.GalleryApp;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+public class ClusterAlbumSet extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ClusterAlbumSet";
+ private GalleryApp mApplication;
+ private MediaSet mBaseSet;
+ private int mKind;
+ private ArrayList<ClusterAlbum> mAlbums = new ArrayList<ClusterAlbum>();
+ 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;
+ }
+
+ @Override
+ 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;
+ synchronized (DataManager.LOCK) {
+ album = (ClusterAlbum) dataManager.peekMediaObject(childPath);
+ if (album == null) {
+ album = new ClusterAlbum(childPath, dataManager, this);
+ }
+ }
+ album.setMediaItems(clustering.getCluster(i));
+ album.setName(childName);
+ album.setCoverMediaItem(clustering.getClusterCover(i));
+ mAlbums.add(album);
+ }
+ }
+
+ private void updateClustersContents() {
+ final HashSet<Path> existing = new HashSet<Path>();
+ mBaseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ @Override
+ 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<Path> oldPaths = mAlbums.get(i).getMediaItems();
+ ArrayList<Path> newPaths = new ArrayList<Path>();
+ 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..4072bf57b
--- /dev/null
+++ b/src/com/android/gallery3d/data/Clustering.java
@@ -0,0 +1,29 @@
+/*
+ * 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<Path> getCluster(int index);
+ public abstract String getClusterName(int index);
+ public MediaItem getClusterCover(int index) {
+ return null;
+ }
+}
diff --git a/src/com/android/gallery3d/data/ComboAlbum.java b/src/com/android/gallery3d/data/ComboAlbum.java
new file mode 100644
index 000000000..cadd9f8af
--- /dev/null
+++ b/src/com/android/gallery3d/data/ComboAlbum.java
@@ -0,0 +1,103 @@
+/*
+ * 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.util.Future;
+
+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 {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ComboAlbum";
+ private final MediaSet[] mSets;
+ private 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<MediaItem> getMediaItem(int start, int count) {
+ ArrayList<MediaItem> items = new ArrayList<MediaItem>();
+ for (MediaSet set : mSets) {
+ int size = set.getMediaItemCount();
+ if (count < 1) break;
+ if (start < size) {
+ int fetchCount = (start + count <= size) ? count : size - start;
+ ArrayList<MediaItem> 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 boolean isLeafAlbum() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ public void useNameOfChild(int i) {
+ if (i < mSets.length) mName = mSets[i].getName();
+ }
+
+ @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;
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ @Override
+ public Future<Integer> requestSync(SyncListener listener) {
+ return requestSyncOnMultipleSets(mSets, listener);
+ }
+}
diff --git a/src/com/android/gallery3d/data/ComboAlbumSet.java b/src/com/android/gallery3d/data/ComboAlbumSet.java
new file mode 100644
index 000000000..3f3674500
--- /dev/null
+++ b/src/com/android/gallery3d/data/ComboAlbumSet.java
@@ -0,0 +1,96 @@
+/*
+ * 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.Future;
+
+// 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 {
+ @SuppressWarnings("unused")
+ 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 boolean isLoading() {
+ for (int i = 0, n = mSets.length; i < n; ++i) {
+ if (mSets[i].isLoading()) return true;
+ }
+ return false;
+ }
+
+ @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;
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ @Override
+ public Future<Integer> requestSync(SyncListener listener) {
+ return requestSyncOnMultipleSets(mSets, listener);
+ }
+}
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..38865e9f1
--- /dev/null
+++ b/src/com/android/gallery3d/data/DataManager.java
@@ -0,0 +1,371 @@
+/*
+ * 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.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.StitchingChangeListener;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+import com.android.gallery3d.data.MediaSource.PathId;
+import com.android.gallery3d.picasasource.PicasaSource;
+
+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 implements StitchingChangeListener {
+ 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();
+
+ public static DataManager from(Context context) {
+ GalleryApp app = (GalleryApp) context.getApplicationContext();
+ return app.getDataManager();
+ }
+
+ 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/{/local/all,/picasa/all}";
+
+ private static final String TOP_IMAGE_SET_PATH = "/combo/{/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<MediaItem> sDateTakenComparator =
+ new DateTakenComparator();
+
+ private static class DateTakenComparator implements Comparator<MediaItem> {
+ @Override
+ 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<Uri, NotifyBroker> mNotifierMap =
+ new HashMap<Uri, NotifyBroker>();
+
+
+ private HashMap<String, MediaSource> mSourceMap =
+ new LinkedHashMap<String, MediaSource>();
+
+ 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 ComboSource(mApplication));
+ addSource(new ClusterSource(mApplication));
+ addSource(new FilterSource(mApplication));
+ addSource(new SecureSource(mApplication));
+ addSource(new UriSource(mApplication));
+ addSource(new SnailSource(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) {
+ if (source == null) return;
+ mSourceMap.put(source.getPrefix(), source);
+ }
+
+ // A common usage of this method is:
+ // synchronized (DataManager.LOCK) {
+ // MediaObject object = peekMediaObject(path);
+ // if (object == null) {
+ // object = createMediaObject(...);
+ // }
+ // }
+ public MediaObject peekMediaObject(Path path) {
+ return path.getObject();
+ }
+
+ public MediaObject getMediaObject(Path path) {
+ synchronized (LOCK) {
+ 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;
+ }
+
+ try {
+ MediaObject object = source.createMediaObject(path);
+ if (object == null) {
+ Log.w(TAG, "cannot create media object: " + path);
+ }
+ return object;
+ } catch (Throwable t) {
+ Log.w(TAG, "exception in creating media object: " + path, t);
+ return null;
+ }
+ }
+ }
+
+ 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<Path> list, ItemConsumer consumer,
+ int startIndex) {
+ HashMap<String, ArrayList<PathId>> map =
+ new HashMap<String, ArrayList<PathId>>();
+
+ // 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<PathId> group = map.get(prefix);
+ if (group == null) {
+ group = new ArrayList<PathId>();
+ map.put(prefix, group);
+ }
+ group.add(new PathId(path, i + startIndex));
+ }
+
+ // For each group, ask the corresponding media source to map it.
+ for (Entry<String, ArrayList<PathId>> 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 getPanoramaSupport(Path path, PanoramaSupportCallback callback) {
+ getMediaObject(path).getPanoramaSupport(callback);
+ }
+
+ 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 Path findPathByUri(Uri uri, String type) {
+ if (uri == null) return null;
+ for (MediaSource source : mSourceMap.values()) {
+ Path path = source.findPathByUri(uri, type);
+ 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<ChangeNotifier, Object> mNotifiers =
+ new WeakHashMap<ChangeNotifier, Object>();
+
+ 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);
+ }
+ }
+ }
+
+ @Override
+ public void onStitchingQueued(Uri uri) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onStitchingResult(Uri uri) {
+ Path path = findPathByUri(uri, null);
+ if (path != null) {
+ MediaObject mediaObject = getMediaObject(path);
+ if (mediaObject != null) {
+ mediaObject.clearCachedPanoramaSupport();
+ }
+ }
+ }
+
+ @Override
+ public void onStitchingProgress(Uri uri, int progress) {
+ // Do nothing.
+ }
+}
diff --git a/src/com/android/gallery3d/data/DataSourceType.java b/src/com/android/gallery3d/data/DataSourceType.java
new file mode 100644
index 000000000..ab534d0c3
--- /dev/null
+++ b/src/com/android/gallery3d/data/DataSourceType.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.data;
+
+import com.android.gallery3d.util.MediaSetUtils;
+
+public final class DataSourceType {
+ public static final int TYPE_NOT_CATEGORIZED = 0;
+ public static final int TYPE_LOCAL = 1;
+ public static final int TYPE_PICASA = 2;
+ public static final int TYPE_CAMERA = 3;
+
+ private static final Path PICASA_ROOT = Path.fromString("/picasa");
+ private static final Path LOCAL_ROOT = Path.fromString("/local");
+
+ public static int identifySourceType(MediaSet set) {
+ if (set == null) {
+ return TYPE_NOT_CATEGORIZED;
+ }
+
+ Path path = set.getPath();
+ if (MediaSetUtils.isCameraSource(path)) return TYPE_CAMERA;
+
+ Path prefix = path.getPrefixPath();
+
+ if (prefix == PICASA_ROOT) return TYPE_PICASA;
+ if (prefix == LOCAL_ROOT) return TYPE_LOCAL;
+
+ return TYPE_NOT_CATEGORIZED;
+ }
+}
diff --git a/src/com/android/gallery3d/data/DecodeUtils.java b/src/com/android/gallery3d/data/DecodeUtils.java
new file mode 100644
index 000000000..fa709157d
--- /dev/null
+++ b/src/com/android/gallery3d/data/DecodeUtils.java
@@ -0,0 +1,312 @@
+/*
+ * 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.annotation.TargetApi;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+import android.graphics.BitmapRegionDecoder;
+import android.os.Build;
+import android.util.FloatMath;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.ui.Log;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.InputStream;
+
+public class DecodeUtils {
+ private static final String TAG = "DecodeUtils";
+
+ private static class DecodeCanceller implements CancelListener {
+ Options mOptions;
+
+ public DecodeCanceller(Options options) {
+ mOptions = options;
+ }
+
+ @Override
+ public void onCancel() {
+ mOptions.requestCancelDecode();
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ public static void setOptionsMutable(Options options) {
+ if (ApiHelper.HAS_OPTIONS_IN_MUTABLE) options.inMutable = true;
+ }
+
+ public static Bitmap decode(JobContext jc, FileDescriptor fd, Options options) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+ setOptionsMutable(options);
+ return ensureGLCompatibleBitmap(
+ BitmapFactory.decodeFileDescriptor(fd, null, options));
+ }
+
+ public static void decodeBounds(JobContext jc, FileDescriptor fd,
+ Options options) {
+ Utils.assertTrue(options != null);
+ options.inJustDecodeBounds = true;
+ jc.setCancelListener(new DecodeCanceller(options));
+ BitmapFactory.decodeFileDescriptor(fd, null, options);
+ options.inJustDecodeBounds = false;
+ }
+
+ public static Bitmap decode(JobContext jc, byte[] bytes, Options options) {
+ return decode(jc, bytes, 0, bytes.length, options);
+ }
+
+ public static Bitmap decode(JobContext jc, byte[] bytes, int offset,
+ int length, Options options) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+ setOptionsMutable(options);
+ return ensureGLCompatibleBitmap(
+ BitmapFactory.decodeByteArray(bytes, offset, length, options));
+ }
+
+ public static void decodeBounds(JobContext jc, byte[] bytes, int offset,
+ int length, Options options) {
+ Utils.assertTrue(options != null);
+ options.inJustDecodeBounds = true;
+ jc.setCancelListener(new DecodeCanceller(options));
+ BitmapFactory.decodeByteArray(bytes, offset, length, options);
+ options.inJustDecodeBounds = false;
+ }
+
+ public static Bitmap decodeThumbnail(
+ JobContext jc, String filePath, Options options, int targetSize, int type) {
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(filePath);
+ FileDescriptor fd = fis.getFD();
+ return decodeThumbnail(jc, fd, options, targetSize, type);
+ } catch (Exception ex) {
+ Log.w(TAG, ex);
+ return null;
+ } finally {
+ Utils.closeSilently(fis);
+ }
+ }
+
+ public static Bitmap decodeThumbnail(
+ JobContext jc, FileDescriptor fd, Options options, int targetSize, int type) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(fd, null, options);
+ if (jc.isCancelled()) return null;
+
+ int w = options.outWidth;
+ int h = options.outHeight;
+
+ if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
+ // We center-crop the original image as it's micro thumbnail. In this case,
+ // we want to make sure the shorter side >= "targetSize".
+ float scale = (float) targetSize / Math.min(w, h);
+ options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
+
+ // For an extremely wide image, e.g. 300x30000, we may got OOM when decoding
+ // it for TYPE_MICROTHUMBNAIL. So we add a max number of pixels limit here.
+ final int MAX_PIXEL_COUNT = 640000; // 400 x 1600
+ if ((w / options.inSampleSize) * (h / options.inSampleSize) > MAX_PIXEL_COUNT) {
+ options.inSampleSize = BitmapUtils.computeSampleSize(
+ FloatMath.sqrt((float) MAX_PIXEL_COUNT / (w * h)));
+ }
+ } else {
+ // For screen nail, we only want to keep the longer side >= targetSize.
+ float scale = (float) targetSize / Math.max(w, h);
+ options.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
+ }
+
+ options.inJustDecodeBounds = false;
+ setOptionsMutable(options);
+
+ Bitmap result = BitmapFactory.decodeFileDescriptor(fd, null, options);
+ if (result == null) return null;
+
+ // We need to resize down if the decoder does not support inSampleSize
+ // (For example, GIF images)
+ float scale = (float) targetSize / (type == MediaItem.TYPE_MICROTHUMBNAIL
+ ? Math.min(result.getWidth(), result.getHeight())
+ : Math.max(result.getWidth(), result.getHeight()));
+
+ if (scale <= 0.5) result = BitmapUtils.resizeBitmapByScale(result, scale, true);
+ return ensureGLCompatibleBitmap(result);
+ }
+
+ /**
+ * Decodes the bitmap from the given byte array if the image size is larger than the given
+ * requirement.
+ *
+ * Note: The returned image may be resized down. However, both width and height must be
+ * larger than the <code>targetSize</code>.
+ */
+ public static Bitmap decodeIfBigEnough(JobContext jc, byte[] data,
+ Options options, int targetSize) {
+ if (options == null) options = new Options();
+ jc.setCancelListener(new DecodeCanceller(options));
+
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(data, 0, data.length, options);
+ if (jc.isCancelled()) return null;
+ if (options.outWidth < targetSize || options.outHeight < targetSize) {
+ return null;
+ }
+ options.inSampleSize = BitmapUtils.computeSampleSizeLarger(
+ options.outWidth, options.outHeight, targetSize);
+ options.inJustDecodeBounds = false;
+ setOptionsMutable(options);
+
+ return ensureGLCompatibleBitmap(
+ BitmapFactory.decodeByteArray(data, 0, data.length, 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 createBitmapRegionDecoder(
+ 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 createBitmapRegionDecoder(
+ JobContext jc, String filePath, boolean shareable) {
+ try {
+ return BitmapRegionDecoder.newInstance(filePath, shareable);
+ } catch (Throwable t) {
+ Log.w(TAG, t);
+ return null;
+ }
+ }
+
+ public static BitmapRegionDecoder createBitmapRegionDecoder(
+ JobContext jc, FileDescriptor fd, boolean shareable) {
+ try {
+ return BitmapRegionDecoder.newInstance(fd, shareable);
+ } catch (Throwable t) {
+ Log.w(TAG, t);
+ return null;
+ }
+ }
+
+ public static BitmapRegionDecoder createBitmapRegionDecoder(
+ JobContext jc, InputStream is, boolean shareable) {
+ try {
+ return BitmapRegionDecoder.newInstance(is, shareable);
+ } catch (Throwable t) {
+ // We often cancel the creating of bitmap region decoder,
+ // so just log one line.
+ Log.w(TAG, "requestCreateBitmapRegionDecoder: " + t);
+ return null;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static Bitmap decodeUsingPool(JobContext jc, byte[] data, int offset,
+ int length, BitmapFactory.Options options) {
+ if (options == null) options = new BitmapFactory.Options();
+ if (options.inSampleSize < 1) options.inSampleSize = 1;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ options.inBitmap = (options.inSampleSize == 1)
+ ? findCachedBitmap(jc, data, offset, length, options) : null;
+ try {
+ Bitmap bitmap = decode(jc, data, offset, length, options);
+ if (options.inBitmap != null && options.inBitmap != bitmap) {
+ GalleryBitmapPool.getInstance().put(options.inBitmap);
+ options.inBitmap = null;
+ }
+ return bitmap;
+ } catch (IllegalArgumentException e) {
+ if (options.inBitmap == null) throw e;
+
+ Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
+ GalleryBitmapPool.getInstance().put(options.inBitmap);
+ options.inBitmap = null;
+ return decode(jc, data, offset, length, options);
+ }
+ }
+
+ // This is the same as the method above except the source data comes
+ // from a file descriptor instead of a byte array.
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static Bitmap decodeUsingPool(JobContext jc,
+ FileDescriptor fileDescriptor, Options options) {
+ if (options == null) options = new BitmapFactory.Options();
+ if (options.inSampleSize < 1) options.inSampleSize = 1;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ options.inBitmap = (options.inSampleSize == 1)
+ ? findCachedBitmap(jc, fileDescriptor, options) : null;
+ try {
+ Bitmap bitmap = DecodeUtils.decode(jc, fileDescriptor, options);
+ if (options.inBitmap != null && options.inBitmap != bitmap) {
+ GalleryBitmapPool.getInstance().put(options.inBitmap);
+ options.inBitmap = null;
+ }
+ return bitmap;
+ } catch (IllegalArgumentException e) {
+ if (options.inBitmap == null) throw e;
+
+ Log.w(TAG, "decode fail with a given bitmap, try decode to a new bitmap");
+ GalleryBitmapPool.getInstance().put(options.inBitmap);
+ options.inBitmap = null;
+ return decode(jc, fileDescriptor, options);
+ }
+ }
+
+ private static Bitmap findCachedBitmap(JobContext jc, byte[] data,
+ int offset, int length, Options options) {
+ decodeBounds(jc, data, offset, length, options);
+ return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight);
+ }
+
+ private static Bitmap findCachedBitmap(JobContext jc, FileDescriptor fileDescriptor,
+ Options options) {
+ decodeBounds(jc, fileDescriptor, options);
+ return GalleryBitmapPool.getInstance().get(options.outWidth, options.outHeight);
+ }
+}
diff --git a/src/com/android/gallery3d/data/DownloadCache.java b/src/com/android/gallery3d/data/DownloadCache.java
new file mode 100644
index 000000000..be7820b01
--- /dev/null
+++ b/src/com/android/gallery3d/data/DownloadCache.java
@@ -0,0 +1,370 @@
+/*
+ * 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.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+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 java.io.File;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+
+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<String, Entry> mEntryMap =
+ new LruCache<String, Entry>(LRU_CAPACITY);
+ private final HashMap<String, DownloadTask> mTaskMap =
+ new HashMap<String, DownloadTask>();
+ private final File mRoot;
+ private final GalleryApp mApplication;
+ private final SQLiteDatabase mDatabase;
+ private final long mCapacity;
+
+ private long mTotalBytes = 0;
+ private boolean mInitialized = false;
+
+ 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 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);
+ }
+ }
+
+ private class DownloadTask implements Job<File>, FutureListener<File> {
+ private HashSet<TaskProxy> mProxySet = new HashSet<TaskProxy>();
+ private Future<File> 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);
+ }
+
+ @Override
+ public void onFutureDone(Future<File> 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);
+ }
+ }
+
+ @Override
+ 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() {
+ @Override
+ 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..137898e91
--- /dev/null
+++ b/src/com/android/gallery3d/data/DownloadUtils.java
@@ -0,0 +1,79 @@
+/*
+ * 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.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 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() {
+ @Override
+ 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/EmptyAlbumImage.java b/src/com/android/gallery3d/data/EmptyAlbumImage.java
new file mode 100644
index 000000000..6f8c37c6b
--- /dev/null
+++ b/src/com/android/gallery3d/data/EmptyAlbumImage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012 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;
+
+public class EmptyAlbumImage extends ActionImage {
+ @SuppressWarnings("unused")
+ private static final String TAG = "EmptyAlbumImage";
+
+ public EmptyAlbumImage(Path path, GalleryApp application) {
+ super(path, application, R.drawable.placeholder_empty);
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return super.getSupportedOperations() | SUPPORT_BACK;
+ }
+}
diff --git a/src/com/android/gallery3d/data/Exif.java b/src/com/android/gallery3d/data/Exif.java
new file mode 100644
index 000000000..950e7de18
--- /dev/null
+++ b/src/com/android/gallery3d/data/Exif.java
@@ -0,0 +1,48 @@
+/*
+ * 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 android.util.Log;
+
+import com.android.gallery3d.exif.ExifInterface;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class Exif {
+ private static final String TAG = "CameraExif";
+
+ // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
+ public static int getOrientation(InputStream is) {
+ if (is == null) {
+ return 0;
+ }
+ ExifInterface exif = new ExifInterface();
+ try {
+ exif.readExif(is);
+ Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+ if (val == null) {
+ return 0;
+ } else {
+ return ExifInterface.getRotationForOrientationValue(val.shortValue());
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to read EXIF orientation", e);
+ return 0;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/Face.java b/src/com/android/gallery3d/data/Face.java
new file mode 100644
index 000000000..d2dc22bfc
--- /dev/null
+++ b/src/com/android/gallery3d/data/Face.java
@@ -0,0 +1,65 @@
+/*
+ * 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 android.graphics.Rect;
+
+import com.android.gallery3d.common.Utils;
+
+import java.util.StringTokenizer;
+
+public class Face implements Comparable<Face> {
+ private String mName;
+ private String mPersonId;
+ private Rect mPosition;
+
+ public Face(String name, String personId, String rect) {
+ mName = name;
+ mPersonId = personId;
+ Utils.assertTrue(mName != null && mPersonId != null && rect != null);
+ StringTokenizer tokenizer = new StringTokenizer(rect);
+ mPosition = new Rect();
+ while (tokenizer.hasMoreElements()) {
+ mPosition.left = Integer.parseInt(tokenizer.nextToken());
+ mPosition.top = Integer.parseInt(tokenizer.nextToken());
+ mPosition.right = Integer.parseInt(tokenizer.nextToken());
+ mPosition.bottom = Integer.parseInt(tokenizer.nextToken());
+ }
+ }
+
+ public Rect getPosition() {
+ return mPosition;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Face) {
+ Face face = (Face) obj;
+ return mPersonId.equals(face.mPersonId);
+ }
+ return false;
+ }
+
+ @Override
+ public int compareTo(Face another) {
+ return mName.compareTo(another.mName);
+ }
+}
diff --git a/src/com/android/gallery3d/data/FaceClustering.java b/src/com/android/gallery3d/data/FaceClustering.java
new file mode 100644
index 000000000..819915edb
--- /dev/null
+++ b/src/com/android/gallery3d/data/FaceClustering.java
@@ -0,0 +1,142 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.Rect;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.picasasource.PicasaSource;
+
+import java.util.ArrayList;
+import java.util.TreeMap;
+
+public class FaceClustering extends Clustering {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FaceClustering";
+
+ private FaceCluster[] mClusters;
+ private String mUntaggedString;
+ private Context mContext;
+
+ private class FaceCluster {
+ ArrayList<Path> mPaths = new ArrayList<Path>();
+ String mName;
+ MediaItem mCoverItem;
+ Rect mCoverRegion;
+ int mCoverFaceIndex;
+
+ public FaceCluster(String name) {
+ mName = name;
+ }
+
+ public void add(MediaItem item, int faceIndex) {
+ Path path = item.getPath();
+ mPaths.add(path);
+ Face[] faces = item.getFaces();
+ if (faces != null) {
+ Face face = faces[faceIndex];
+ if (mCoverItem == null) {
+ mCoverItem = item;
+ mCoverRegion = face.getPosition();
+ mCoverFaceIndex = faceIndex;
+ } else {
+ Rect region = face.getPosition();
+ if (mCoverRegion.width() < region.width() &&
+ mCoverRegion.height() < region.height()) {
+ mCoverItem = item;
+ mCoverRegion = face.getPosition();
+ mCoverFaceIndex = faceIndex;
+ }
+ }
+ }
+ }
+
+ public int size() {
+ return mPaths.size();
+ }
+
+ public MediaItem getCover() {
+ if (mCoverItem != null) {
+ if (PicasaSource.isPicasaImage(mCoverItem)) {
+ return PicasaSource.getFaceItem(mContext, mCoverItem, mCoverFaceIndex);
+ } else {
+ return mCoverItem;
+ }
+ }
+ return null;
+ }
+ }
+
+ public FaceClustering(Context context) {
+ mUntaggedString = context.getResources().getString(R.string.untagged);
+ mContext = context;
+ }
+
+ @Override
+ public void run(MediaSet baseSet) {
+ final TreeMap<Face, FaceCluster> map =
+ new TreeMap<Face, FaceCluster>();
+ final FaceCluster untagged = new FaceCluster(mUntaggedString);
+
+ baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ Face[] faces = item.getFaces();
+ if (faces == null || faces.length == 0) {
+ untagged.add(item, -1);
+ return;
+ }
+ for (int j = 0; j < faces.length; j++) {
+ Face face = faces[j];
+ FaceCluster cluster = map.get(face);
+ if (cluster == null) {
+ cluster = new FaceCluster(face.getName());
+ map.put(face, cluster);
+ }
+ cluster.add(item, j);
+ }
+ }
+ });
+
+ int m = map.size();
+ mClusters = map.values().toArray(new FaceCluster[m + ((untagged.size() > 0) ? 1 : 0)]);
+ if (untagged.size() > 0) {
+ mClusters[m] = untagged;
+ }
+ }
+
+ @Override
+ public int getNumberOfClusters() {
+ return mClusters.length;
+ }
+
+ @Override
+ public ArrayList<Path> getCluster(int index) {
+ return mClusters[index].mPaths;
+ }
+
+ @Override
+ public String getClusterName(int index) {
+ return mClusters[index].mName;
+ }
+
+ @Override
+ public MediaItem getClusterCover(int index) {
+ return mClusters[index].getCover();
+ }
+}
diff --git a/src/com/android/gallery3d/data/FilterDeleteSet.java b/src/com/android/gallery3d/data/FilterDeleteSet.java
new file mode 100644
index 000000000..c76412ff8
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterDeleteSet.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2012 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;
+
+// FilterDeleteSet filters a base MediaSet to remove some deletion items (we
+// expect the number to be small). The user can use the following methods to
+// add/remove deletion items:
+//
+// void addDeletion(Path path, int index);
+// void removeDelection(Path path);
+// void clearDeletion();
+// int getNumberOfDeletions();
+//
+public class FilterDeleteSet extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilterDeleteSet";
+
+ private static final int REQUEST_ADD = 1;
+ private static final int REQUEST_REMOVE = 2;
+ private static final int REQUEST_CLEAR = 3;
+
+ private static class Request {
+ int type; // one of the REQUEST_* constants
+ Path path;
+ int indexHint;
+ public Request(int type, Path path, int indexHint) {
+ this.type = type;
+ this.path = path;
+ this.indexHint = indexHint;
+ }
+ }
+
+ private static class Deletion {
+ Path path;
+ int index;
+ public Deletion(Path path, int index) {
+ this.path = path;
+ this.index = index;
+ }
+ }
+
+ // The underlying MediaSet
+ private final MediaSet mBaseSet;
+
+ // Pending Requests
+ private ArrayList<Request> mRequests = new ArrayList<Request>();
+
+ // Deletions currently in effect, ordered by index
+ private ArrayList<Deletion> mCurrent = new ArrayList<Deletion>();
+
+ public FilterDeleteSet(Path path, MediaSet baseSet) {
+ super(path, INVALID_DATA_VERSION);
+ mBaseSet = baseSet;
+ mBaseSet.addContentListener(this);
+ }
+
+ @Override
+ public boolean isCameraRoll() {
+ return mBaseSet.isCameraRoll();
+ }
+
+ @Override
+ public String getName() {
+ return mBaseSet.getName();
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ return mBaseSet.getMediaItemCount() - mCurrent.size();
+ }
+
+ // Gets the MediaItems whose (post-deletion) index are in the range [start,
+ // start + count). Because we remove some of the MediaItems, the index need
+ // to be adjusted.
+ //
+ // For example, if there are 12 items in total. The deleted items are 3, 5,
+ // 10, and the the requested range is [3, 7]:
+ //
+ // The original index: 0 1 2 3 4 5 6 7 8 9 A B C
+ // The deleted items: X X X
+ // The new index: 0 1 2 3 4 5 6 7 8 9
+ // Requested: * * * * *
+ //
+ // We need to figure out the [3, 7] actually maps to the original index 4,
+ // 6, 7, 8, 9.
+ //
+ // We can break the MediaItems into segments, each segment other than the
+ // last one ends in a deleted item. The difference between the new index and
+ // the original index increases with each segment:
+ //
+ // 0 1 2 X (new index = old index)
+ // 4 X (new index = old index - 1)
+ // 6 7 8 9 X (new index = old index - 2)
+ // B C (new index = old index - 3)
+ //
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ if (count <= 0) return new ArrayList<MediaItem>();
+
+ int end = start + count - 1;
+ int n = mCurrent.size();
+ // Find the segment that "start" falls into. Count the number of items
+ // not yet deleted until it reaches "start".
+ int i = 0;
+ for (i = 0; i < n; i++) {
+ Deletion d = mCurrent.get(i);
+ if (d.index - i > start) break;
+ }
+ // Find the segment that "end" falls into.
+ int j = i;
+ for (; j < n; j++) {
+ Deletion d = mCurrent.get(j);
+ if (d.index - j > end) break;
+ }
+
+ // Now get enough to cover deleted items in [start, end]
+ ArrayList<MediaItem> base = mBaseSet.getMediaItem(start + i, count + (j - i));
+
+ // Remove the deleted items.
+ for (int m = j - 1; m >= i; m--) {
+ Deletion d = mCurrent.get(m);
+ int k = d.index - (start + i);
+ base.remove(k);
+ }
+ return base;
+ }
+
+ // We apply the pending requests in the mRequests to construct mCurrent in reload().
+ @Override
+ public long reload() {
+ boolean newData = mBaseSet.reload() > mDataVersion;
+ synchronized (mRequests) {
+ if (!newData && mRequests.isEmpty()) {
+ return mDataVersion;
+ }
+ for (int i = 0; i < mRequests.size(); i++) {
+ Request r = mRequests.get(i);
+ switch (r.type) {
+ case REQUEST_ADD: {
+ // Add the path into mCurrent if there is no duplicate.
+ int n = mCurrent.size();
+ int j;
+ for (j = 0; j < n; j++) {
+ if (mCurrent.get(j).path == r.path) break;
+ }
+ if (j == n) {
+ mCurrent.add(new Deletion(r.path, r.indexHint));
+ }
+ break;
+ }
+ case REQUEST_REMOVE: {
+ // Remove the path from mCurrent.
+ int n = mCurrent.size();
+ for (int j = 0; j < n; j++) {
+ if (mCurrent.get(j).path == r.path) {
+ mCurrent.remove(j);
+ break;
+ }
+ }
+ break;
+ }
+ case REQUEST_CLEAR: {
+ mCurrent.clear();
+ break;
+ }
+ }
+ }
+ mRequests.clear();
+ }
+
+ if (!mCurrent.isEmpty()) {
+ // See if the elements in mCurrent can be found in the MediaSet. We
+ // don't want to search the whole mBaseSet, so we just search a
+ // small window that contains the index hints (plus some margin).
+ int minIndex = mCurrent.get(0).index;
+ int maxIndex = minIndex;
+ for (int i = 1; i < mCurrent.size(); i++) {
+ Deletion d = mCurrent.get(i);
+ minIndex = Math.min(d.index, minIndex);
+ maxIndex = Math.max(d.index, maxIndex);
+ }
+
+ int n = mBaseSet.getMediaItemCount();
+ int from = Math.max(minIndex - 5, 0);
+ int to = Math.min(maxIndex + 5, n);
+ ArrayList<MediaItem> items = mBaseSet.getMediaItem(from, to - from);
+ ArrayList<Deletion> result = new ArrayList<Deletion>();
+ for (int i = 0; i < items.size(); i++) {
+ MediaItem item = items.get(i);
+ if (item == null) continue;
+ Path p = item.getPath();
+ // Find the matching path in mCurrent, if found move it to result
+ for (int j = 0; j < mCurrent.size(); j++) {
+ Deletion d = mCurrent.get(j);
+ if (d.path == p) {
+ d.index = from + i;
+ result.add(d);
+ mCurrent.remove(j);
+ break;
+ }
+ }
+ }
+ mCurrent = result;
+ }
+
+ mDataVersion = nextVersionNumber();
+ return mDataVersion;
+ }
+
+ private void sendRequest(int type, Path path, int indexHint) {
+ Request r = new Request(type, path, indexHint);
+ synchronized (mRequests) {
+ mRequests.add(r);
+ }
+ notifyContentChanged();
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ public void addDeletion(Path path, int indexHint) {
+ sendRequest(REQUEST_ADD, path, indexHint);
+ }
+
+ public void removeDeletion(Path path) {
+ sendRequest(REQUEST_REMOVE, path, 0 /* unused */);
+ }
+
+ public void clearDeletion() {
+ sendRequest(REQUEST_CLEAR, null /* unused */ , 0 /* unused */);
+ }
+
+ // Returns number of deletions _in effect_ (the number will only gets
+ // updated after a reload()).
+ public int getNumberOfDeletions() {
+ return mCurrent.size();
+ }
+}
diff --git a/src/com/android/gallery3d/data/FilterEmptyPromptSet.java b/src/com/android/gallery3d/data/FilterEmptyPromptSet.java
new file mode 100644
index 000000000..b576e06d4
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterEmptyPromptSet.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2012 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 FilterEmptyPromptSet extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilterEmptyPromptSet";
+
+ private ArrayList<MediaItem> mEmptyItem;
+ private MediaSet mBaseSet;
+
+ public FilterEmptyPromptSet(Path path, MediaSet baseSet, MediaItem emptyItem) {
+ super(path, INVALID_DATA_VERSION);
+ mEmptyItem = new ArrayList<MediaItem>(1);
+ mEmptyItem.add(emptyItem);
+ mBaseSet = baseSet;
+ mBaseSet.addContentListener(this);
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ int itemCount = mBaseSet.getMediaItemCount();
+ if (itemCount > 0) {
+ return itemCount;
+ } else {
+ return 1;
+ }
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ int itemCount = mBaseSet.getMediaItemCount();
+ if (itemCount > 0) {
+ return mBaseSet.getMediaItem(start, count);
+ } else if (start == 0 && count == 1) {
+ return mEmptyItem;
+ } else {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ }
+
+ @Override
+ public void onContentDirty() {
+ notifyContentChanged();
+ }
+
+ @Override
+ public boolean isLeafAlbum() {
+ return true;
+ }
+
+ @Override
+ public boolean isCameraRoll() {
+ return mBaseSet.isCameraRoll();
+ }
+
+ @Override
+ public long reload() {
+ return mBaseSet.reload();
+ }
+
+ @Override
+ public String getName() {
+ return mBaseSet.getName();
+ }
+}
diff --git a/src/com/android/gallery3d/data/FilterSource.java b/src/com/android/gallery3d/data/FilterSource.java
new file mode 100644
index 000000000..d689fe336
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterSource.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.app.GalleryApp;
+
+public class FilterSource extends MediaSource {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilterSource";
+ private static final int FILTER_BY_MEDIATYPE = 0;
+ private static final int FILTER_BY_DELETE = 1;
+ private static final int FILTER_BY_EMPTY = 2;
+ private static final int FILTER_BY_EMPTY_ITEM = 3;
+ private static final int FILTER_BY_CAMERA_SHORTCUT = 4;
+ private static final int FILTER_BY_CAMERA_SHORTCUT_ITEM = 5;
+
+ public static final String FILTER_EMPTY_ITEM = "/filter/empty_prompt";
+ public static final String FILTER_CAMERA_SHORTCUT = "/filter/camera_shortcut";
+ private static final String FILTER_CAMERA_SHORTCUT_ITEM = "/filter/camera_shortcut_item";
+
+ private GalleryApp mApplication;
+ private PathMatcher mMatcher;
+ private MediaItem mEmptyItem;
+ private MediaItem mCameraShortcutItem;
+
+ public FilterSource(GalleryApp application) {
+ super("filter");
+ mApplication = application;
+ mMatcher = new PathMatcher();
+ mMatcher.add("/filter/mediatype/*/*", FILTER_BY_MEDIATYPE);
+ mMatcher.add("/filter/delete/*", FILTER_BY_DELETE);
+ mMatcher.add("/filter/empty/*", FILTER_BY_EMPTY);
+ mMatcher.add(FILTER_EMPTY_ITEM, FILTER_BY_EMPTY_ITEM);
+ mMatcher.add(FILTER_CAMERA_SHORTCUT, FILTER_BY_CAMERA_SHORTCUT);
+ mMatcher.add(FILTER_CAMERA_SHORTCUT_ITEM, FILTER_BY_CAMERA_SHORTCUT_ITEM);
+
+ mEmptyItem = new EmptyAlbumImage(Path.fromString(FILTER_EMPTY_ITEM),
+ mApplication);
+ mCameraShortcutItem = new CameraShortcutImage(
+ Path.fromString(FILTER_CAMERA_SHORTCUT_ITEM), mApplication);
+ }
+
+ // The name we accept are:
+ // /filter/mediatype/k/{set} where k is the media type we want.
+ // /filter/delete/{set}
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ int matchType = mMatcher.match(path);
+ DataManager dataManager = mApplication.getDataManager();
+ switch (matchType) {
+ case FILTER_BY_MEDIATYPE: {
+ int mediaType = mMatcher.getIntVar(0);
+ String setsName = mMatcher.getVar(1);
+ MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+ return new FilterTypeSet(path, dataManager, sets[0], mediaType);
+ }
+ case FILTER_BY_DELETE: {
+ String setsName = mMatcher.getVar(0);
+ MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+ return new FilterDeleteSet(path, sets[0]);
+ }
+ case FILTER_BY_EMPTY: {
+ String setsName = mMatcher.getVar(0);
+ MediaSet[] sets = dataManager.getMediaSetsFromString(setsName);
+ return new FilterEmptyPromptSet(path, sets[0], mEmptyItem);
+ }
+ case FILTER_BY_EMPTY_ITEM: {
+ return mEmptyItem;
+ }
+ case FILTER_BY_CAMERA_SHORTCUT: {
+ return new SingleItemAlbum(path, mCameraShortcutItem);
+ }
+ case FILTER_BY_CAMERA_SHORTCUT_ITEM: {
+ return mCameraShortcutItem;
+ }
+ default:
+ throw new RuntimeException("bad path: " + path);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/FilterTypeSet.java b/src/com/android/gallery3d/data/FilterTypeSet.java
new file mode 100644
index 000000000..477ef73ad
--- /dev/null
+++ b/src/com/android/gallery3d/data/FilterTypeSet.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;
+
+// FilterTypeSet filters a base MediaSet according to a matching media type.
+public class FilterTypeSet extends MediaSet implements ContentListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FilterTypeSet";
+
+ private final DataManager mDataManager;
+ private final MediaSet mBaseSet;
+ private final int mMediaType;
+ private final ArrayList<Path> mPaths = new ArrayList<Path>();
+ private final ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>();
+
+ public FilterTypeSet(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<MediaItem> 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() {
+ @Override
+ 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() {
+ @Override
+ 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/ImageCacheRequest.java b/src/com/android/gallery3d/data/ImageCacheRequest.java
new file mode 100644
index 000000000..6cbc5c5ea
--- /dev/null
+++ b/src/com/android/gallery3d/data/ImageCacheRequest.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 android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.data.BytesBufferPool.BytesBuffer;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+abstract class ImageCacheRequest implements Job<Bitmap> {
+ private static final String TAG = "ImageCacheRequest";
+
+ protected GalleryApp mApplication;
+ private Path mPath;
+ private int mType;
+ private int mTargetSize;
+ private long mTimeModified;
+
+ public ImageCacheRequest(GalleryApp application,
+ Path path, long timeModified, int type, int targetSize) {
+ mApplication = application;
+ mPath = path;
+ mType = type;
+ mTargetSize = targetSize;
+ mTimeModified = timeModified;
+ }
+
+ private String debugTag() {
+ return mPath + "," + mTimeModified + "," +
+ ((mType == MediaItem.TYPE_THUMBNAIL) ? "THUMB" :
+ (mType == MediaItem.TYPE_MICROTHUMBNAIL) ? "MICROTHUMB" : "?");
+ }
+
+ @Override
+ public Bitmap run(JobContext jc) {
+ ImageCacheService cacheService = mApplication.getImageCacheService();
+
+ BytesBuffer buffer = MediaItem.getBytesBufferPool().get();
+ try {
+ boolean found = cacheService.getImageData(mPath, mTimeModified, mType, buffer);
+ if (jc.isCancelled()) return null;
+ if (found) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ Bitmap bitmap;
+ if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+ bitmap = DecodeUtils.decodeUsingPool(jc,
+ buffer.data, buffer.offset, buffer.length, options);
+ } else {
+ bitmap = DecodeUtils.decodeUsingPool(jc,
+ buffer.data, buffer.offset, buffer.length, options);
+ }
+ if (bitmap == null && !jc.isCancelled()) {
+ Log.w(TAG, "decode cached failed " + debugTag());
+ }
+ return bitmap;
+ }
+ } finally {
+ MediaItem.getBytesBufferPool().recycle(buffer);
+ }
+ 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.resizeAndCropCenter(bitmap, mTargetSize, true);
+ } else {
+ bitmap = BitmapUtils.resizeDownBySideLength(bitmap, mTargetSize, true);
+ }
+ if (jc.isCancelled()) return null;
+
+ byte[] array = BitmapUtils.compressToBytes(bitmap);
+ if (jc.isCancelled()) return null;
+
+ cacheService.putImageData(mPath, mTimeModified, 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..1c7cb8c5e
--- /dev/null
+++ b/src/com/android/gallery3d/data/ImageCacheService.java
@@ -0,0 +1,123 @@
+/*
+ * 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.content.Context;
+
+import com.android.gallery3d.common.BlobCache;
+import com.android.gallery3d.common.BlobCache.LookupRequest;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.BytesBufferPool.BytesBuffer;
+import com.android.gallery3d.util.CacheManager;
+import com.android.gallery3d.util.GalleryUtils;
+
+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 = 7;
+
+ 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);
+ }
+
+ /**
+ * Gets the cached image data for the given <code>path</code>,
+ * <code>timeModified</code> and <code>type</code>.
+ *
+ * The image data will be stored in <code>buffer.data</code>, started from
+ * <code>buffer.offset</code> for <code>buffer.length</code> bytes. If the
+ * buffer.data is not big enough, a new byte array will be allocated and returned.
+ *
+ * @return true if the image data is found; false if not found.
+ */
+ public boolean getImageData(Path path, long timeModified, int type, BytesBuffer buffer) {
+ byte[] key = makeKey(path, timeModified, type);
+ long cacheKey = Utils.crc64Long(key);
+ try {
+ LookupRequest request = new LookupRequest();
+ request.key = cacheKey;
+ request.buffer = buffer.data;
+ synchronized (mCache) {
+ if (!mCache.lookup(request)) return false;
+ }
+ if (isSameKey(key, request.buffer)) {
+ buffer.data = request.buffer;
+ buffer.offset = key.length;
+ buffer.length = request.length - buffer.offset;
+ return true;
+ }
+ } catch (IOException ex) {
+ // ignore.
+ }
+ return false;
+ }
+
+ public void putImageData(Path path, long timeModified, int type, byte[] value) {
+ byte[] key = makeKey(path, timeModified, 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.
+ }
+ }
+ }
+
+ public void clearImageData(Path path, long timeModified, int type) {
+ byte[] key = makeKey(path, timeModified, type);
+ long cacheKey = Utils.crc64Long(key);
+ synchronized (mCache) {
+ try {
+ mCache.clearEntry(cacheKey);
+ } catch (IOException ex) {
+ // ignore.
+ }
+ }
+ }
+
+ private static byte[] makeKey(Path path, long timeModified, int type) {
+ return GalleryUtils.getBytes(path.toString() + "+" + timeModified + "+" + 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..7b7015af6
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalAlbum.java
@@ -0,0 +1,325 @@
+/*
+ * 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.content.ContentResolver;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.BucketNames;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MediaSetUtils;
+
+import java.io.File;
+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 mName;
+ 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;
+ mName = 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,
+ BucketHelper.getBucketName(
+ application.getContentResolver(), bucketId));
+ }
+
+ @Override
+ public boolean isCameraRoll() {
+ return mBucketId == MediaSetUtils.CAMERA_BUCKET_ID;
+ }
+
+ @Override
+ public Uri getContentUri() {
+ if (mIsImage) {
+ return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
+ .appendQueryParameter(LocalSource.KEY_BUCKET_ID,
+ String.valueOf(mBucketId)).build();
+ } else {
+ return MediaStore.Video.Media.EXTERNAL_CONTENT_URI.buildUpon()
+ .appendQueryParameter(LocalSource.KEY_BUCKET_ID,
+ String.valueOf(mBucketId)).build();
+ }
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ DataManager dataManager = mApplication.getDataManager();
+ Uri uri = mBaseUri.buildUpon()
+ .appendQueryParameter("limit", start + "," + count).build();
+ ArrayList<MediaItem> list = new ArrayList<MediaItem>();
+ 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) {
+ synchronized (DataManager.LOCK) {
+ 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<Integer> 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 getLocalizedName(mApplication.getResources(), mBucketId, mName);
+ }
+
+ @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;
+ }
+
+ public static String getLocalizedName(Resources res, int bucketId,
+ String name) {
+ if (bucketId == MediaSetUtils.CAMERA_BUCKET_ID) {
+ return res.getString(R.string.folder_camera);
+ } else if (bucketId == MediaSetUtils.DOWNLOAD_BUCKET_ID) {
+ return res.getString(R.string.folder_download);
+ } else if (bucketId == MediaSetUtils.IMPORTED_BUCKET_ID) {
+ return res.getString(R.string.folder_imported);
+ } else if (bucketId == MediaSetUtils.SNAPSHOT_BUCKET_ID) {
+ return res.getString(R.string.folder_screenshot);
+ } else if (bucketId == MediaSetUtils.EDITED_ONLINE_PHOTOS_BUCKET_ID) {
+ return res.getString(R.string.folder_edited_online_photos);
+ } else {
+ return name;
+ }
+ }
+
+ // Relative path is the absolute path minus external storage path
+ public static String getRelativePath(int bucketId) {
+ String relativePath = "/";
+ if (bucketId == MediaSetUtils.CAMERA_BUCKET_ID) {
+ relativePath += BucketNames.CAMERA;
+ } else if (bucketId == MediaSetUtils.DOWNLOAD_BUCKET_ID) {
+ relativePath += BucketNames.DOWNLOAD;
+ } else if (bucketId == MediaSetUtils.IMPORTED_BUCKET_ID) {
+ relativePath += BucketNames.IMPORTED;
+ } else if (bucketId == MediaSetUtils.SNAPSHOT_BUCKET_ID) {
+ relativePath += BucketNames.SCREENSHOTS;
+ } else if (bucketId == MediaSetUtils.EDITED_ONLINE_PHOTOS_BUCKET_ID) {
+ relativePath += BucketNames.EDITED_ONLINE_PHOTOS;
+ } else {
+ // If the first few cases didn't hit the matching path, do a
+ // thorough search in the local directories.
+ File extStorage = Environment.getExternalStorageDirectory();
+ String path = GalleryUtils.searchDirForPath(extStorage, bucketId);
+ if (path == null) {
+ Log.w(TAG, "Relative path for bucket id: " + bucketId + " is not found.");
+ relativePath = null;
+ } else {
+ relativePath = path.substring(extStorage.getAbsolutePath().length());
+ }
+ }
+ return relativePath;
+ }
+
+}
diff --git a/src/com/android/gallery3d/data/LocalAlbumSet.java b/src/com/android/gallery3d/data/LocalAlbumSet.java
new file mode 100644
index 000000000..b2b4b8c5d
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalAlbumSet.java
@@ -0,0 +1,211 @@
+/*
+ * 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;
+import android.os.Handler;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.BucketHelper.BucketEntry;
+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 com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+// 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
+ implements FutureListener<ArrayList<MediaSet>> {
+ @SuppressWarnings("unused")
+ private static final String TAG = "LocalAlbumSet";
+
+ 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 Uri[] mWatchUris =
+ {Images.Media.EXTERNAL_CONTENT_URI, Video.Media.EXTERNAL_CONTENT_URI};
+
+ private final GalleryApp mApplication;
+ private final int mType;
+ private ArrayList<MediaSet> mAlbums = new ArrayList<MediaSet>();
+ private final ChangeNotifier mNotifier;
+ private final String mName;
+ private final Handler mHandler;
+ private boolean mIsLoading;
+
+ private Future<ArrayList<MediaSet>> mLoadTask;
+ private ArrayList<MediaSet> mLoadBuffer;
+
+ public LocalAlbumSet(Path path, GalleryApp application) {
+ super(path, nextVersionNumber());
+ mApplication = application;
+ mHandler = new Handler(application.getMainLooper());
+ mType = getTypeFromPath(path);
+ mNotifier = new ChangeNotifier(this, mWatchUris, 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());
+ }
+ return getTypeFromString(name[1]);
+ }
+
+ @Override
+ public MediaSet getSubMediaSet(int index) {
+ return mAlbums.get(index);
+ }
+
+ @Override
+ public int getSubMediaSetCount() {
+ return mAlbums.size();
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ 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;
+ }
+
+ private class AlbumsLoader implements ThreadPool.Job<ArrayList<MediaSet>> {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public ArrayList<MediaSet> run(JobContext jc) {
+ // Note: it will be faster if we only select media_type and bucket_id.
+ // need to test the performance if that is worth
+ BucketEntry[] entries = BucketHelper.loadBucketEntries(
+ jc, mApplication.getContentResolver(), mType);
+
+ if (jc.isCancelled()) return null;
+
+ int offset = 0;
+ // Move camera and download bucket to the front, while keeping the
+ // order of others.
+ int index = findBucket(entries, MediaSetUtils.CAMERA_BUCKET_ID);
+ if (index != -1) {
+ circularShiftRight(entries, offset++, index);
+ }
+ index = findBucket(entries, MediaSetUtils.DOWNLOAD_BUCKET_ID);
+ if (index != -1) {
+ circularShiftRight(entries, offset++, index);
+ }
+
+ ArrayList<MediaSet> albums = new ArrayList<MediaSet>();
+ DataManager dataManager = mApplication.getDataManager();
+ for (BucketEntry entry : entries) {
+ MediaSet album = getLocalAlbum(dataManager,
+ mType, mPath, entry.bucketId, entry.bucketName);
+ albums.add(album);
+ }
+ return albums;
+ }
+ }
+
+ private MediaSet getLocalAlbum(
+ DataManager manager, int type, Path parent, int id, String name) {
+ synchronized (DataManager.LOCK) {
+ 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<MediaItem> 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)}, id);
+ }
+ throw new IllegalArgumentException(String.valueOf(type));
+ }
+ }
+
+ @Override
+ public synchronized boolean isLoading() {
+ return mIsLoading;
+ }
+
+ @Override
+ // synchronized on this function for
+ // 1. Prevent calling reload() concurrently.
+ // 2. Prevent calling onFutureDone() and reload() concurrently
+ public synchronized long reload() {
+ if (mNotifier.isDirty()) {
+ if (mLoadTask != null) mLoadTask.cancel();
+ mIsLoading = true;
+ mLoadTask = mApplication.getThreadPool().submit(new AlbumsLoader(), this);
+ }
+ if (mLoadBuffer != null) {
+ mAlbums = mLoadBuffer;
+ mLoadBuffer = null;
+ for (MediaSet album : mAlbums) {
+ album.reload();
+ }
+ mDataVersion = nextVersionNumber();
+ }
+ return mDataVersion;
+ }
+
+ @Override
+ public synchronized void onFutureDone(Future<ArrayList<MediaSet>> future) {
+ if (mLoadTask != future) return; // ignore, wait for the latest task
+ mLoadBuffer = future.get();
+ mIsLoading = false;
+ if (mLoadBuffer == null) mLoadBuffer = new ArrayList<MediaSet>();
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ notifyContentChanged();
+ }
+ });
+ }
+
+ // For debug only. Fake there is a ContentObserver.onChange() event.
+ void fakeChange() {
+ mNotifier.fakeChange();
+ }
+
+ // Circular shift the array range from a[i] to a[j] (inclusive). That is,
+ // a[i] -> a[i+1] -> a[i+2] -> ... -> a[j], and a[j] -> a[i]
+ private static <T> void circularShiftRight(T[] array, int i, int j) {
+ T temp = array[j];
+ for (int k = j; k > i; k--) {
+ array[k] = array[k - 1];
+ }
+ array[i] = temp;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
new file mode 100644
index 000000000..cc70dd457
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -0,0 +1,355 @@
+/*
+ * 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.annotation.TargetApi;
+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.net.Uri;
+import android.os.Build;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.MediaColumns;
+import android.util.Log;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.PanoramaMetadataSupport;
+import com.android.gallery3d.app.StitchingProgressManager;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.gallery3d.util.UpdateHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+// LocalImage represents an image in the local storage.
+public class LocalImage extends LocalMediaItem {
+ 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 = 11;
+ private static final int INDEX_WIDTH = 12;
+ private static final int INDEX_HEIGHT = 13;
+
+ 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
+ "0", // 12
+ "0" // 13
+ };
+
+ static {
+ updateWidthAndHeightProjection();
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private static void updateWidthAndHeightProjection() {
+ if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) {
+ PROJECTION[INDEX_WIDTH] = MediaColumns.WIDTH;
+ PROJECTION[INDEX_HEIGHT] = MediaColumns.HEIGHT;
+ }
+ }
+
+ private final GalleryApp mApplication;
+
+ public int rotation;
+
+ private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this);
+
+ 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);
+ dateAddedInSec = cursor.getLong(INDEX_DATE_ADDED);
+ dateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED);
+ filePath = cursor.getString(INDEX_DATA);
+ rotation = cursor.getInt(INDEX_ORIENTATION);
+ bucketId = cursor.getInt(INDEX_BUCKET_ID);
+ fileSize = cursor.getLong(INDEX_SIZE);
+ width = cursor.getInt(INDEX_WIDTH);
+ height = cursor.getInt(INDEX_HEIGHT);
+ }
+
+ @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));
+ width = uh.update(width, cursor.getInt(INDEX_WIDTH));
+ height = uh.update(height, cursor.getInt(INDEX_HEIGHT));
+ return uh.isUpdated();
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new LocalImageRequest(mApplication, mPath, dateModifiedInSec,
+ type, filePath);
+ }
+
+ public static class LocalImageRequest extends ImageCacheRequest {
+ private String mLocalFilePath;
+
+ LocalImageRequest(GalleryApp application, Path path, long timeModified,
+ int type, String localFilePath) {
+ super(application, path, timeModified, type,
+ MediaItem.getTargetSize(type));
+ mLocalFilePath = localFilePath;
+ }
+
+ @Override
+ public Bitmap onDecodeOriginal(JobContext jc, final int type) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ int targetSize = MediaItem.getTargetSize(type);
+
+ // try to decode from JPEG EXIF
+ if (type == MediaItem.TYPE_MICROTHUMBNAIL) {
+ ExifInterface exif = new ExifInterface();
+ byte[] thumbData = null;
+ try {
+ exif.readExif(mLocalFilePath);
+ thumbData = exif.getThumbnail();
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "failed to find file to read thumbnail: " + mLocalFilePath);
+ } catch (IOException e) {
+ Log.w(TAG, "failed to get thumbnail from: " + mLocalFilePath);
+ }
+ if (thumbData != null) {
+ Bitmap bitmap = DecodeUtils.decodeIfBigEnough(
+ jc, thumbData, options, targetSize);
+ if (bitmap != null) return bitmap;
+ }
+ }
+
+ return DecodeUtils.decodeThumbnail(jc, mLocalFilePath, options, targetSize, type);
+ }
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> requestLargeImage() {
+ return new LocalLargeImageRequest(filePath);
+ }
+
+ public static class LocalLargeImageRequest
+ implements Job<BitmapRegionDecoder> {
+ String mLocalFilePath;
+
+ public LocalLargeImageRequest(String localFilePath) {
+ mLocalFilePath = localFilePath;
+ }
+
+ @Override
+ public BitmapRegionDecoder run(JobContext jc) {
+ return DecodeUtils.createBitmapRegionDecoder(jc, mLocalFilePath, false);
+ }
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ StitchingProgressManager progressManager = mApplication.getStitchingProgressManager();
+ if (progressManager != null && progressManager.getProgress(getContentUri()) != null) {
+ return 0; // doesn't support anything while stitching!
+ }
+ 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 getPanoramaSupport(PanoramaSupportCallback callback) {
+ mPanoramaMetadata.getPanoramaSupport(mApplication, callback);
+ }
+
+ @Override
+ public void clearCachedPanoramaSupport() {
+ mPanoramaMetadata.clearCachedValues();
+ }
+
+ @Override
+ public void delete() {
+ GalleryUtils.assertNotInRenderThread();
+ Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
+ ContentResolver contentResolver = mApplication.getContentResolver();
+ SaveImage.deleteAuxFiles(contentResolver, getContentUri());
+ contentResolver.delete(baseUri, "_id=?",
+ new String[]{String.valueOf(id)});
+ }
+
+ @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")) {
+ ExifInterface exifInterface = new ExifInterface();
+ ExifTag tag = exifInterface.buildTag(ExifInterface.TAG_ORIENTATION,
+ ExifInterface.getOrientationValueForRotation(rotation));
+ if(tag != null) {
+ exifInterface.setTag(tag);
+ try {
+ exifInterface.forceRewriteExif(filePath);
+ fileSize = new File(filePath).length();
+ values.put(Images.Media.SIZE, fileSize);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "cannot find file to set exif: " + filePath);
+ } catch (IOException e) {
+ Log.w(TAG, "cannot set exif data: " + filePath);
+ }
+ } else {
+ Log.w(TAG, "Could not build tag: " + ExifInterface.TAG_ORIENTATION);
+ }
+ }
+
+ 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));
+ if (MIME_TYPE_JPEG.equals(mimeType)) {
+ // ExifInterface returns incorrect values for photos in other format.
+ // For example, the width and height of an webp images is always '0'.
+ MediaDetails.extractExifInfo(details, filePath);
+ }
+ return details;
+ }
+
+ @Override
+ public int getRotation() {
+ return rotation;
+ }
+
+ @Override
+ public int getWidth() {
+ return width;
+ }
+
+ @Override
+ public int getHeight() {
+ return height;
+ }
+
+ @Override
+ public String getFilePath() {
+ return filePath;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocalMediaItem.java b/src/com/android/gallery3d/data/LocalMediaItem.java
new file mode 100644
index 000000000..7e003cd3a
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalMediaItem.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 android.database.Cursor;
+
+import com.android.gallery3d.util.GalleryUtils;
+
+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 int width;
+ public int height;
+
+ 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(dateModifiedInSec * 1000)));
+ details.addDetail(MediaDetails.INDEX_WIDTH, width);
+ details.addDetail(MediaDetails.INDEX_HEIGHT, height);
+
+ 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;
+ }
+
+ @Override
+ 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..f0b5e5726
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalMergeAlbum.java
@@ -0,0 +1,257 @@
+/*
+ * 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;
+import android.provider.MediaStore;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.lang.ref.SoftReference;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.NoSuchElementException;
+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<MediaItem> mComparator;
+ private final MediaSet[] mSources;
+
+ private FetchCache[] mFetcher;
+ private int mSupportedOperation;
+ private int mBucketId;
+
+ // mIndex maps global position to the position of each underlying media sets.
+ private TreeMap<Integer, int[]> mIndex = new TreeMap<Integer, int[]>();
+
+ public LocalMergeAlbum(
+ Path path, Comparator<MediaItem> comparator, MediaSet[] sources, int bucketId) {
+ super(path, INVALID_DATA_VERSION);
+ mComparator = comparator;
+ mSources = sources;
+ mBucketId = bucketId;
+ for (MediaSet set : mSources) {
+ set.addContentListener(this);
+ }
+ reload();
+ }
+
+ @Override
+ public boolean isCameraRoll() {
+ if (mSources.length == 0) return false;
+ for(MediaSet set : mSources) {
+ if (!set.isCameraRoll()) return false;
+ }
+ return true;
+ }
+
+ private void updateData() {
+ ArrayList<MediaSet> matches = new ArrayList<MediaSet>();
+ 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]);
+ }
+
+ 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 Uri getContentUri() {
+ String bucketId = String.valueOf(mBucketId);
+ if (ApiHelper.HAS_MEDIA_PROVIDER_FILES_TABLE) {
+ return MediaStore.Files.getContentUri("external").buildUpon()
+ .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId)
+ .build();
+ } else {
+ // We don't have a single URL for a merged image before ICS
+ // So we used the image's URL as a substitute.
+ return MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
+ .appendQueryParameter(LocalSource.KEY_BUCKET_ID, bucketId)
+ .build();
+ }
+ }
+
+ @Override
+ public String getName() {
+ return mSources.length == 0 ? "" : mSources[0].getName();
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ return getTotalMediaItemCount();
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+
+ // First find the nearest mark position <= start.
+ SortedMap<Integer, int[]> 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<MediaItem> result = new ArrayList<MediaItem>();
+
+ 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<ArrayList<MediaItem>> mCacheRef;
+ private int mStartPos;
+
+ public FetchCache(MediaSet baseSet) {
+ mBaseSet = baseSet;
+ }
+
+ public void invalidate() {
+ mCacheRef = null;
+ }
+
+ public MediaItem getItem(int index) {
+ boolean needLoading = false;
+ ArrayList<MediaItem> 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<ArrayList<MediaItem>>(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..a2e3d1443
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalSource.java
@@ -0,0 +1,275 @@
+/*
+ * 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.content.ContentProviderClient;
+import android.content.ContentUris;
+import android.content.UriMatcher;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import com.android.gallery3d.app.Gallery;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+
+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<PathId> 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);
+ mUriMatcher.addURI(MediaStore.AUTHORITY,
+ "external/file", LOCAL_ALL_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<MediaItem> comp = DataManager.sDateTakenComparator;
+ return new LocalMergeAlbum(
+ path, comp, new MediaSet[] {imageSet, videoSet}, bucketId);
+ }
+ 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_ALL = 0;
+ 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("/local/all").getChild(id);
+ }
+ }
+
+ @Override
+ public Path findPathByUri(Uri uri, String type) {
+ 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);
+ }
+ case LOCAL_ALL_ALBUM: {
+ return getAlbumPath(uri, MEDIA_TYPE_ALL);
+ }
+ }
+ } 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 LocalMediaItem) {
+ return Path.fromString("/local/all").getChild(
+ String.valueOf(((LocalMediaItem) object).getBucketId()));
+ }
+ return null;
+ }
+
+ @Override
+ public void mapMediaItems(ArrayList<PathId> list, ItemConsumer consumer) {
+ ArrayList<PathId> imageList = new ArrayList<PathId>();
+ ArrayList<PathId> videoList = new ArrayList<PathId>();
+ 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<PathId> 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<Integer> ids = new ArrayList<Integer>();
+ 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<PathId> {
+ @Override
+ 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..4b8774ca4
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocalVideo.java
@@ -0,0 +1,242 @@
+/*
+ * 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.content.ContentResolver;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+import android.net.Uri;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.gallery3d.util.UpdateHelper;
+
+// LocalVideo represents a video in the local storage.
+public class LocalVideo extends LocalMediaItem {
+ private static final String TAG = "LocalVideo";
+ 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 = 11;
+ private static final int INDEX_RESOLUTION = 12;
+
+ 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,
+ VideoColumns.RESOLUTION,
+ };
+
+ private final GalleryApp mApplication;
+
+ 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);
+ dateAddedInSec = cursor.getLong(INDEX_DATE_ADDED);
+ dateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED);
+ filePath = cursor.getString(INDEX_DATA);
+ durationInSec = cursor.getInt(INDEX_DURATION) / 1000;
+ bucketId = cursor.getInt(INDEX_BUCKET_ID);
+ fileSize = cursor.getLong(INDEX_SIZE);
+ parseResolution(cursor.getString(INDEX_RESOLUTION));
+ }
+
+ private void parseResolution(String resolution) {
+ if (resolution == null) return;
+ int m = resolution.indexOf('x');
+ if (m == -1) return;
+ try {
+ int w = Integer.parseInt(resolution.substring(0, m));
+ int h = Integer.parseInt(resolution.substring(m + 1));
+ width = w;
+ height = h;
+ } catch (Throwable t) {
+ Log.w(TAG, t);
+ }
+ }
+
+ @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));
+ return uh.isUpdated();
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new LocalVideoRequest(mApplication, getPath(), dateModifiedInSec,
+ type, filePath);
+ }
+
+ public static class LocalVideoRequest extends ImageCacheRequest {
+ private String mLocalFilePath;
+
+ LocalVideoRequest(GalleryApp application, Path path, long timeModified,
+ int type, String localFilePath) {
+ super(application, path, timeModified, type,
+ MediaItem.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<BitmapRegionDecoder> 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 | SUPPORT_TRIM | SUPPORT_MUTE;
+ }
+
+ @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 getContentUri();
+ }
+
+ @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;
+ }
+
+ @Override
+ public int getWidth() {
+ return width;
+ }
+
+ @Override
+ public int getHeight() {
+ return height;
+ }
+
+ @Override
+ public String getFilePath() {
+ return filePath;
+ }
+}
diff --git a/src/com/android/gallery3d/data/LocationClustering.java b/src/com/android/gallery3d/data/LocationClustering.java
new file mode 100644
index 000000000..540322a33
--- /dev/null
+++ b/src/com/android/gallery3d/data/LocationClustering.java
@@ -0,0 +1,316 @@
+/*
+ * 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.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.FloatMath;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ReverseGeocoder;
+
+import java.util.ArrayList;
+
+class LocationClustering extends Clustering {
+ @SuppressWarnings("unused")
+ 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<ArrayList<SmallItem>> mClusters;
+ private ArrayList<String> mNames;
+ private String mNoLocationString;
+ private Handler mHandler;
+
+ 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);
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ @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() {
+ @Override
+ 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<SmallItem> withLatLong = new ArrayList<SmallItem>();
+ final ArrayList<SmallItem> withoutLatLong = new ArrayList<SmallItem>();
+ final ArrayList<Point> points = new ArrayList<Point>();
+ 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<ArrayList<SmallItem>> clusters = new ArrayList<ArrayList<SmallItem>>();
+
+ 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<SmallItem>());
+ }
+
+ for (int i = 0; i < m; i++) {
+ clusters.get(index[i]).add(withLatLong.get(i));
+ }
+ }
+
+ ReverseGeocoder geocoder = new ReverseGeocoder(mContext);
+ mNames = new ArrayList<String>();
+ boolean hasUnresolvedAddress = false;
+ mClusters = new ArrayList<ArrayList<SmallItem>>();
+ for (ArrayList<SmallItem> 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) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(mContext, R.string.no_connectivity,
+ Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+ }
+
+ private static String generateName(ArrayList<SmallItem> 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<Path> getCluster(int index) {
+ ArrayList<SmallItem> items = mClusters.get(index);
+ ArrayList<Path> result = new ArrayList<Path>(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 * FloatMath.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..cac524b88
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaDetails.java
@@ -0,0 +1,170 @@
+/*
+ * 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.common.Utils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.exif.Rational;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+public class MediaDetails implements Iterable<Entry<Integer, Object>> {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MediaDetails";
+
+ private TreeMap<Integer, Object> mDetails = new TreeMap<Integer, Object>();
+ private HashMap<Integer, Integer> mUnits = new HashMap<Integer, Integer>();
+
+ 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 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();
+ }
+
+ @Override
+ public Iterator<Entry<Integer, Object>> 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, ExifTag tag,
+ int key) {
+ if (tag != null) {
+ String value = null;
+ int type = tag.getDataType();
+ if (type == ExifTag.TYPE_UNSIGNED_RATIONAL || type == ExifTag.TYPE_RATIONAL) {
+ value = String.valueOf(tag.getValueAsRational(0).toDouble());
+ } else if (type == ExifTag.TYPE_ASCII) {
+ value = tag.getValueAsString();
+ } else {
+ value = String.valueOf(tag.forceGetValueAsLong(0));
+ }
+ 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) {
+
+ ExifInterface exif = new ExifInterface();
+ try {
+ exif.readExif(filePath);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Could not find file to read exif: " + filePath, e);
+ } catch (IOException e) {
+ Log.w(TAG, "Could not read exif from file: " + filePath, e);
+ }
+
+ setExifData(details, exif.getTag(ExifInterface.TAG_FLASH),
+ MediaDetails.INDEX_FLASH);
+ setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_WIDTH),
+ MediaDetails.INDEX_WIDTH);
+ setExifData(details, exif.getTag(ExifInterface.TAG_IMAGE_LENGTH),
+ MediaDetails.INDEX_HEIGHT);
+ setExifData(details, exif.getTag(ExifInterface.TAG_MAKE),
+ MediaDetails.INDEX_MAKE);
+ setExifData(details, exif.getTag(ExifInterface.TAG_MODEL),
+ MediaDetails.INDEX_MODEL);
+ setExifData(details, exif.getTag(ExifInterface.TAG_APERTURE_VALUE),
+ MediaDetails.INDEX_APERTURE);
+ setExifData(details, exif.getTag(ExifInterface.TAG_ISO_SPEED_RATINGS),
+ MediaDetails.INDEX_ISO);
+ setExifData(details, exif.getTag(ExifInterface.TAG_WHITE_BALANCE),
+ MediaDetails.INDEX_WHITE_BALANCE);
+ setExifData(details, exif.getTag(ExifInterface.TAG_EXPOSURE_TIME),
+ MediaDetails.INDEX_EXPOSURE_TIME);
+ ExifTag focalTag = exif.getTag(ExifInterface.TAG_FOCAL_LENGTH);
+ if (focalTag != null) {
+ details.addDetail(MediaDetails.INDEX_FOCAL_LENGTH,
+ focalTag.getValueAsRational(0).toDouble());
+ details.setUnit(MediaDetails.INDEX_FOCAL_LENGTH, R.string.unit_mm);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/MediaItem.java b/src/com/android/gallery3d/data/MediaItem.java
new file mode 100644
index 000000000..59ea86551
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaItem.java
@@ -0,0 +1,134 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.util.ThreadPool.Job;
+
+// 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 CACHED_IMAGE_QUALITY = 95;
+
+ public static final int IMAGE_READY = 0;
+ public static final int IMAGE_WAIT = 1;
+ public static final int IMAGE_ERROR = -1;
+
+ public static final String MIME_TYPE_JPEG = "image/jpeg";
+
+ private static final int BYTESBUFFE_POOL_SIZE = 4;
+ private static final int BYTESBUFFER_SIZE = 200 * 1024;
+
+ private static int sMicrothumbnailTargetSize = 200;
+ private static final BytesBufferPool sMicroThumbBufferPool =
+ new BytesBufferPool(BYTESBUFFE_POOL_SIZE, BYTESBUFFER_SIZE);
+
+ private static int sThumbnailTargetSize = 640;
+
+ // TODO: fix default value for latlng and change this.
+ public static final double INVALID_LATLNG = 0f;
+
+ public abstract Job<Bitmap> requestImage(int type);
+ public abstract Job<BitmapRegionDecoder> 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;
+ }
+
+ // The rotation of the full-resolution image. By default, it returns the value of
+ // getRotation().
+ public int getFullImageRotation() {
+ return getRotation();
+ }
+
+ public int getRotation() {
+ return 0;
+ }
+
+ public long getSize() {
+ return 0;
+ }
+
+ public abstract String getMimeType();
+
+ public String getFilePath() {
+ return "";
+ }
+
+ // Returns width and height of the media item.
+ // Returns 0, 0 if the information is not available.
+ public abstract int getWidth();
+ public abstract int getHeight();
+
+ // This is an alternative for requestImage() in PhotoPage. If this
+ // is implemented, you don't need to implement requestImage().
+ public ScreenNail getScreenNail() {
+ return null;
+ }
+
+ public static int getTargetSize(int type) {
+ switch (type) {
+ case TYPE_THUMBNAIL:
+ return sThumbnailTargetSize;
+ case TYPE_MICROTHUMBNAIL:
+ return sMicrothumbnailTargetSize;
+ default:
+ throw new RuntimeException(
+ "should only request thumb/microthumb from cache");
+ }
+ }
+
+ public static BytesBufferPool getBytesBufferPool() {
+ return sMicroThumbBufferPool;
+ }
+
+ public static void setThumbnailSizes(int size, int microSize) {
+ sThumbnailTargetSize = size;
+ if (sMicrothumbnailTargetSize != microSize) {
+ sMicrothumbnailTargetSize = microSize;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/MediaObject.java b/src/com/android/gallery3d/data/MediaObject.java
new file mode 100644
index 000000000..270d4cf0b
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaObject.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 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_TRIM = 1 << 11;
+ public static final int SUPPORT_UNLOCK = 1 << 12;
+ public static final int SUPPORT_BACK = 1 << 13;
+ public static final int SUPPORT_ACTION = 1 << 14;
+ public static final int SUPPORT_CAMERA_SHORTCUT = 1 << 15;
+ public static final int SUPPORT_MUTE = 1 << 16;
+ 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;
+
+ public static final String MEDIA_TYPE_IMAGE_STRING = "image";
+ public static final String MEDIA_TYPE_VIDEO_STRING = "video";
+ public static final String MEDIA_TYPE_ALL_STRING = "all";
+
+ // 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 interface PanoramaSupportCallback {
+ void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+ boolean isPanorama360);
+ }
+
+ 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 getPanoramaSupport(PanoramaSupportCallback callback) {
+ callback.panoramaInfoAvailable(this, false, false);
+ }
+
+ public void clearCachedPanoramaSupport() {
+ }
+
+ public void delete() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void rotate(int degrees) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Uri getContentUri() {
+ String className = getClass().getName();
+ Log.e(TAG, "Class " + className + "should implement getContentUri.");
+ Log.e(TAG, "The object was created from path: " + getPath());
+ throw new UnsupportedOperationException();
+ }
+
+ public Uri getPlayUri() {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getMediaType() {
+ return MEDIA_TYPE_UNKNOWN;
+ }
+
+ 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;
+ }
+
+ public static int getTypeFromString(String s) {
+ if (MEDIA_TYPE_ALL_STRING.equals(s)) return MediaObject.MEDIA_TYPE_ALL;
+ if (MEDIA_TYPE_IMAGE_STRING.equals(s)) return MediaObject.MEDIA_TYPE_IMAGE;
+ if (MEDIA_TYPE_VIDEO_STRING.equals(s)) return MediaObject.MEDIA_TYPE_VIDEO;
+ throw new IllegalArgumentException(s);
+ }
+
+ public static String getTypeString(int type) {
+ switch (type) {
+ case MEDIA_TYPE_IMAGE: return MEDIA_TYPE_IMAGE_STRING;
+ case MEDIA_TYPE_VIDEO: return MEDIA_TYPE_VIDEO_STRING;
+ case MEDIA_TYPE_ALL: return MEDIA_TYPE_ALL_STRING;
+ }
+ throw new IllegalArgumentException();
+ }
+}
diff --git a/src/com/android/gallery3d/data/MediaSet.java b/src/com/android/gallery3d/data/MediaSet.java
new file mode 100644
index 000000000..683aa6b32
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaSet.java
@@ -0,0 +1,348 @@
+/*
+ * 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.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 {
+ @SuppressWarnings("unused")
+ private static final String TAG = "MediaSet";
+
+ public static final int MEDIAITEM_BATCH_FETCH_COUNT = 500;
+ public static final int INDEX_NOT_FOUND = -1;
+
+ public static final int SYNC_RESULT_SUCCESS = 0;
+ public static final int SYNC_RESULT_CANCELLED = 1;
+ public static final int SYNC_RESULT_ERROR = 2;
+
+ /** Listener to be used with requestSync(SyncListener). */
+ public static interface SyncListener {
+ /**
+ * Called when the sync task completed. Completion may be due to normal termination,
+ * an exception, or cancellation.
+ *
+ * @param mediaSet the MediaSet that's done with sync
+ * @param resultCode one of the SYNC_RESULT_* constants
+ */
+ void onSyncDone(MediaSet mediaSet, int resultCode);
+ }
+
+ 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<MediaItem> getMediaItem(int start, int count) {
+ return new ArrayList<MediaItem>();
+ }
+
+ public MediaItem getCoverMediaItem() {
+ ArrayList<MediaItem> items = getMediaItem(0, 1);
+ if (items.size() > 0) return items.get(0);
+ for (int i = 0, n = getSubMediaSetCount(); i < n; i++) {
+ MediaItem cover = getSubMediaSet(i).getCoverMediaItem();
+ if (cover != null) return cover;
+ }
+ return null;
+ }
+
+ public int getSubMediaSetCount() {
+ return 0;
+ }
+
+ public MediaSet getSubMediaSet(int index) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ public boolean isLeafAlbum() {
+ return false;
+ }
+
+ public boolean isCameraRoll() {
+ return false;
+ }
+
+ /**
+ * Method {@link #reload()} may process the loading task in background, this method tells
+ * its client whether the loading is still in process or not.
+ */
+ public boolean isLoading() {
+ 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<MediaItem> 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<MediaItem> list) {
+ for (int i = 0, n = list.size(); i < n; ++i) {
+ // item could be null only in ClusterAlbum
+ MediaObject item = list.get(i);
+ if (item != null && item.mPath == path) return i;
+ }
+ return INDEX_NOT_FOUND;
+ }
+
+ public abstract String getName();
+
+ private WeakHashMap<ContentListener, Object> mListeners =
+ new WeakHashMap<ContentListener, Object>();
+
+ // 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) {
+ mListeners.put(listener, null);
+ }
+
+ public void removeContentListener(ContentListener listener) {
+ 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<MediaItem> 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;
+ }
+
+ /**
+ * Requests sync on this MediaSet. It returns a Future object that can be used by the caller
+ * to query the status of the sync. The sync result code is one of the SYNC_RESULT_* constants
+ * defined in this class and can be obtained by Future.get().
+ *
+ * Subclasses should perform sync on a different thread.
+ *
+ * The default implementation here returns a Future stub that does nothing and returns
+ * SYNC_RESULT_SUCCESS by get().
+ */
+ public Future<Integer> requestSync(SyncListener listener) {
+ listener.onSyncDone(this, SYNC_RESULT_SUCCESS);
+ return FUTURE_STUB;
+ }
+
+ private static final Future<Integer> FUTURE_STUB = new Future<Integer>() {
+ @Override
+ public void cancel() {}
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDone() {
+ return true;
+ }
+
+ @Override
+ public Integer get() {
+ return SYNC_RESULT_SUCCESS;
+ }
+
+ @Override
+ public void waitDone() {}
+ };
+
+ protected Future<Integer> requestSyncOnMultipleSets(MediaSet[] sets, SyncListener listener) {
+ return new MultiSetSyncFuture(sets, listener);
+ }
+
+ private class MultiSetSyncFuture implements Future<Integer>, SyncListener {
+ @SuppressWarnings("hiding")
+ private static final String TAG = "Gallery.MultiSetSync";
+
+ private final SyncListener mListener;
+ private final Future<Integer> mFutures[];
+
+ private boolean mIsCancelled = false;
+ private int mResult = -1;
+ private int mPendingCount;
+
+ @SuppressWarnings("unchecked")
+ MultiSetSyncFuture(MediaSet[] sets, SyncListener listener) {
+ mListener = listener;
+ mPendingCount = sets.length;
+ mFutures = new Future[sets.length];
+
+ synchronized (this) {
+ for (int i = 0, n = sets.length; i < n; ++i) {
+ mFutures[i] = sets[i].requestSync(this);
+ Log.d(TAG, " request sync: " + Utils.maskDebugInfo(sets[i].getName()));
+ }
+ }
+ }
+
+ @Override
+ public synchronized void cancel() {
+ if (mIsCancelled) return;
+ mIsCancelled = true;
+ for (Future<Integer> future : mFutures) future.cancel();
+ if (mResult < 0) mResult = SYNC_RESULT_CANCELLED;
+ }
+
+ @Override
+ public synchronized boolean isCancelled() {
+ return mIsCancelled;
+ }
+
+ @Override
+ public synchronized boolean isDone() {
+ return mPendingCount == 0;
+ }
+
+ @Override
+ public synchronized Integer get() {
+ waitDone();
+ return mResult;
+ }
+
+ @Override
+ public synchronized void waitDone() {
+ try {
+ while (!isDone()) wait();
+ } catch (InterruptedException e) {
+ Log.d(TAG, "waitDone() interrupted");
+ }
+ }
+
+ // SyncListener callback
+ @Override
+ public void onSyncDone(MediaSet mediaSet, int resultCode) {
+ SyncListener listener = null;
+ synchronized (this) {
+ if (resultCode == SYNC_RESULT_ERROR) mResult = SYNC_RESULT_ERROR;
+ --mPendingCount;
+ if (mPendingCount == 0) {
+ listener = mListener;
+ notifyAll();
+ }
+ Log.d(TAG, "onSyncDone: " + Utils.maskDebugInfo(mediaSet.getName())
+ + " #pending=" + mPendingCount);
+ }
+ if (listener != null) listener.onSyncDone(MediaSet.this, mResult);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/MediaSource.java b/src/com/android/gallery3d/data/MediaSource.java
new file mode 100644
index 000000000..95901283b
--- /dev/null
+++ b/src/com/android/gallery3d/data/MediaSource.java
@@ -0,0 +1,96 @@
+/*
+ * 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;
+
+import com.android.gallery3d.data.MediaSet.ItemConsumer;
+
+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, String type) {
+ 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<PathId> list, ItemConsumer consumer) {
+ int n = list.size();
+ for (int i = 0; i < n; i++) {
+ PathId pid = list.get(i);
+ MediaObject obj;
+ synchronized (DataManager.LOCK) {
+ 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..737b5b60d
--- /dev/null
+++ b/src/com/android/gallery3d/data/MtpClient.java
@@ -0,0 +1,443 @@
+/*
+ * 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.annotation.TargetApi;
+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.MtpObjectInfo;
+import android.mtp.MtpStorageInfo;
+import android.util.Log;
+
+import com.android.gallery3d.common.ApiHelper;
+
+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.
+ */
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB_MR1)
+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<Listener> mListeners = new ArrayList<Listener>();
+ // 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<String, MtpDevice> mDevices = new HashMap<String, MtpDevice>();
+ // List of MTP devices we should not try to open for which we are currently
+ // asking for permission to open.
+ private final ArrayList<String> mRequestPermissionDevices = new ArrayList<String>();
+ // 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<String> mIgnoredDevices = new ArrayList<String>();
+
+ 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 usbDevice 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 com.android.gallery3d.data.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 com.android.gallery3d.data.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<MtpDevice> 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<MtpDevice>(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<MtpStorageInfo> 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<MtpStorageInfo> storageList = new ArrayList<MtpStorageInfo>(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<MtpObjectInfo> 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<MtpObjectInfo> objectList = new ArrayList<MtpObjectInfo>(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/PanoramaMetadataJob.java b/src/com/android/gallery3d/data/PanoramaMetadataJob.java
new file mode 100644
index 000000000..ab99d6a81
--- /dev/null
+++ b/src/com/android/gallery3d/data/PanoramaMetadataJob.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.content.Context;
+import android.net.Uri;
+
+import com.android.gallery3d.util.LightCycleHelper;
+import com.android.gallery3d.util.LightCycleHelper.PanoramaMetadata;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class PanoramaMetadataJob implements Job<PanoramaMetadata> {
+ Context mContext;
+ Uri mUri;
+
+ public PanoramaMetadataJob(Context context, Uri uri) {
+ mContext = context;
+ mUri = uri;
+ }
+
+ @Override
+ public PanoramaMetadata run(JobContext jc) {
+ return LightCycleHelper.getPanoramaMetadata(mContext, mUri);
+ }
+}
diff --git a/src/com/android/gallery3d/data/Path.java b/src/com/android/gallery3d/data/Path.java
new file mode 100644
index 000000000..fcae65e66
--- /dev/null
+++ b/src/com/android/gallery3d/data/Path.java
@@ -0,0 +1,241 @@
+/*
+ * 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<MediaObject> mObject;
+ private IdentityCache<String, Path> 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<String, Path>();
+ } 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<MediaObject>(object);
+ }
+ }
+
+ MediaObject getObject() {
+ synchronized (Path.class) {
+ return (mObject == null) ? null : mObject.get();
+ }
+ }
+
+ @Override
+ // TODO: toString() should be more efficient, will fix it later
+ 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 boolean equalsIgnoreCase (String p) {
+ String path = toString();
+ return path.equalsIgnoreCase(p);
+ }
+
+ 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<String> segments = new ArrayList<String>();
+ 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<String> segments = new ArrayList<String>();
+ 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() {
+ if (this == sRoot) return "";
+ return getPrefixPath().mSegment;
+ }
+
+ public Path getPrefixPath() {
+ synchronized (Path.class) {
+ Path current = this;
+ if (current == sRoot) {
+ throw new IllegalStateException();
+ }
+ while (current.mParent != sRoot) {
+ current = current.mParent;
+ }
+ return current;
+ }
+ }
+
+ public String getSuffix() {
+ // We don't need lock because mSegment is final.
+ return 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<String> 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<String> mVariables = new ArrayList<String>();
+ 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<String, Node> mMap;
+ private int mKind = NOT_FOUND;
+
+ Node addChild(String segment) {
+ if (mMap == null) {
+ mMap = new HashMap<String, Node>();
+ } 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/SecureAlbum.java b/src/com/android/gallery3d/data/SecureAlbum.java
new file mode 100644
index 000000000..204f848f8
--- /dev/null
+++ b/src/com/android/gallery3d/data/SecureAlbum.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2012 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.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.MediaColumns;
+import android.provider.MediaStore.Video;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.StitchingChangeListener;
+import com.android.gallery3d.util.MediaSetUtils;
+
+import java.util.ArrayList;
+
+// This class lists all media items added by the client.
+public class SecureAlbum extends MediaSet implements StitchingChangeListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SecureAlbum";
+ private static final String[] PROJECTION = {MediaColumns._ID};
+ private int mMinImageId = Integer.MAX_VALUE; // the smallest id of images
+ private int mMaxImageId = Integer.MIN_VALUE; // the biggest id in images
+ private int mMinVideoId = Integer.MAX_VALUE; // the smallest id of videos
+ private int mMaxVideoId = Integer.MIN_VALUE; // the biggest id of videos
+ // All the media items added by the client.
+ private ArrayList<Path> mAllItems = new ArrayList<Path>();
+ // The types of items in mAllItems. True is video and false is image.
+ private ArrayList<Boolean> mAllItemTypes = new ArrayList<Boolean>();
+ private ArrayList<Path> mExistingItems = new ArrayList<Path>();
+ private Context mContext;
+ private DataManager mDataManager;
+ private static final Uri[] mWatchUris =
+ {Images.Media.EXTERNAL_CONTENT_URI, Video.Media.EXTERNAL_CONTENT_URI};
+ private final ChangeNotifier mNotifier;
+ // A placeholder image in the end of secure album. When it is tapped, it
+ // will take the user to the lock screen.
+ private MediaItem mUnlockItem;
+ private boolean mShowUnlockItem;
+
+ public SecureAlbum(Path path, GalleryApp application, MediaItem unlock) {
+ super(path, nextVersionNumber());
+ mContext = application.getAndroidContext();
+ mDataManager = application.getDataManager();
+ mNotifier = new ChangeNotifier(this, mWatchUris, application);
+ mUnlockItem = unlock;
+ mShowUnlockItem = (!isCameraBucketEmpty(Images.Media.EXTERNAL_CONTENT_URI)
+ || !isCameraBucketEmpty(Video.Media.EXTERNAL_CONTENT_URI));
+ }
+
+ public void addMediaItem(boolean isVideo, int id) {
+ Path pathBase;
+ if (isVideo) {
+ pathBase = LocalVideo.ITEM_PATH;
+ mMinVideoId = Math.min(mMinVideoId, id);
+ mMaxVideoId = Math.max(mMaxVideoId, id);
+ } else {
+ pathBase = LocalImage.ITEM_PATH;
+ mMinImageId = Math.min(mMinImageId, id);
+ mMaxImageId = Math.max(mMaxImageId, id);
+ }
+ Path path = pathBase.getChild(id);
+ if (!mAllItems.contains(path)) {
+ mAllItems.add(path);
+ mAllItemTypes.add(isVideo);
+ mNotifier.fakeChange();
+ }
+ }
+
+ // The sequence is stitching items, local media items, and unlock image.
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ int existingCount = mExistingItems.size();
+ if (start >= existingCount + 1) {
+ return new ArrayList<MediaItem>();
+ }
+
+ // Add paths of requested stitching items.
+ int end = Math.min(start + count, existingCount);
+ ArrayList<Path> subset = new ArrayList<Path>(mExistingItems.subList(start, end));
+
+ // Convert paths to media items.
+ final MediaItem[] buf = new MediaItem[end - start];
+ ItemConsumer consumer = new ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ buf[index] = item;
+ }
+ };
+ mDataManager.mapMediaItems(subset, consumer, 0);
+ ArrayList<MediaItem> result = new ArrayList<MediaItem>(end - start);
+ for (int i = 0; i < buf.length; i++) {
+ result.add(buf[i]);
+ }
+ if (mShowUnlockItem) result.add(mUnlockItem);
+ return result;
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ return (mExistingItems.size() + (mShowUnlockItem ? 1 : 0));
+ }
+
+ @Override
+ public String getName() {
+ return "secure";
+ }
+
+ @Override
+ public long reload() {
+ if (mNotifier.isDirty()) {
+ mDataVersion = nextVersionNumber();
+ updateExistingItems();
+ }
+ return mDataVersion;
+ }
+
+ private ArrayList<Integer> queryExistingIds(Uri uri, int minId, int maxId) {
+ ArrayList<Integer> ids = new ArrayList<Integer>();
+ if (minId == Integer.MAX_VALUE || maxId == Integer.MIN_VALUE) return ids;
+
+ String[] selectionArgs = {String.valueOf(minId), String.valueOf(maxId)};
+ Cursor cursor = mContext.getContentResolver().query(uri, PROJECTION,
+ "_id BETWEEN ? AND ?", selectionArgs, null);
+ if (cursor == null) return ids;
+ try {
+ while (cursor.moveToNext()) {
+ ids.add(cursor.getInt(0));
+ }
+ } finally {
+ cursor.close();
+ }
+ return ids;
+ }
+
+ private boolean isCameraBucketEmpty(Uri baseUri) {
+ Uri uri = baseUri.buildUpon()
+ .appendQueryParameter("limit", "1").build();
+ String[] selection = {String.valueOf(MediaSetUtils.CAMERA_BUCKET_ID)};
+ Cursor cursor = mContext.getContentResolver().query(uri, PROJECTION,
+ "bucket_id = ?", selection, null);
+ if (cursor == null) return true;
+ try {
+ return (cursor.getCount() == 0);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void updateExistingItems() {
+ if (mAllItems.size() == 0) return;
+
+ // Query existing ids.
+ ArrayList<Integer> imageIds = queryExistingIds(
+ Images.Media.EXTERNAL_CONTENT_URI, mMinImageId, mMaxImageId);
+ ArrayList<Integer> videoIds = queryExistingIds(
+ Video.Media.EXTERNAL_CONTENT_URI, mMinVideoId, mMaxVideoId);
+
+ // Construct the existing items list.
+ mExistingItems.clear();
+ for (int i = mAllItems.size() - 1; i >= 0; i--) {
+ Path path = mAllItems.get(i);
+ boolean isVideo = mAllItemTypes.get(i);
+ int id = Integer.parseInt(path.getSuffix());
+ if (isVideo) {
+ if (videoIds.contains(id)) mExistingItems.add(path);
+ } else {
+ if (imageIds.contains(id)) mExistingItems.add(path);
+ }
+ }
+ }
+
+ @Override
+ public boolean isLeafAlbum() {
+ return true;
+ }
+
+ @Override
+ public void onStitchingQueued(Uri uri) {
+ int id = Integer.parseInt(uri.getLastPathSegment());
+ addMediaItem(false, id);
+ }
+
+ @Override
+ public void onStitchingResult(Uri uri) {
+ }
+
+ @Override
+ public void onStitchingProgress(Uri uri, final int progress) {
+ }
+}
diff --git a/src/com/android/gallery3d/data/SecureSource.java b/src/com/android/gallery3d/data/SecureSource.java
new file mode 100644
index 000000000..6bc8cc295
--- /dev/null
+++ b/src/com/android/gallery3d/data/SecureSource.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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;
+
+public class SecureSource extends MediaSource {
+ private GalleryApp mApplication;
+ private static PathMatcher mMatcher = new PathMatcher();
+ private static final int SECURE_ALBUM = 0;
+ private static final int SECURE_UNLOCK = 1;
+
+ static {
+ mMatcher.add("/secure/all/*", SECURE_ALBUM);
+ mMatcher.add("/secure/unlock", SECURE_UNLOCK);
+ }
+
+ public SecureSource(GalleryApp context) {
+ super("secure");
+ mApplication = context;
+ }
+
+ public static boolean isSecurePath(String path) {
+ return (SECURE_ALBUM == mMatcher.match(Path.fromString(path)));
+ }
+
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ switch (mMatcher.match(path)) {
+ case SECURE_ALBUM: {
+ DataManager dataManager = mApplication.getDataManager();
+ MediaItem unlock = (MediaItem) dataManager.getMediaObject(
+ "/secure/unlock");
+ return new SecureAlbum(path, mApplication, unlock);
+ }
+ case SECURE_UNLOCK:
+ return new UnlockImage(path, mApplication);
+ default:
+ throw new RuntimeException("bad path: " + path);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/data/SingleItemAlbum.java b/src/com/android/gallery3d/data/SingleItemAlbum.java
new file mode 100644
index 000000000..a0093e0c3
--- /dev/null
+++ b/src/com/android/gallery3d/data/SingleItemAlbum.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012 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 SingleItemAlbum extends MediaSet {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SingleItemAlbum";
+ private final MediaItem mItem;
+ private final String mName;
+
+ public SingleItemAlbum(Path path, MediaItem item) {
+ super(path, nextVersionNumber());
+ mItem = item;
+ mName = "SingleItemAlbum("+mItem.getClass().getSimpleName()+")";
+ }
+
+ @Override
+ public int getMediaItemCount() {
+ return 1;
+ }
+
+ @Override
+ public ArrayList<MediaItem> getMediaItem(int start, int count) {
+ ArrayList<MediaItem> result = new ArrayList<MediaItem>();
+
+ // If [start, start+count) contains the index 0, return the item.
+ if (start <= 0 && start + count > 0) {
+ result.add(mItem);
+ }
+
+ return result;
+ }
+
+ public MediaItem getItem() {
+ return mItem;
+ }
+
+ @Override
+ public boolean isLeafAlbum() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return mName;
+ }
+
+ @Override
+ public long reload() {
+ return mDataVersion;
+ }
+}
diff --git a/src/com/android/gallery3d/data/SizeClustering.java b/src/com/android/gallery3d/data/SizeClustering.java
new file mode 100644
index 000000000..b809c841b
--- /dev/null
+++ b/src/com/android/gallery3d/data/SizeClustering.java
@@ -0,0 +1,141 @@
+/*
+ * 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.content.Context;
+import android.content.res.Resources;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class SizeClustering extends Clustering {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SizeClustering";
+
+ private Context mContext;
+ private ArrayList<Path>[] 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;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void run(MediaSet baseSet) {
+ @SuppressWarnings("unchecked")
+ final ArrayList<Path>[] group = new ArrayList[SIZE_LEVELS.length];
+ baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ @Override
+ 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<Path> list = group[i];
+ if (list == null) {
+ list = new ArrayList<Path>();
+ group[i] = list;
+ }
+ list.add(item.getPath());
+ }
+ });
+
+ int count = 0;
+ for (int i = 0; i < group.length; i++) {
+ if (group[i] != null) {
+ count++;
+ }
+ }
+
+ mClusters = 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<Path> 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/SnailAlbum.java b/src/com/android/gallery3d/data/SnailAlbum.java
new file mode 100644
index 000000000..7bce7a695
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailAlbum.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2012 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.concurrent.atomic.AtomicBoolean;
+
+// This is a simple MediaSet which contains only one MediaItem -- a SnailItem.
+public class SnailAlbum extends SingleItemAlbum {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SnailAlbum";
+ private AtomicBoolean mDirty = new AtomicBoolean(false);
+
+ public SnailAlbum(Path path, SnailItem item) {
+ super(path, item);
+ }
+
+ @Override
+ public long reload() {
+ if (mDirty.compareAndSet(true, false)) {
+ ((SnailItem) getItem()).updateVersion();
+ mDataVersion = nextVersionNumber();
+ }
+ return mDataVersion;
+ }
+
+ public void notifyChange() {
+ mDirty.set(true);
+ notifyContentChanged();
+ }
+}
diff --git a/src/com/android/gallery3d/data/SnailItem.java b/src/com/android/gallery3d/data/SnailItem.java
new file mode 100644
index 000000000..3586d2cab
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailItem.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 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.graphics.Bitmap;
+import android.graphics.BitmapRegionDecoder;
+
+import com.android.gallery3d.ui.ScreenNail;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+// SnailItem is a MediaItem which can provide a ScreenNail. This is
+// used so we can show an foreign component (like an
+// android.view.View) instead of a Bitmap.
+public class SnailItem extends MediaItem {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SnailItem";
+ private ScreenNail mScreenNail;
+
+ public SnailItem(Path path) {
+ super(path, nextVersionNumber());
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ // nothing to return
+ return new Job<Bitmap>() {
+ @Override
+ public Bitmap run(JobContext jc) {
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> requestLargeImage() {
+ // nothing to return
+ return new Job<BitmapRegionDecoder>() {
+ @Override
+ public BitmapRegionDecoder run(JobContext jc) {
+ return null;
+ }
+ };
+ }
+
+ // We do not provide requestImage or requestLargeImage, instead we
+ // provide a ScreenNail.
+ @Override
+ public ScreenNail getScreenNail() {
+ return mScreenNail;
+ }
+
+ @Override
+ public String getMimeType() {
+ return "";
+ }
+
+ // Returns width and height of the media item.
+ // Returns 0, 0 if the information is not available.
+ @Override
+ public int getWidth() {
+ return 0;
+ }
+
+ @Override
+ public int getHeight() {
+ return 0;
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // Extra methods for SnailItem
+ //////////////////////////////////////////////////////////////////////////
+
+ public void setScreenNail(ScreenNail screenNail) {
+ mScreenNail = screenNail;
+ }
+
+ public void updateVersion() {
+ mDataVersion = nextVersionNumber();
+ }
+}
diff --git a/src/com/android/gallery3d/data/SnailSource.java b/src/com/android/gallery3d/data/SnailSource.java
new file mode 100644
index 000000000..5c690ccdb
--- /dev/null
+++ b/src/com/android/gallery3d/data/SnailSource.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 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;
+
+public class SnailSource extends MediaSource {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SnailSource";
+ private static final int SNAIL_ALBUM = 0;
+ private static final int SNAIL_ITEM = 1;
+
+ private GalleryApp mApplication;
+ private PathMatcher mMatcher;
+ private static int sNextId;
+
+ public SnailSource(GalleryApp application) {
+ super("snail");
+ mApplication = application;
+ mMatcher = new PathMatcher();
+ mMatcher.add("/snail/set/*", SNAIL_ALBUM);
+ mMatcher.add("/snail/item/*", SNAIL_ITEM);
+ }
+
+ // The only path we accept is "/snail/set/id" and "/snail/item/id"
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ DataManager dataManager = mApplication.getDataManager();
+ switch (mMatcher.match(path)) {
+ case SNAIL_ALBUM:
+ String itemPath = "/snail/item/" + mMatcher.getVar(0);
+ SnailItem item =
+ (SnailItem) dataManager.getMediaObject(itemPath);
+ return new SnailAlbum(path, item);
+ case SNAIL_ITEM: {
+ int id = mMatcher.getIntVar(0);
+ return new SnailItem(path);
+ }
+ }
+ return null;
+ }
+
+ // Registers a new SnailAlbum containing a SnailItem and returns the id of
+ // them. You can obtain the Path of the SnailAlbum and SnailItem associated
+ // with the id by getSetPath and getItemPath().
+ public static synchronized int newId() {
+ return sNextId++;
+ }
+
+ public static Path getSetPath(int id) {
+ return Path.fromString("/snail/set").getChild(id);
+ }
+
+ public static Path getItemPath(int id) {
+ return Path.fromString("/snail/item").getChild(id);
+ }
+}
diff --git a/src/com/android/gallery3d/data/TagClustering.java b/src/com/android/gallery3d/data/TagClustering.java
new file mode 100644
index 000000000..407ca84c4
--- /dev/null
+++ b/src/com/android/gallery3d/data/TagClustering.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 android.content.Context;
+
+import com.android.gallery3d.R;
+
+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<ArrayList<Path>> 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<String, ArrayList<Path>> map =
+ new TreeMap<String, ArrayList<Path>>();
+ final ArrayList<Path> untagged = new ArrayList<Path>();
+
+ baseSet.enumerateTotalMediaItems(new MediaSet.ItemConsumer() {
+ @Override
+ 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<Path> list = map.get(key);
+ if (list == null) {
+ list = new ArrayList<Path>();
+ map.put(key, list);
+ }
+ list.add(path);
+ }
+ }
+ });
+
+ int m = map.size();
+ mClusters = new ArrayList<ArrayList<Path>>();
+ mNames = new String[m + ((untagged.size() > 0) ? 1 : 0)];
+ int i = 0;
+ for (Map.Entry<String, ArrayList<Path>> 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<Path> 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..35cbab1ee
--- /dev/null
+++ b/src/com/android/gallery3d/data/TimeClustering.java
@@ -0,0 +1,439 @@
+/*
+ * 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.content.Context;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+public class TimeClustering extends Clustering {
+ @SuppressWarnings("unused")
+ 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<Cluster> 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<SmallItem> sDateComparator =
+ new DateComparator();
+
+ private static class DateComparator implements Comparator<SmallItem> {
+ @Override
+ public int compare(SmallItem item1, SmallItem item2) {
+ return -Utils.compare(item1.dateInMs, item2.dateInMs);
+ }
+ }
+
+ public TimeClustering(Context context) {
+ mContext = context;
+ mClusters = new ArrayList<Cluster>();
+ 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() {
+ @Override
+ 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<SmallItem> items = new ArrayList<SmallItem>(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<Path> getCluster(int index) {
+ ArrayList<SmallItem> items = mClusters.get(index).getItems();
+ ArrayList<Path> result = new ArrayList<Path>(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<SmallItem> 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<SmallItem> 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<SmallItem> 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<SmallItem> mItems = new ArrayList<SmallItem>();
+
+ 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<SmallItem> 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/UnlockImage.java b/src/com/android/gallery3d/data/UnlockImage.java
new file mode 100644
index 000000000..ed3b485c4
--- /dev/null
+++ b/src/com/android/gallery3d/data/UnlockImage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2012 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;
+
+public class UnlockImage extends ActionImage {
+ @SuppressWarnings("unused")
+ private static final String TAG = "UnlockImage";
+
+ public UnlockImage(Path path, GalleryApp application) {
+ super(path, application, R.drawable.placeholder_locked);
+ }
+
+ @Override
+ public int getSupportedOperations() {
+ return super.getSupportedOperations() | SUPPORT_UNLOCK;
+ }
+}
diff --git a/src/com/android/gallery3d/data/UriImage.java b/src/com/android/gallery3d/data/UriImage.java
new file mode 100644
index 000000000..e8875b572
--- /dev/null
+++ b/src/com/android/gallery3d/data/UriImage.java
@@ -0,0 +1,298 @@
+/*
+ * 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.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 com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.app.PanoramaMetadataSupport;
+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 java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+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 int mRotation;
+ private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this);
+
+ private GalleryApp mApplication;
+
+ public UriImage(GalleryApp application, Path path, Uri uri, String contentType) {
+ super(path, nextVersionNumber());
+ mUri = uri;
+ mApplication = Utils.checkNotNull(application);
+ mContentType = contentType;
+ }
+
+ @Override
+ public Job<Bitmap> requestImage(int type) {
+ return new BitmapJob(type);
+ }
+
+ @Override
+ public Job<BitmapRegionDecoder> 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 {
+ if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) {
+ InputStream is = mApplication.getContentResolver()
+ .openInputStream(mUri);
+ mRotation = Exif.getOrientation(is);
+ Utils.closeSilently(is);
+ }
+ 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;
+ }
+ if (MIME_TYPE_JPEG.equalsIgnoreCase(mContentType)) {
+ InputStream is = new FileInputStream(mCacheEntry.cacheFile);
+ mRotation = Exif.getOrientation(is);
+ Utils.closeSilently(is);
+ }
+ 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() {
+ @Override
+ 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<BitmapRegionDecoder> {
+ @Override
+ public BitmapRegionDecoder run(JobContext jc) {
+ if (!prepareInputFile(jc)) return null;
+ BitmapRegionDecoder decoder = DecodeUtils.createBitmapRegionDecoder(
+ jc, mFileDescriptor.getFileDescriptor(), false);
+ mWidth = decoder.getWidth();
+ mHeight = decoder.getHeight();
+ return decoder;
+ }
+ }
+
+ private class BitmapJob implements Job<Bitmap> {
+ private int mType;
+
+ protected BitmapJob(int type) {
+ mType = type;
+ }
+
+ @Override
+ public Bitmap run(JobContext jc) {
+ if (!prepareInputFile(jc)) return null;
+ int targetSize = MediaItem.getTargetSize(mType);
+ Options options = new Options();
+ options.inPreferredConfig = Config.ARGB_8888;
+ Bitmap bitmap = DecodeUtils.decodeThumbnail(jc,
+ mFileDescriptor.getFileDescriptor(), options, targetSize, mType);
+
+ if (jc.isCancelled() || bitmap == null) {
+ return null;
+ }
+
+ if (mType == MediaItem.TYPE_MICROTHUMBNAIL) {
+ bitmap = BitmapUtils.resizeAndCropCenter(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;
+ }
+
+ @Override
+ public void getPanoramaSupport(PanoramaSupportCallback callback) {
+ mPanoramaMetadata.getPanoramaSupport(mApplication, callback);
+ }
+
+ @Override
+ public void clearCachedPanoramaSupport() {
+ mPanoramaMetadata.clearCachedValues();
+ }
+
+ 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);
+ }
+ if (mContentType != null) {
+ 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();
+ }
+ }
+
+ @Override
+ public int getWidth() {
+ return 0;
+ }
+
+ @Override
+ public int getHeight() {
+ return 0;
+ }
+
+ @Override
+ public int getRotation() {
+ return mRotation;
+ }
+}
diff --git a/src/com/android/gallery3d/data/UriSource.java b/src/com/android/gallery3d/data/UriSource.java
new file mode 100644
index 000000000..f66bacd7b
--- /dev/null
+++ b/src/com/android/gallery3d/data/UriSource.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 android.content.ContentResolver;
+import android.net.Uri;
+import android.webkit.MimeTypeMap;
+
+import com.android.gallery3d.app.GalleryApp;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+
+class UriSource extends MediaSource {
+ @SuppressWarnings("unused")
+ private static final String TAG = "UriSource";
+ private static final String IMAGE_TYPE_PREFIX = "image/";
+ private static final String IMAGE_TYPE_ANY = "image/*";
+ private static final String CHARSET_UTF_8 = "utf-8";
+
+ private GalleryApp mApplication;
+
+ public UriSource(GalleryApp context) {
+ super("uri");
+ mApplication = context;
+ }
+
+ @Override
+ public MediaObject createMediaObject(Path path) {
+ String segment[] = path.split();
+ if (segment.length != 3) {
+ throw new RuntimeException("bad path: " + path);
+ }
+ try {
+ String uri = URLDecoder.decode(segment[1], CHARSET_UTF_8);
+ String type = URLDecoder.decode(segment[2], CHARSET_UTF_8);
+ return new UriImage(mApplication, path, Uri.parse(uri), type);
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private String getMimeType(Uri uri) {
+ if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+ String extension =
+ MimeTypeMap.getFileExtensionFromUrl(uri.toString());
+ String type = MimeTypeMap.getSingleton()
+ .getMimeTypeFromExtension(extension.toLowerCase());
+ if (type != null) return type;
+ }
+ // Assume the type is image if the type cannot be resolved
+ // This could happen for "http" URI.
+ String type = mApplication.getContentResolver().getType(uri);
+ if (type == null) type = "image/*";
+ return type;
+ }
+
+ @Override
+ public Path findPathByUri(Uri uri, String type) {
+ String mimeType = getMimeType(uri);
+
+ // Try to find a most specific type but it has to be started with "image/"
+ if ((type == null) || (IMAGE_TYPE_ANY.equals(type)
+ && mimeType.startsWith(IMAGE_TYPE_PREFIX))) {
+ type = mimeType;
+ }
+
+ if (type.startsWith(IMAGE_TYPE_PREFIX)) {
+ try {
+ return Path.fromString("/uri/"
+ + URLEncoder.encode(uri.toString(), CHARSET_UTF_8)
+ + "/" +URLEncoder.encode(type, CHARSET_UTF_8));
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+ // We have no clues that it is an image
+ return null;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/CenteredLinearLayout.java b/src/com/android/gallery3d/filtershow/CenteredLinearLayout.java
new file mode 100644
index 000000000..bc9342d6f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/CenteredLinearLayout.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2012 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.filtershow;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+
+public class CenteredLinearLayout extends LinearLayout {
+ private final int mMaxWidth;
+
+ public CenteredLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CenteredLinearLayout);
+ mMaxWidth = a.getDimensionPixelSize(R.styleable.CenteredLinearLayout_max_width, 0);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
+ Resources r = getContext().getResources();
+ float value = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, parentWidth,
+ r.getDisplayMetrics());
+ if (mMaxWidth > 0 && parentWidth > mMaxWidth) {
+ int measureMode = MeasureSpec.getMode(widthMeasureSpec);
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, measureMode);
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java b/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java
new file mode 100644
index 000000000..95abce114
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/EditorPlaceHolder.java
@@ -0,0 +1,82 @@
+package com.android.gallery3d.filtershow;
+
+import android.view.View;
+import android.view.ViewParent;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.editors.Editor;
+import com.android.gallery3d.filtershow.imageshow.ImageShow;
+
+import java.util.HashMap;
+import java.util.Vector;
+
+public class EditorPlaceHolder {
+ private static final String LOGTAG = "EditorPlaceHolder";
+
+ private FilterShowActivity mActivity = null;
+ private FrameLayout mContainer = null;
+ private HashMap<Integer, Editor> mEditors = new HashMap<Integer, Editor>();
+ private Vector<ImageShow> mOldViews = new Vector<ImageShow>();
+
+ public EditorPlaceHolder(FilterShowActivity activity) {
+ mActivity = activity;
+ }
+
+ public void setContainer(FrameLayout container) {
+ mContainer = container;
+ }
+
+ public void addEditor(Editor c) {
+ mEditors.put(c.getID(), c);
+ }
+
+ public boolean contains(int type) {
+ if (mEditors.get(type) != null) {
+ return true;
+ }
+ return false;
+ }
+
+ public Editor showEditor(int type) {
+ Editor editor = mEditors.get(type);
+ if (editor == null) {
+ return null;
+ }
+
+ editor.createEditor(mActivity, mContainer);
+ editor.getImageShow().bindAsImageLoadListener();
+ mContainer.setVisibility(View.VISIBLE);
+ mContainer.removeAllViews();
+ View eview = editor.getTopLevelView();
+ ViewParent parent = eview.getParent();
+
+ if (parent != null && parent instanceof FrameLayout) {
+ ((FrameLayout) parent).removeAllViews();
+ }
+
+ mContainer.addView(eview);
+ hideOldViews();
+ editor.setVisibility(View.VISIBLE);
+ return editor;
+ }
+
+ public void setOldViews(Vector<ImageShow> views) {
+ mOldViews = views;
+ }
+
+ public void hide() {
+ mContainer.setVisibility(View.GONE);
+ }
+
+ public void hideOldViews() {
+ for (View view : mOldViews) {
+ view.setVisibility(View.GONE);
+ }
+ }
+
+ public Editor getEditor(int editorId) {
+ return mEditors.get(editorId);
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
new file mode 100644
index 000000000..4700fccfe
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
@@ -0,0 +1,1121 @@
+/*
+ * Copyright (C) 2012 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.filtershow;
+
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewPropertyAnimator;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.FrameLayout;
+import android.widget.ShareActionProvider;
+import android.widget.ShareActionProvider.OnShareTargetSelectedListener;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.PhotoPage;
+import com.android.gallery3d.data.LocalAlbum;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.category.Action;
+import com.android.gallery3d.filtershow.category.CategoryAdapter;
+import com.android.gallery3d.filtershow.category.MainPanel;
+import com.android.gallery3d.filtershow.data.UserPresetsManager;
+import com.android.gallery3d.filtershow.editors.BasicEditor;
+import com.android.gallery3d.filtershow.editors.Editor;
+import com.android.gallery3d.filtershow.editors.EditorChanSat;
+import com.android.gallery3d.filtershow.editors.EditorCrop;
+import com.android.gallery3d.filtershow.editors.EditorDraw;
+import com.android.gallery3d.filtershow.editors.EditorGrad;
+import com.android.gallery3d.filtershow.editors.EditorManager;
+import com.android.gallery3d.filtershow.editors.EditorMirror;
+import com.android.gallery3d.filtershow.editors.EditorPanel;
+import com.android.gallery3d.filtershow.editors.EditorRedEye;
+import com.android.gallery3d.filtershow.editors.EditorRotate;
+import com.android.gallery3d.filtershow.editors.EditorStraighten;
+import com.android.gallery3d.filtershow.editors.EditorTinyPlanet;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.history.HistoryItem;
+import com.android.gallery3d.filtershow.history.HistoryManager;
+import com.android.gallery3d.filtershow.imageshow.ImageShow;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.imageshow.Spline;
+import com.android.gallery3d.filtershow.pipeline.CachingPipeline;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.filtershow.pipeline.ProcessingService;
+import com.android.gallery3d.filtershow.presets.PresetManagementDialog;
+import com.android.gallery3d.filtershow.presets.UserPresetsAdapter;
+import com.android.gallery3d.filtershow.provider.SharedImageProvider;
+import com.android.gallery3d.filtershow.state.StateAdapter;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+import com.android.gallery3d.filtershow.tools.XmpPresets;
+import com.android.gallery3d.filtershow.tools.XmpPresets.XMresults;
+import com.android.gallery3d.filtershow.ui.ExportDialog;
+import com.android.gallery3d.filtershow.ui.FramedTextButton;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.UsageStatistics;
+import com.android.photos.data.GalleryBitmapPool;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Vector;
+
+public class FilterShowActivity extends FragmentActivity implements OnItemClickListener,
+ OnShareTargetSelectedListener {
+
+ private String mAction = "";
+ MasterImage mMasterImage = null;
+
+ private static final long LIMIT_SUPPORTS_HIGHRES = 134217728; // 128Mb
+
+ public static final String TINY_PLANET_ACTION = "com.android.camera.action.TINY_PLANET";
+ public static final String LAUNCH_FULLSCREEN = "launch-fullscreen";
+ private ImageShow mImageShow = null;
+
+ private View mSaveButton = null;
+
+ private EditorPlaceHolder mEditorPlaceHolder = new EditorPlaceHolder(this);
+
+ private static final int SELECT_PICTURE = 1;
+ private static final String LOGTAG = "FilterShowActivity";
+
+ private boolean mShowingTinyPlanet = false;
+ private boolean mShowingImageStatePanel = false;
+
+ private final Vector<ImageShow> mImageViews = new Vector<ImageShow>();
+
+ private ShareActionProvider mShareActionProvider;
+ private File mSharedOutputFile = null;
+
+ private boolean mSharingImage = false;
+
+ private WeakReference<ProgressDialog> mSavingProgressDialog;
+
+ private LoadBitmapTask mLoadBitmapTask;
+
+ private Uri mOriginalImageUri = null;
+ private ImagePreset mOriginalPreset = null;
+
+ private Uri mSelectedImageUri = null;
+
+ private UserPresetsManager mUserPresetsManager = null;
+ private UserPresetsAdapter mUserPresetsAdapter = null;
+ private CategoryAdapter mCategoryLooksAdapter = null;
+ private CategoryAdapter mCategoryBordersAdapter = null;
+ private CategoryAdapter mCategoryGeometryAdapter = null;
+ private CategoryAdapter mCategoryFiltersAdapter = null;
+ private int mCurrentPanel = MainPanel.LOOKS;
+
+ private ProcessingService mBoundService;
+ private boolean mIsBound = false;
+
+ public ProcessingService getProcessingService() {
+ return mBoundService;
+ }
+
+ public boolean isSimpleEditAction() {
+ return !PhotoPage.ACTION_NEXTGEN_EDIT.equalsIgnoreCase(mAction);
+ }
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ /*
+ * This is called when the connection with the service has been
+ * established, giving us the service object we can use to
+ * interact with the service. Because we have bound to a explicit
+ * service that we know is running in our own process, we can
+ * cast its IBinder to a concrete class and directly access it.
+ */
+ mBoundService = ((ProcessingService.LocalBinder)service).getService();
+ mBoundService.setFiltershowActivity(FilterShowActivity.this);
+ mBoundService.onStart();
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ /*
+ * This is called when the connection with the service has been
+ * unexpectedly disconnected -- that is, its process crashed.
+ * Because it is running in our same process, we should never
+ * see this happen.
+ */
+ mBoundService = null;
+ }
+ };
+
+ void doBindService() {
+ /*
+ * Establish a connection with the service. We use an explicit
+ * class name because we want a specific service implementation that
+ * we know will be running in our own process (and thus won't be
+ * supporting component replacement by other applications).
+ */
+ bindService(new Intent(FilterShowActivity.this, ProcessingService.class),
+ mConnection, Context.BIND_AUTO_CREATE);
+ mIsBound = true;
+ }
+
+ void doUnbindService() {
+ if (mIsBound) {
+ // Detach our existing connection.
+ unbindService(mConnection);
+ mIsBound = false;
+ }
+ }
+
+ private void setupPipeline() {
+ doBindService();
+ ImageFilter.setActivityForMemoryToasts(this);
+ mUserPresetsManager = new UserPresetsManager(this);
+ mUserPresetsAdapter = new UserPresetsAdapter(this);
+ mCategoryLooksAdapter = new CategoryAdapter(this);
+ }
+
+ public void updateUIAfterServiceStarted() {
+ fillCategories();
+ loadMainPanel();
+ setDefaultPreset();
+ extractXMPData();
+ processIntent();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ boolean onlyUsePortrait = getResources().getBoolean(R.bool.only_use_portrait);
+ if (onlyUsePortrait) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ }
+ MasterImage.setMaster(mMasterImage);
+
+ clearGalleryBitmapPool();
+ setupPipeline();
+
+ setupMasterImage();
+ setDefaultValues();
+ fillEditors();
+
+ loadXML();
+ UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_EDITOR, "Main");
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+ UsageStatistics.CATEGORY_LIFECYCLE, UsageStatistics.LIFECYCLE_START);
+ }
+
+ public boolean isShowingImageStatePanel() {
+ return mShowingImageStatePanel;
+ }
+
+ public void loadMainPanel() {
+ if (findViewById(R.id.main_panel_container) == null) {
+ return;
+ }
+ MainPanel panel = new MainPanel();
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+ transaction.replace(R.id.main_panel_container, panel, MainPanel.FRAGMENT_TAG);
+ transaction.commit();
+ }
+
+ public void loadEditorPanel(FilterRepresentation representation,
+ final Editor currentEditor) {
+ if (representation.getEditorId() == ImageOnlyEditor.ID) {
+ currentEditor.reflectCurrentFilter();
+ return;
+ }
+ final int currentId = currentEditor.getID();
+ Runnable showEditor = new Runnable() {
+ @Override
+ public void run() {
+ EditorPanel panel = new EditorPanel();
+ panel.setEditor(currentId);
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+ transaction.remove(getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG));
+ transaction.replace(R.id.main_panel_container, panel, MainPanel.FRAGMENT_TAG);
+ transaction.commit();
+ }
+ };
+ Fragment main = getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG);
+ boolean doAnimation = false;
+ if (mShowingImageStatePanel
+ && getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+ doAnimation = true;
+ }
+ if (doAnimation && main != null && main instanceof MainPanel) {
+ MainPanel mainPanel = (MainPanel) main;
+ View container = mainPanel.getView().findViewById(R.id.category_panel_container);
+ View bottom = mainPanel.getView().findViewById(R.id.bottom_panel);
+ int panelHeight = container.getHeight() + bottom.getHeight();
+ ViewPropertyAnimator anim = mainPanel.getView().animate();
+ anim.translationY(panelHeight).start();
+ final Handler handler = new Handler();
+ handler.postDelayed(showEditor, anim.getDuration());
+ } else {
+ showEditor.run();
+ }
+ }
+
+ private void loadXML() {
+ setContentView(R.layout.filtershow_activity);
+
+ ActionBar actionBar = getActionBar();
+ actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+ actionBar.setCustomView(R.layout.filtershow_actionbar);
+
+ mSaveButton = actionBar.getCustomView();
+ mSaveButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ saveImage();
+ }
+ });
+
+ mImageShow = (ImageShow) findViewById(R.id.imageShow);
+ mImageViews.add(mImageShow);
+
+ setupEditors();
+
+ mEditorPlaceHolder.hide();
+ mImageShow.bindAsImageLoadListener();
+
+ setupStatePanel();
+ }
+
+ public void fillCategories() {
+ fillLooks();
+ loadUserPresets();
+ fillBorders();
+ fillTools();
+ fillEffects();
+ }
+
+ public void setupStatePanel() {
+ MasterImage.getImage().setHistoryManager(mMasterImage.getHistory());
+ }
+
+ private void fillEffects() {
+ FiltersManager filtersManager = FiltersManager.getManager();
+ ArrayList<FilterRepresentation> filtersRepresentations = filtersManager.getEffects();
+ mCategoryFiltersAdapter = new CategoryAdapter(this);
+ for (FilterRepresentation representation : filtersRepresentations) {
+ if (representation.getTextId() != 0) {
+ representation.setName(getString(representation.getTextId()));
+ }
+ mCategoryFiltersAdapter.add(new Action(this, representation));
+ }
+ }
+
+ private void fillTools() {
+ FiltersManager filtersManager = FiltersManager.getManager();
+ ArrayList<FilterRepresentation> filtersRepresentations = filtersManager.getTools();
+ mCategoryGeometryAdapter = new CategoryAdapter(this);
+ for (FilterRepresentation representation : filtersRepresentations) {
+ mCategoryGeometryAdapter.add(new Action(this, representation));
+ }
+ }
+
+ private void processIntent() {
+ Intent intent = getIntent();
+ if (intent.getBooleanExtra(LAUNCH_FULLSCREEN, false)) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+
+ mAction = intent.getAction();
+ mSelectedImageUri = intent.getData();
+ Uri loadUri = mSelectedImageUri;
+ if (mOriginalImageUri != null) {
+ loadUri = mOriginalImageUri;
+ }
+ if (loadUri != null) {
+ startLoadBitmap(loadUri);
+ } else {
+ pickImage();
+ }
+ }
+
+ private void setupEditors() {
+ mEditorPlaceHolder.setContainer((FrameLayout) findViewById(R.id.editorContainer));
+ EditorManager.addEditors(mEditorPlaceHolder);
+ mEditorPlaceHolder.setOldViews(mImageViews);
+ }
+
+ private void fillEditors() {
+ mEditorPlaceHolder.addEditor(new EditorChanSat());
+ mEditorPlaceHolder.addEditor(new EditorGrad());
+ mEditorPlaceHolder.addEditor(new EditorDraw());
+ mEditorPlaceHolder.addEditor(new BasicEditor());
+ mEditorPlaceHolder.addEditor(new ImageOnlyEditor());
+ mEditorPlaceHolder.addEditor(new EditorTinyPlanet());
+ mEditorPlaceHolder.addEditor(new EditorRedEye());
+ mEditorPlaceHolder.addEditor(new EditorCrop());
+ mEditorPlaceHolder.addEditor(new EditorMirror());
+ mEditorPlaceHolder.addEditor(new EditorRotate());
+ mEditorPlaceHolder.addEditor(new EditorStraighten());
+ }
+
+ private void setDefaultValues() {
+ Resources res = getResources();
+
+ // TODO: get those values from XML.
+ FramedTextButton.setTextSize((int) getPixelsFromDip(14));
+ FramedTextButton.setTrianglePadding((int) getPixelsFromDip(4));
+ FramedTextButton.setTriangleSize((int) getPixelsFromDip(10));
+
+ Drawable curveHandle = res.getDrawable(R.drawable.camera_crop);
+ int curveHandleSize = (int) res.getDimension(R.dimen.crop_indicator_size);
+ Spline.setCurveHandle(curveHandle, curveHandleSize);
+ Spline.setCurveWidth((int) getPixelsFromDip(3));
+ }
+
+ private void startLoadBitmap(Uri uri) {
+ final View loading = findViewById(R.id.loading);
+ final View imageShow = findViewById(R.id.imageShow);
+ imageShow.setVisibility(View.INVISIBLE);
+ loading.setVisibility(View.VISIBLE);
+ mShowingTinyPlanet = false;
+ mLoadBitmapTask = new LoadBitmapTask();
+ mLoadBitmapTask.execute(uri);
+ }
+
+ private void fillBorders() {
+ FiltersManager filtersManager = FiltersManager.getManager();
+ ArrayList<FilterRepresentation> borders = filtersManager.getBorders();
+
+ for (int i = 0; i < borders.size(); i++) {
+ FilterRepresentation filter = borders.get(i);
+ filter.setName(getString(R.string.borders));
+ if (i == 0) {
+ filter.setName(getString(R.string.none));
+ }
+ }
+
+ mCategoryBordersAdapter = new CategoryAdapter(this);
+ for (FilterRepresentation representation : borders) {
+ if (representation.getTextId() != 0) {
+ representation.setName(getString(representation.getTextId()));
+ }
+ mCategoryBordersAdapter.add(new Action(this, representation, Action.FULL_VIEW));
+ }
+ }
+
+ public UserPresetsAdapter getUserPresetsAdapter() {
+ return mUserPresetsAdapter;
+ }
+
+ public CategoryAdapter getCategoryLooksAdapter() {
+ return mCategoryLooksAdapter;
+ }
+
+ public CategoryAdapter getCategoryBordersAdapter() {
+ return mCategoryBordersAdapter;
+ }
+
+ public CategoryAdapter getCategoryGeometryAdapter() {
+ return mCategoryGeometryAdapter;
+ }
+
+ public CategoryAdapter getCategoryFiltersAdapter() {
+ return mCategoryFiltersAdapter;
+ }
+
+ public void removeFilterRepresentation(FilterRepresentation filterRepresentation) {
+ if (filterRepresentation == null) {
+ return;
+ }
+ ImagePreset oldPreset = MasterImage.getImage().getPreset();
+ ImagePreset copy = new ImagePreset(oldPreset);
+ copy.removeFilter(filterRepresentation);
+ MasterImage.getImage().setPreset(copy, copy.getLastRepresentation(), true);
+ if (MasterImage.getImage().getCurrentFilterRepresentation() == filterRepresentation) {
+ FilterRepresentation lastRepresentation = copy.getLastRepresentation();
+ MasterImage.getImage().setCurrentFilterRepresentation(lastRepresentation);
+ }
+ }
+
+ public void useFilterRepresentation(FilterRepresentation filterRepresentation) {
+ if (filterRepresentation == null) {
+ return;
+ }
+ if (MasterImage.getImage().getCurrentFilterRepresentation() == filterRepresentation) {
+ return;
+ }
+ ImagePreset oldPreset = MasterImage.getImage().getPreset();
+ ImagePreset copy = new ImagePreset(oldPreset);
+ FilterRepresentation representation = copy.getRepresentation(filterRepresentation);
+ if (representation == null) {
+ copy.addFilter(filterRepresentation);
+ } else if (filterRepresentation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
+ filterRepresentation = representation;
+ } else {
+ if (filterRepresentation.allowsSingleInstanceOnly()) {
+ // Don't just update the filter representation. Centralize the
+ // logic in the addFilter(), such that we can keep "None" as
+ // null.
+ copy.removeFilter(representation);
+ copy.addFilter(filterRepresentation);
+ }
+ }
+ MasterImage.getImage().setPreset(copy, filterRepresentation, true);
+ MasterImage.getImage().setCurrentFilterRepresentation(filterRepresentation);
+ }
+
+ public void showRepresentation(FilterRepresentation representation) {
+ if (representation == null) {
+ return;
+ }
+
+ useFilterRepresentation(representation);
+
+ // show representation
+ Editor mCurrentEditor = mEditorPlaceHolder.showEditor(representation.getEditorId());
+ loadEditorPanel(representation, mCurrentEditor);
+ }
+
+ public Editor getEditor(int editorID) {
+ return mEditorPlaceHolder.getEditor(editorID);
+ }
+
+ public void setCurrentPanel(int currentPanel) {
+ mCurrentPanel = currentPanel;
+ }
+
+ public int getCurrentPanel() {
+ return mCurrentPanel;
+ }
+
+ public void updateCategories() {
+ ImagePreset preset = mMasterImage.getPreset();
+ mCategoryLooksAdapter.reflectImagePreset(preset);
+ mCategoryBordersAdapter.reflectImagePreset(preset);
+ }
+
+ private class LoadHighresBitmapTask extends AsyncTask<Void, Void, Boolean> {
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ MasterImage master = MasterImage.getImage();
+ Rect originalBounds = master.getOriginalBounds();
+ if (master.supportsHighRes()) {
+ int highresPreviewSize = master.getOriginalBitmapLarge().getWidth() * 2;
+ if (highresPreviewSize > originalBounds.width()) {
+ highresPreviewSize = originalBounds.width();
+ }
+ Rect bounds = new Rect();
+ Bitmap originalHires = ImageLoader.loadOrientedConstrainedBitmap(master.getUri(),
+ master.getActivity(), highresPreviewSize,
+ master.getOrientation(), bounds);
+ master.setOriginalBounds(bounds);
+ master.setOriginalBitmapHighres(originalHires);
+ mBoundService.setOriginalBitmapHighres(originalHires);
+ master.warnListeners();
+ }
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ Bitmap highresBitmap = MasterImage.getImage().getOriginalBitmapHighres();
+ if (highresBitmap != null) {
+ float highResPreviewScale = (float) highresBitmap.getWidth()
+ / (float) MasterImage.getImage().getOriginalBounds().width();
+ mBoundService.setHighresPreviewScaleFactor(highResPreviewScale);
+ }
+ }
+ }
+
+ private class LoadBitmapTask extends AsyncTask<Uri, Boolean, Boolean> {
+ int mBitmapSize;
+
+ public LoadBitmapTask() {
+ mBitmapSize = getScreenImageSize();
+ }
+
+ @Override
+ protected Boolean doInBackground(Uri... params) {
+ if (!MasterImage.getImage().loadBitmap(params[0], mBitmapSize)) {
+ return false;
+ }
+ publishProgress(ImageLoader.queryLightCycle360(MasterImage.getImage().getActivity()));
+ return true;
+ }
+
+ @Override
+ protected void onProgressUpdate(Boolean... values) {
+ super.onProgressUpdate(values);
+ if (isCancelled()) {
+ return;
+ }
+ if (values[0]) {
+ mShowingTinyPlanet = true;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ MasterImage.setMaster(mMasterImage);
+ if (isCancelled()) {
+ return;
+ }
+
+ if (!result) {
+ cannotLoadImage();
+ }
+
+ if (null == CachingPipeline.getRenderScriptContext()){
+ Log.v(LOGTAG,"RenderScript context destroyed during load");
+ return;
+ }
+ final View loading = findViewById(R.id.loading);
+ loading.setVisibility(View.GONE);
+ final View imageShow = findViewById(R.id.imageShow);
+ imageShow.setVisibility(View.VISIBLE);
+
+ Bitmap largeBitmap = MasterImage.getImage().getOriginalBitmapLarge();
+ mBoundService.setOriginalBitmap(largeBitmap);
+
+ float previewScale = (float) largeBitmap.getWidth()
+ / (float) MasterImage.getImage().getOriginalBounds().width();
+ mBoundService.setPreviewScaleFactor(previewScale);
+ if (!mShowingTinyPlanet) {
+ mCategoryFiltersAdapter.removeTinyPlanet();
+ }
+ mCategoryLooksAdapter.imageLoaded();
+ mCategoryBordersAdapter.imageLoaded();
+ mCategoryGeometryAdapter.imageLoaded();
+ mCategoryFiltersAdapter.imageLoaded();
+ mLoadBitmapTask = null;
+
+ if (mOriginalPreset != null) {
+ MasterImage.getImage().setLoadedPreset(mOriginalPreset);
+ MasterImage.getImage().setPreset(mOriginalPreset,
+ mOriginalPreset.getLastRepresentation(), true);
+ mOriginalPreset = null;
+ }
+
+ if (mAction == TINY_PLANET_ACTION) {
+ showRepresentation(mCategoryFiltersAdapter.getTinyPlanet());
+ }
+ LoadHighresBitmapTask highresLoad = new LoadHighresBitmapTask();
+ highresLoad.execute();
+ super.onPostExecute(result);
+ }
+
+ }
+
+ private void clearGalleryBitmapPool() {
+ (new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ // Free memory held in Gallery's Bitmap pool. May be O(n) for n bitmaps.
+ GalleryBitmapPool.getInstance().clear();
+ return null;
+ }
+ }).execute();
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mLoadBitmapTask != null) {
+ mLoadBitmapTask.cancel(false);
+ }
+ mUserPresetsManager.close();
+ doUnbindService();
+ super.onDestroy();
+ }
+
+ // TODO: find a more robust way of handling image size selection
+ // for high screen densities.
+ private int getScreenImageSize() {
+ DisplayMetrics outMetrics = new DisplayMetrics();
+ getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
+ return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels);
+ }
+
+ private void showSavingProgress(String albumName) {
+ ProgressDialog progress;
+ if (mSavingProgressDialog != null) {
+ progress = mSavingProgressDialog.get();
+ if (progress != null) {
+ progress.show();
+ return;
+ }
+ }
+ // TODO: Allow cancellation of the saving process
+ String progressText;
+ if (albumName == null) {
+ progressText = getString(R.string.saving_image);
+ } else {
+ progressText = getString(R.string.filtershow_saving_image, albumName);
+ }
+ progress = ProgressDialog.show(this, "", progressText, true, false);
+ mSavingProgressDialog = new WeakReference<ProgressDialog>(progress);
+ }
+
+ private void hideSavingProgress() {
+ if (mSavingProgressDialog != null) {
+ ProgressDialog progress = mSavingProgressDialog.get();
+ if (progress != null)
+ progress.dismiss();
+ }
+ }
+
+ public void completeSaveImage(Uri saveUri) {
+ if (mSharingImage && mSharedOutputFile != null) {
+ // Image saved, we unblock the content provider
+ Uri uri = Uri.withAppendedPath(SharedImageProvider.CONTENT_URI,
+ Uri.encode(mSharedOutputFile.getAbsolutePath()));
+ ContentValues values = new ContentValues();
+ values.put(SharedImageProvider.PREPARE, false);
+ getContentResolver().insert(uri, values);
+ }
+ setResult(RESULT_OK, new Intent().setData(saveUri));
+ hideSavingProgress();
+ finish();
+ }
+
+ @Override
+ public boolean onShareTargetSelected(ShareActionProvider arg0, Intent arg1) {
+ // First, let's tell the SharedImageProvider that it will need to wait
+ // for the image
+ Uri uri = Uri.withAppendedPath(SharedImageProvider.CONTENT_URI,
+ Uri.encode(mSharedOutputFile.getAbsolutePath()));
+ ContentValues values = new ContentValues();
+ values.put(SharedImageProvider.PREPARE, true);
+ getContentResolver().insert(uri, values);
+ mSharingImage = true;
+
+ // Process and save the image in the background.
+ showSavingProgress(null);
+ mImageShow.saveImage(this, mSharedOutputFile);
+ return true;
+ }
+
+ private Intent getDefaultShareIntent() {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.setType(SharedImageProvider.MIME_TYPE);
+ mSharedOutputFile = SaveImage.getNewFile(this, MasterImage.getImage().getUri());
+ Uri uri = Uri.withAppendedPath(SharedImageProvider.CONTENT_URI,
+ Uri.encode(mSharedOutputFile.getAbsolutePath()));
+ intent.putExtra(Intent.EXTRA_STREAM, uri);
+ return intent;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.filtershow_activity_menu, menu);
+ MenuItem showState = menu.findItem(R.id.showImageStateButton);
+ if (mShowingImageStatePanel) {
+ showState.setTitle(R.string.hide_imagestate_panel);
+ } else {
+ showState.setTitle(R.string.show_imagestate_panel);
+ }
+ mShareActionProvider = (ShareActionProvider) menu.findItem(R.id.menu_share)
+ .getActionProvider();
+ mShareActionProvider.setShareIntent(getDefaultShareIntent());
+ mShareActionProvider.setOnShareTargetSelectedListener(this);
+
+ MenuItem undoItem = menu.findItem(R.id.undoButton);
+ MenuItem redoItem = menu.findItem(R.id.redoButton);
+ MenuItem resetItem = menu.findItem(R.id.resetHistoryButton);
+ mMasterImage.getHistory().setMenuItems(undoItem, redoItem, resetItem);
+ return true;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (mShareActionProvider != null) {
+ mShareActionProvider.setOnShareTargetSelectedListener(null);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mShareActionProvider != null) {
+ mShareActionProvider.setOnShareTargetSelectedListener(this);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.undoButton: {
+ HistoryManager adapter = mMasterImage.getHistory();
+ int position = adapter.undo();
+ mMasterImage.onHistoryItemClick(position);
+ backToMain();
+ invalidateViews();
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+ UsageStatistics.CATEGORY_BUTTON_PRESS, "Undo");
+ return true;
+ }
+ case R.id.redoButton: {
+ HistoryManager adapter = mMasterImage.getHistory();
+ int position = adapter.redo();
+ mMasterImage.onHistoryItemClick(position);
+ invalidateViews();
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+ UsageStatistics.CATEGORY_BUTTON_PRESS, "Redo");
+ return true;
+ }
+ case R.id.resetHistoryButton: {
+ resetHistory();
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+ UsageStatistics.CATEGORY_BUTTON_PRESS, "ResetHistory");
+ return true;
+ }
+ case R.id.showImageStateButton: {
+ toggleImageStatePanel();
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+ UsageStatistics.CATEGORY_BUTTON_PRESS,
+ mShowingImageStatePanel ? "ShowPanel" : "HidePanel");
+ return true;
+ }
+ case R.id.exportFlattenButton: {
+ showExportOptionsDialog();
+ return true;
+ }
+ case android.R.id.home: {
+ saveImage();
+ return true;
+ }
+ case R.id.manageUserPresets: {
+ manageUserPresets();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void manageUserPresets() {
+ DialogFragment dialog = new PresetManagementDialog();
+ dialog.show(getSupportFragmentManager(), "NoticeDialogFragment");
+ }
+
+ private void showExportOptionsDialog() {
+ DialogFragment dialog = new ExportDialog();
+ dialog.show(getSupportFragmentManager(), "ExportDialogFragment");
+ }
+
+ public void updateUserPresetsFromAdapter(UserPresetsAdapter adapter) {
+ ArrayList<FilterUserPresetRepresentation> representations =
+ adapter.getDeletedRepresentations();
+ for (FilterUserPresetRepresentation representation : representations) {
+ deletePreset(representation.getId());
+ }
+ ArrayList<FilterUserPresetRepresentation> changedRepresentations =
+ adapter.getChangedRepresentations();
+ for (FilterUserPresetRepresentation representation : changedRepresentations) {
+ updatePreset(representation);
+ }
+ adapter.clearDeletedRepresentations();
+ adapter.clearChangedRepresentations();
+ loadUserPresets();
+ }
+
+ public void loadUserPresets() {
+ mUserPresetsManager.load();
+ }
+
+ public void updateUserPresetsFromManager() {
+ ArrayList<FilterUserPresetRepresentation> presets = mUserPresetsManager.getRepresentations();
+ if (presets == null) {
+ return;
+ }
+ if (mCategoryLooksAdapter != null) {
+ fillLooks();
+ }
+ mUserPresetsAdapter.clear();
+ for (int i = 0; i < presets.size(); i++) {
+ FilterUserPresetRepresentation representation = presets.get(i);
+ mCategoryLooksAdapter.add(
+ new Action(this, representation, Action.FULL_VIEW));
+ mUserPresetsAdapter.add(new Action(this, representation, Action.FULL_VIEW));
+ }
+ mCategoryLooksAdapter.notifyDataSetInvalidated();
+
+ }
+
+ public void saveCurrentImagePreset() {
+ mUserPresetsManager.save(MasterImage.getImage().getPreset());
+ }
+
+ private void deletePreset(int id) {
+ mUserPresetsManager.delete(id);
+ }
+
+ private void updatePreset(FilterUserPresetRepresentation representation) {
+ mUserPresetsManager.update(representation);
+ }
+
+ public void enableSave(boolean enable) {
+ if (mSaveButton != null) {
+ mSaveButton.setEnabled(enable);
+ }
+ }
+
+ private void fillLooks() {
+ FiltersManager filtersManager = FiltersManager.getManager();
+ ArrayList<FilterRepresentation> filtersRepresentations = filtersManager.getLooks();
+
+ mCategoryLooksAdapter.clear();
+ int verticalItemHeight = (int) getResources().getDimension(R.dimen.action_item_height);
+ mCategoryLooksAdapter.setItemHeight(verticalItemHeight);
+ for (FilterRepresentation representation : filtersRepresentations) {
+ mCategoryLooksAdapter.add(new Action(this, representation, Action.FULL_VIEW));
+ }
+ }
+
+ public void setDefaultPreset() {
+ // Default preset (original)
+ ImagePreset preset = new ImagePreset(); // empty
+ mMasterImage.setPreset(preset, preset.getLastRepresentation(), true);
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////
+ // Some utility functions
+ // TODO: finish the cleanup.
+
+ public void invalidateViews() {
+ for (ImageShow views : mImageViews) {
+ views.updateImage();
+ }
+ }
+
+ public void hideImageViews() {
+ for (View view : mImageViews) {
+ view.setVisibility(View.GONE);
+ }
+ mEditorPlaceHolder.hide();
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////
+ // imageState panel...
+
+ public void toggleImageStatePanel() {
+ invalidateOptionsMenu();
+ mShowingImageStatePanel = !mShowingImageStatePanel;
+ Fragment panel = getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG);
+ if (panel != null) {
+ if (panel instanceof EditorPanel) {
+ EditorPanel editorPanel = (EditorPanel) panel;
+ editorPanel.showImageStatePanel(mShowingImageStatePanel);
+ } else if (panel instanceof MainPanel) {
+ MainPanel mainPanel = (MainPanel) panel;
+ mainPanel.showImageStatePanel(mShowingImageStatePanel);
+ }
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig)
+ {
+ super.onConfigurationChanged(newConfig);
+ setDefaultValues();
+ loadXML();
+ fillCategories();
+ loadMainPanel();
+
+ // mLoadBitmapTask==null implies you have looked at the intent
+ if (!mShowingTinyPlanet && (mLoadBitmapTask == null)) {
+ mCategoryFiltersAdapter.removeTinyPlanet();
+ }
+ final View loading = findViewById(R.id.loading);
+ loading.setVisibility(View.GONE);
+ }
+
+ public void setupMasterImage() {
+
+ HistoryManager historyManager = new HistoryManager();
+ StateAdapter imageStateAdapter = new StateAdapter(this, 0);
+ MasterImage.reset();
+ mMasterImage = MasterImage.getImage();
+ mMasterImage.setHistoryManager(historyManager);
+ mMasterImage.setStateAdapter(imageStateAdapter);
+ mMasterImage.setActivity(this);
+
+ if (Runtime.getRuntime().maxMemory() > LIMIT_SUPPORTS_HIGHRES) {
+ mMasterImage.setSupportsHighRes(true);
+ } else {
+ mMasterImage.setSupportsHighRes(false);
+ }
+ }
+
+ void resetHistory() {
+ HistoryManager adapter = mMasterImage.getHistory();
+ adapter.reset();
+ HistoryItem historyItem = adapter.getItem(0);
+ ImagePreset original = new ImagePreset(historyItem.getImagePreset());
+ mMasterImage.setPreset(original, historyItem.getFilterRepresentation(), true);
+ invalidateViews();
+ backToMain();
+ }
+
+ public void showDefaultImageView() {
+ mEditorPlaceHolder.hide();
+ mImageShow.setVisibility(View.VISIBLE);
+ MasterImage.getImage().setCurrentFilter(null);
+ MasterImage.getImage().setCurrentFilterRepresentation(null);
+ }
+
+ public void backToMain() {
+ Fragment currentPanel = getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG);
+ if (currentPanel instanceof MainPanel) {
+ return;
+ }
+ loadMainPanel();
+ showDefaultImageView();
+ }
+
+ @Override
+ public void onBackPressed() {
+ Fragment currentPanel = getSupportFragmentManager().findFragmentByTag(MainPanel.FRAGMENT_TAG);
+ if (currentPanel instanceof MainPanel) {
+ if (!mImageShow.hasModifications()) {
+ done();
+ } else {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.unsaved).setTitle(R.string.save_before_exit);
+ builder.setPositiveButton(R.string.save_and_exit, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ saveImage();
+ }
+ });
+ builder.setNegativeButton(R.string.exit, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ done();
+ }
+ });
+ builder.show();
+ }
+ } else {
+ backToMain();
+ }
+ }
+
+ public void cannotLoadImage() {
+ Toast.makeText(this, R.string.cannot_load_image, Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////
+
+ public float getPixelsFromDip(float value) {
+ Resources r = getResources();
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value,
+ r.getDisplayMetrics());
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position,
+ long id) {
+ mMasterImage.onHistoryItemClick(position);
+ invalidateViews();
+ }
+
+ public void pickImage() {
+ Intent intent = new Intent();
+ intent.setType("image/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ startActivityForResult(Intent.createChooser(intent, getString(R.string.select_image)),
+ SELECT_PICTURE);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == RESULT_OK) {
+ if (requestCode == SELECT_PICTURE) {
+ Uri selectedImageUri = data.getData();
+ startLoadBitmap(selectedImageUri);
+ }
+ }
+ }
+
+
+ public void saveImage() {
+ if (mImageShow.hasModifications()) {
+ // Get the name of the album, to which the image will be saved
+ File saveDir = SaveImage.getFinalSaveDirectory(this, mSelectedImageUri);
+ int bucketId = GalleryUtils.getBucketId(saveDir.getPath());
+ String albumName = LocalAlbum.getLocalizedName(getResources(), bucketId, null);
+ showSavingProgress(albumName);
+ mImageShow.saveImage(this, null);
+ } else {
+ done();
+ }
+ }
+
+
+ public void done() {
+ hideSavingProgress();
+ if (mLoadBitmapTask != null) {
+ mLoadBitmapTask.cancel(false);
+ }
+ finish();
+ }
+
+ private void extractXMPData() {
+ XMresults res = XmpPresets.extractXMPData(
+ getBaseContext(), mMasterImage, getIntent().getData());
+ if (res == null)
+ return;
+
+ mOriginalImageUri = res.originalimage;
+ mOriginalPreset = res.preset;
+ }
+
+ public Uri getSelectedImageUri() {
+ return mSelectedImageUri;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
new file mode 100644
index 000000000..b6c72fd9d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2012 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.filtershow.cache;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPMeta;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.util.XmpUtilHelper;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+public final class ImageLoader {
+
+ private static final String LOGTAG = "ImageLoader";
+
+ public static final String JPEG_MIME_TYPE = "image/jpeg";
+ public static final int DEFAULT_COMPRESS_QUALITY = 95;
+
+ public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT;
+ public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP;
+ public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT;
+ public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM;
+ public static final int ORI_FLIP_HOR = ExifInterface.Orientation.TOP_RIGHT;
+ public static final int ORI_FLIP_VERT = ExifInterface.Orientation.BOTTOM_RIGHT;
+ public static final int ORI_TRANSPOSE = ExifInterface.Orientation.LEFT_TOP;
+ public static final int ORI_TRANSVERSE = ExifInterface.Orientation.LEFT_BOTTOM;
+
+ private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5;
+
+ private ImageLoader() {}
+
+ /**
+ * Returns the Mime type for a Url. Safe to use with Urls that do not
+ * come from Gallery's content provider.
+ */
+ public static String getMimeType(Uri src) {
+ String postfix = MimeTypeMap.getFileExtensionFromUrl(src.toString());
+ String ret = null;
+ if (postfix != null) {
+ ret = MimeTypeMap.getSingleton().getMimeTypeFromExtension(postfix);
+ }
+ return ret;
+ }
+
+ /**
+ * Returns the image's orientation flag. Defaults to ORI_NORMAL if no valid
+ * orientation was found.
+ */
+ public static int getMetadataOrientation(Context context, Uri uri) {
+ if (uri == null || context == null) {
+ throw new IllegalArgumentException("bad argument to getOrientation");
+ }
+
+ // First try to find orientation data in Gallery's ContentProvider.
+ Cursor cursor = null;
+ try {
+ cursor = context.getContentResolver().query(uri,
+ new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
+ null, null, null);
+ if (cursor != null && cursor.moveToNext()) {
+ int ori = cursor.getInt(0);
+ switch (ori) {
+ case 90:
+ return ORI_ROTATE_90;
+ case 270:
+ return ORI_ROTATE_270;
+ case 180:
+ return ORI_ROTATE_180;
+ default:
+ return ORI_NORMAL;
+ }
+ }
+ } catch (SQLiteException e) {
+ // Do nothing
+ } catch (IllegalArgumentException e) {
+ // Do nothing
+ } finally {
+ Utils.closeSilently(cursor);
+ }
+
+ // Fall back to checking EXIF tags in file.
+ if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+ String mimeType = getMimeType(uri);
+ if (!JPEG_MIME_TYPE.equals(mimeType)) {
+ return ORI_NORMAL;
+ }
+ String path = uri.getPath();
+ ExifInterface exif = new ExifInterface();
+ try {
+ exif.readExif(path);
+ Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+ if (tagval != null) {
+ int orientation = tagval;
+ switch(orientation) {
+ case ORI_NORMAL:
+ case ORI_ROTATE_90:
+ case ORI_ROTATE_180:
+ case ORI_ROTATE_270:
+ case ORI_FLIP_HOR:
+ case ORI_FLIP_VERT:
+ case ORI_TRANSPOSE:
+ case ORI_TRANSVERSE:
+ return orientation;
+ default:
+ return ORI_NORMAL;
+ }
+ }
+ } catch (IOException e) {
+ Log.w(LOGTAG, "Failed to read EXIF orientation", e);
+ }
+ }
+ return ORI_NORMAL;
+ }
+
+ /**
+ * Returns the rotation of image at the given URI as one of 0, 90, 180,
+ * 270. Defaults to 0.
+ */
+ public static int getMetadataRotation(Context context, Uri uri) {
+ int orientation = getMetadataOrientation(context, uri);
+ switch(orientation) {
+ case ORI_ROTATE_90:
+ return 90;
+ case ORI_ROTATE_180:
+ return 180;
+ case ORI_ROTATE_270:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Takes an orientation and a bitmap, and returns the bitmap transformed
+ * to that orientation.
+ */
+ public static Bitmap orientBitmap(Bitmap bitmap, int ori) {
+ Matrix matrix = new Matrix();
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ if (ori == ORI_ROTATE_90 ||
+ ori == ORI_ROTATE_270 ||
+ ori == ORI_TRANSPOSE ||
+ ori == ORI_TRANSVERSE) {
+ int tmp = w;
+ w = h;
+ h = tmp;
+ }
+ switch (ori) {
+ case ORI_ROTATE_90:
+ matrix.setRotate(90, w / 2f, h / 2f);
+ break;
+ case ORI_ROTATE_180:
+ matrix.setRotate(180, w / 2f, h / 2f);
+ break;
+ case ORI_ROTATE_270:
+ matrix.setRotate(270, w / 2f, h / 2f);
+ break;
+ case ORI_FLIP_HOR:
+ matrix.preScale(-1, 1);
+ break;
+ case ORI_FLIP_VERT:
+ matrix.preScale(1, -1);
+ break;
+ case ORI_TRANSPOSE:
+ matrix.setRotate(90, w / 2f, h / 2f);
+ matrix.preScale(1, -1);
+ break;
+ case ORI_TRANSVERSE:
+ matrix.setRotate(270, w / 2f, h / 2f);
+ matrix.preScale(1, -1);
+ break;
+ case ORI_NORMAL:
+ default:
+ return bitmap;
+ }
+ return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
+ bitmap.getHeight(), matrix, true);
+ }
+
+ /**
+ * Returns the bitmap for the rectangular region given by "bounds"
+ * if it is a subset of the bitmap stored at uri. Otherwise returns
+ * null.
+ */
+ public static Bitmap loadRegionBitmap(Context context, Uri uri, BitmapFactory.Options options,
+ Rect bounds) {
+ InputStream is = null;
+ try {
+ is = context.getContentResolver().openInputStream(uri);
+ BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
+ Rect r = new Rect(0, 0, decoder.getWidth(), decoder.getHeight());
+ // return null if bounds are not entirely within the bitmap
+ if (!r.contains(bounds)) {
+ return null;
+ }
+ return decoder.decodeRegion(bounds, options);
+ } catch (FileNotFoundException e) {
+ Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
+ } catch (IOException e) {
+ Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
+ } finally {
+ Utils.closeSilently(is);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the bounds of the bitmap stored at a given Url.
+ */
+ public static Rect loadBitmapBounds(Context context, Uri uri) {
+ BitmapFactory.Options o = new BitmapFactory.Options();
+ loadBitmap(context, uri, o);
+ return new Rect(0, 0, o.outWidth, o.outHeight);
+ }
+
+ /**
+ * Loads a bitmap that has been downsampled using sampleSize from a given url.
+ */
+ public static Bitmap loadDownsampledBitmap(Context context, Uri uri, int sampleSize) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ options.inSampleSize = sampleSize;
+ return loadBitmap(context, uri, options);
+ }
+
+
+ /**
+ * Returns the bitmap from the given uri loaded using the given options.
+ * Returns null on failure.
+ */
+ public static Bitmap loadBitmap(Context context, Uri uri, BitmapFactory.Options o) {
+ if (uri == null || context == null) {
+ throw new IllegalArgumentException("bad argument to loadBitmap");
+ }
+ InputStream is = null;
+ try {
+ is = context.getContentResolver().openInputStream(uri);
+ return BitmapFactory.decodeStream(is, null, o);
+ } catch (FileNotFoundException e) {
+ Log.e(LOGTAG, "FileNotFoundException for " + uri, e);
+ } finally {
+ Utils.closeSilently(is);
+ }
+ return null;
+ }
+
+ /**
+ * Loads a bitmap at a given URI that is downsampled so that both sides are
+ * smaller than maxSideLength. The Bitmap's original dimensions are stored
+ * in the rect originalBounds.
+ *
+ * @param uri URI of image to open.
+ * @param context context whose ContentResolver to use.
+ * @param maxSideLength max side length of returned bitmap.
+ * @param originalBounds If not null, set to the actual bounds of the stored bitmap.
+ * @param useMin use min or max side of the original image
+ * @return downsampled bitmap or null if this operation failed.
+ */
+ public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength,
+ Rect originalBounds, boolean useMin) {
+ if (maxSideLength <= 0 || uri == null || context == null) {
+ throw new IllegalArgumentException("bad argument to getScaledBitmap");
+ }
+ // Get width and height of stored bitmap
+ Rect storedBounds = loadBitmapBounds(context, uri);
+ if (originalBounds != null) {
+ originalBounds.set(storedBounds);
+ }
+ int w = storedBounds.width();
+ int h = storedBounds.height();
+
+ // If bitmap cannot be decoded, return null
+ if (w <= 0 || h <= 0) {
+ return null;
+ }
+
+ // Find best downsampling size
+ int imageSide = 0;
+ if (useMin) {
+ imageSide = Math.min(w, h);
+ } else {
+ imageSide = Math.max(w, h);
+ }
+ int sampleSize = 1;
+ while (imageSide > maxSideLength) {
+ imageSide >>>= 1;
+ sampleSize <<= 1;
+ }
+
+ // Make sure sample size is reasonable
+ if (sampleSize <= 0 ||
+ 0 >= (int) (Math.min(w, h) / sampleSize)) {
+ return null;
+ }
+ return loadDownsampledBitmap(context, uri, sampleSize);
+ }
+
+ /**
+ * Loads a bitmap at a given URI that is downsampled so that both sides are
+ * smaller than maxSideLength. The Bitmap's original dimensions are stored
+ * in the rect originalBounds. The output is also transformed to the given
+ * orientation.
+ *
+ * @param uri URI of image to open.
+ * @param context context whose ContentResolver to use.
+ * @param maxSideLength max side length of returned bitmap.
+ * @param orientation the orientation to transform the bitmap to.
+ * @param originalBounds set to the actual bounds of the stored bitmap.
+ * @return downsampled bitmap or null if this operation failed.
+ */
+ public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength,
+ int orientation, Rect originalBounds) {
+ Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false);
+ if (bmap != null) {
+ bmap = orientBitmap(bmap, orientation);
+ }
+ return bmap;
+ }
+
+ public static Bitmap getScaleOneImageForPreset(Context context, Uri uri, Rect bounds,
+ Rect destination) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ if (destination != null) {
+ if (bounds.width() > destination.width()) {
+ int sampleSize = 1;
+ int w = bounds.width();
+ while (w > destination.width()) {
+ sampleSize *= 2;
+ w /= sampleSize;
+ }
+ options.inSampleSize = sampleSize;
+ }
+ }
+ Bitmap bmp = loadRegionBitmap(context, uri, options, bounds);
+ return bmp;
+ }
+
+ /**
+ * Loads a bitmap that is downsampled by at least the input sample size. In
+ * low-memory situations, the bitmap may be downsampled further.
+ */
+ public static Bitmap loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize) {
+ boolean noBitmap = true;
+ int num_tries = 0;
+ if (sampleSize <= 0) {
+ sampleSize = 1;
+ }
+ Bitmap bmap = null;
+ while (noBitmap) {
+ try {
+ // Try to decode, downsample if low-memory.
+ bmap = loadDownsampledBitmap(context, sourceUri, sampleSize);
+ noBitmap = false;
+ } catch (java.lang.OutOfMemoryError e) {
+ // Try with more downsampling before failing for good.
+ if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
+ throw e;
+ }
+ bmap = null;
+ System.gc();
+ sampleSize *= 2;
+ }
+ }
+ return bmap;
+ }
+
+ /**
+ * Loads an oriented bitmap that is downsampled by at least the input sample
+ * size. In low-memory situations, the bitmap may be downsampled further.
+ */
+ public static Bitmap loadOrientedBitmapWithBackouts(Context context, Uri sourceUri,
+ int sampleSize) {
+ Bitmap bitmap = loadBitmapWithBackouts(context, sourceUri, sampleSize);
+ if (bitmap == null) {
+ return null;
+ }
+ int orientation = getMetadataOrientation(context, sourceUri);
+ bitmap = orientBitmap(bitmap, orientation);
+ return bitmap;
+ }
+
+ /**
+ * Loads bitmap from a resource that may be downsampled in low-memory situations.
+ */
+ public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options,
+ int id) {
+ boolean noBitmap = true;
+ int num_tries = 0;
+ if (options.inSampleSize < 1) {
+ options.inSampleSize = 1;
+ }
+ // Stopgap fix for low-memory devices.
+ Bitmap bmap = null;
+ while (noBitmap) {
+ try {
+ // Try to decode, downsample if low-memory.
+ bmap = BitmapFactory.decodeResource(
+ res, id, options);
+ noBitmap = false;
+ } catch (java.lang.OutOfMemoryError e) {
+ // Retry before failing for good.
+ if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) {
+ throw e;
+ }
+ bmap = null;
+ System.gc();
+ options.inSampleSize *= 2;
+ }
+ }
+ return bmap;
+ }
+
+ public static XMPMeta getXmpObject(Context context) {
+ try {
+ InputStream is = context.getContentResolver().openInputStream(
+ MasterImage.getImage().getUri());
+ return XmpUtilHelper.extractXMPMeta(is);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Determine if this is a light cycle 360 image
+ *
+ * @return true if it is a light Cycle image that is full 360
+ */
+ public static boolean queryLightCycle360(Context context) {
+ InputStream is = null;
+ try {
+ is = context.getContentResolver().openInputStream(MasterImage.getImage().getUri());
+ XMPMeta meta = XmpUtilHelper.extractXMPMeta(is);
+ if (meta == null) {
+ return false;
+ }
+ String namespace = "http://ns.google.com/photos/1.0/panorama/";
+ String cropWidthName = "GPano:CroppedAreaImageWidthPixels";
+ String fullWidthName = "GPano:FullPanoWidthPixels";
+
+ if (!meta.doesPropertyExist(namespace, cropWidthName)) {
+ return false;
+ }
+ if (!meta.doesPropertyExist(namespace, fullWidthName)) {
+ return false;
+ }
+
+ Integer cropValue = meta.getPropertyInteger(namespace, cropWidthName);
+ Integer fullValue = meta.getPropertyInteger(namespace, fullWidthName);
+
+ // Definition of a 360:
+ // GFullPanoWidthPixels == CroppedAreaImageWidthPixels
+ if (cropValue != null && fullValue != null) {
+ return cropValue.equals(fullValue);
+ }
+
+ return false;
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (XMPException e) {
+ return false;
+ } finally {
+ Utils.closeSilently(is);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/category/Action.java b/src/com/android/gallery3d/filtershow/category/Action.java
new file mode 100644
index 000000000..332ca18b0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/category/Action.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2013 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.filtershow.category;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.widget.ArrayAdapter;
+import android.widget.ListAdapter;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequest;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+public class Action implements RenderingRequestCaller {
+
+ private static final String LOGTAG = "Action";
+ private FilterRepresentation mRepresentation;
+ private String mName;
+ private Rect mImageFrame;
+ private Bitmap mImage;
+ private ArrayAdapter mAdapter;
+ public static final int FULL_VIEW = 0;
+ public static final int CROP_VIEW = 1;
+ private int mType = CROP_VIEW;
+ private Bitmap mPortraitImage;
+ private Bitmap mOverlayBitmap;
+ private Context mContext;
+
+ public Action(Context context, FilterRepresentation representation, int type) {
+ mContext = context;
+ setRepresentation(representation);
+ setType(type);
+ }
+
+ public Action(Context context, FilterRepresentation representation) {
+ this(context, representation, CROP_VIEW);
+ }
+
+ public FilterRepresentation getRepresentation() {
+ return mRepresentation;
+ }
+
+ public void setRepresentation(FilterRepresentation representation) {
+ mRepresentation = representation;
+ mName = representation.getName();
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void setName(String name) {
+ mName = name;
+ }
+
+ public void setImageFrame(Rect imageFrame, int orientation) {
+ if (mImageFrame != null && mImageFrame.equals(imageFrame)) {
+ return;
+ }
+ Bitmap bitmap = MasterImage.getImage().getLargeThumbnailBitmap();
+ if (bitmap != null) {
+ mImageFrame = imageFrame;
+ int w = mImageFrame.width();
+ int h = mImageFrame.height();
+ if (orientation == CategoryView.VERTICAL
+ && mType == CROP_VIEW) {
+ w /= 2;
+ }
+ Bitmap bitmapCrop = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ drawCenteredImage(bitmap, bitmapCrop, true);
+
+ postNewIconRenderRequest(bitmapCrop);
+ }
+ }
+
+ public Bitmap getImage() {
+ return mImage;
+ }
+
+ public void setImage(Bitmap image) {
+ mImage = image;
+ }
+
+ public void setAdapter(ArrayAdapter adapter) {
+ mAdapter = adapter;
+ }
+
+ public void setType(int type) {
+ mType = type;
+ }
+
+ private void postNewIconRenderRequest(Bitmap bitmap) {
+ if (bitmap != null && mRepresentation != null) {
+ ImagePreset preset = new ImagePreset();
+ preset.addFilter(mRepresentation);
+ RenderingRequest.post(mContext, bitmap,
+ preset, RenderingRequest.ICON_RENDERING, this);
+ }
+ }
+
+ private void drawCenteredImage(Bitmap source, Bitmap destination, boolean scale) {
+ RectF image = new RectF(0, 0, source.getWidth(), source.getHeight());
+ int border = 0;
+ if (!scale) {
+ border = destination.getWidth() - destination.getHeight();
+ if (border < 0) {
+ border = 0;
+ }
+ }
+ RectF frame = new RectF(border, 0,
+ destination.getWidth() - border,
+ destination.getHeight());
+ Matrix m = new Matrix();
+ m.setRectToRect(frame, image, Matrix.ScaleToFit.CENTER);
+ image.set(frame);
+ m.mapRect(image);
+ m.setRectToRect(image, frame, Matrix.ScaleToFit.FILL);
+ Canvas canvas = new Canvas(destination);
+ canvas.drawBitmap(source, m, new Paint(Paint.FILTER_BITMAP_FLAG));
+ }
+
+ @Override
+ public void available(RenderingRequest request) {
+ mImage = request.getBitmap();
+ if (mImage == null) {
+ return;
+ }
+ if (mRepresentation.getOverlayId() != 0 && mOverlayBitmap == null) {
+ mOverlayBitmap = BitmapFactory.decodeResource(
+ mContext.getResources(),
+ mRepresentation.getOverlayId());
+ }
+ if (mOverlayBitmap != null) {
+ if (getRepresentation().getFilterType() == FilterRepresentation.TYPE_BORDER) {
+ Canvas canvas = new Canvas(mImage);
+ canvas.drawBitmap(mOverlayBitmap, new Rect(0, 0, mOverlayBitmap.getWidth(), mOverlayBitmap.getHeight()),
+ new Rect(0, 0, mImage.getWidth(), mImage.getHeight()), new Paint());
+ } else {
+ Canvas canvas = new Canvas(mImage);
+ canvas.drawARGB(128, 0, 0, 0);
+ drawCenteredImage(mOverlayBitmap, mImage, false);
+ }
+ }
+ if (mAdapter != null) {
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ public void setPortraitImage(Bitmap portraitImage) {
+ mPortraitImage = portraitImage;
+ }
+
+ public Bitmap getPortraitImage() {
+ return mPortraitImage;
+ }
+
+ public Bitmap getOverlayBitmap() {
+ return mOverlayBitmap;
+ }
+
+ public void setOverlayBitmap(Bitmap overlayBitmap) {
+ mOverlayBitmap = overlayBitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java b/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java
new file mode 100644
index 000000000..6451c39df
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/category/CategoryAdapter.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2013 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.filtershow.category;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterTinyPlanetRepresentation;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+public class CategoryAdapter extends ArrayAdapter<Action> {
+
+ private static final String LOGTAG = "CategoryAdapter";
+ private int mItemHeight;
+ private View mContainer;
+ private int mItemWidth = ListView.LayoutParams.MATCH_PARENT;
+ private int mSelectedPosition;
+ int mCategory;
+ private int mOrientation;
+
+ public CategoryAdapter(Context context, int textViewResourceId) {
+ super(context, textViewResourceId);
+ mItemHeight = (int) (context.getResources().getDisplayMetrics().density * 100);
+ }
+
+ public CategoryAdapter(Context context) {
+ this(context, 0);
+ }
+
+ public void setItemHeight(int height) {
+ mItemHeight = height;
+ }
+
+ public void setItemWidth(int width) {
+ mItemWidth = width;
+ }
+
+ @Override
+ public void add(Action action) {
+ super.add(action);
+ action.setAdapter(this);
+ }
+
+ public void initializeSelection(int category) {
+ mCategory = category;
+ mSelectedPosition = -1;
+ if (category == MainPanel.LOOKS) {
+ mSelectedPosition = 0;
+ }
+ if (category == MainPanel.BORDERS) {
+ mSelectedPosition = 0;
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = new CategoryView(getContext());
+ }
+ CategoryView view = (CategoryView) convertView;
+ view.setOrientation(mOrientation);
+ view.setAction(getItem(position), this);
+ view.setLayoutParams(
+ new ListView.LayoutParams(mItemWidth, mItemHeight));
+ view.setTag(position);
+ view.invalidate();
+ return view;
+ }
+
+ public void setSelected(View v) {
+ int old = mSelectedPosition;
+ mSelectedPosition = (Integer) v.getTag();
+ if (old != -1) {
+ invalidateView(old);
+ }
+ invalidateView(mSelectedPosition);
+ }
+
+ public boolean isSelected(View v) {
+ return (Integer) v.getTag() == mSelectedPosition;
+ }
+
+ private void invalidateView(int position) {
+ View child = null;
+ if (mContainer instanceof ListView) {
+ ListView lv = (ListView) mContainer;
+ child = lv.getChildAt(position - lv.getFirstVisiblePosition());
+ } else {
+ CategoryTrack ct = (CategoryTrack) mContainer;
+ child = ct.getChildAt(position);
+ }
+ if (child != null) {
+ child.invalidate();
+ }
+ }
+
+ public void setContainer(View container) {
+ mContainer = container;
+ }
+
+ public void imageLoaded() {
+ notifyDataSetChanged();
+ }
+
+ public FilterRepresentation getTinyPlanet() {
+ for (int i = 0; i < getCount(); i++) {
+ Action action = getItem(i);
+ if (action.getRepresentation() != null
+ && action.getRepresentation()
+ instanceof FilterTinyPlanetRepresentation) {
+ return action.getRepresentation();
+ }
+ }
+ return null;
+ }
+
+ public void removeTinyPlanet() {
+ for (int i = 0; i < getCount(); i++) {
+ Action action = getItem(i);
+ if (action.getRepresentation() != null
+ && action.getRepresentation()
+ instanceof FilterTinyPlanetRepresentation) {
+ remove(action);
+ return;
+ }
+ }
+ }
+
+ public void setOrientation(int orientation) {
+ mOrientation = orientation;
+ }
+
+ public void reflectImagePreset(ImagePreset preset) {
+ if (preset == null) {
+ return;
+ }
+ int selected = 0; // if nothing found, select "none" (first element)
+ FilterRepresentation rep = null;
+ if (mCategory == MainPanel.LOOKS) {
+ int pos = preset.getPositionForType(FilterRepresentation.TYPE_FX);
+ if (pos != -1) {
+ rep = preset.getFilterRepresentation(pos);
+ }
+ } else if (mCategory == MainPanel.BORDERS) {
+ int pos = preset.getPositionForType(FilterRepresentation.TYPE_BORDER);
+ if (pos != -1) {
+ rep = preset.getFilterRepresentation(pos);
+ }
+ }
+ if (rep != null) {
+ for (int i = 0; i < getCount(); i++) {
+ if (rep.getName().equalsIgnoreCase(
+ getItem(i).getRepresentation().getName())) {
+ selected = i;
+ break;
+ }
+ }
+ }
+ if (mSelectedPosition != selected) {
+ mSelectedPosition = selected;
+ this.notifyDataSetChanged();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/category/CategoryPanel.java b/src/com/android/gallery3d/filtershow/category/CategoryPanel.java
new file mode 100644
index 000000000..de2481f3f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/category/CategoryPanel.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2013 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.filtershow.category;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+
+public class CategoryPanel extends Fragment {
+
+ public static final String FRAGMENT_TAG = "CategoryPanel";
+ private static final String PARAMETER_TAG = "currentPanel";
+
+ private int mCurrentAdapter = MainPanel.LOOKS;
+ private CategoryAdapter mAdapter;
+
+ public void setAdapter(int value) {
+ mCurrentAdapter = value;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ loadAdapter(mCurrentAdapter);
+ }
+
+ private void loadAdapter(int adapter) {
+ FilterShowActivity activity = (FilterShowActivity) getActivity();
+ switch (adapter) {
+ case MainPanel.LOOKS: {
+ mAdapter = activity.getCategoryLooksAdapter();
+ mAdapter.initializeSelection(MainPanel.LOOKS);
+ activity.updateCategories();
+ break;
+ }
+ case MainPanel.BORDERS: {
+ mAdapter = activity.getCategoryBordersAdapter();
+ mAdapter.initializeSelection(MainPanel.BORDERS);
+ activity.updateCategories();
+ break;
+ }
+ case MainPanel.GEOMETRY: {
+ mAdapter = activity.getCategoryGeometryAdapter();
+ mAdapter.initializeSelection(MainPanel.GEOMETRY);
+ break;
+ }
+ case MainPanel.FILTERS: {
+ mAdapter = activity.getCategoryFiltersAdapter();
+ mAdapter.initializeSelection(MainPanel.FILTERS);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle state) {
+ super.onSaveInstanceState(state);
+ state.putInt(PARAMETER_TAG, mCurrentAdapter);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ LinearLayout main = (LinearLayout) inflater.inflate(
+ R.layout.filtershow_category_panel_new, container,
+ false);
+
+ if (savedInstanceState != null) {
+ int selectedPanel = savedInstanceState.getInt(PARAMETER_TAG);
+ loadAdapter(selectedPanel);
+ }
+
+ View panelView = main.findViewById(R.id.listItems);
+ if (panelView instanceof CategoryTrack) {
+ CategoryTrack panel = (CategoryTrack) panelView;
+ mAdapter.setOrientation(CategoryView.HORIZONTAL);
+ panel.setAdapter(mAdapter);
+ mAdapter.setContainer(panel);
+ } else {
+ ListView panel = (ListView) main.findViewById(R.id.listItems);
+ panel.setAdapter(mAdapter);
+ mAdapter.setContainer(panel);
+ }
+ return main;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/category/CategoryTrack.java b/src/com/android/gallery3d/filtershow/category/CategoryTrack.java
new file mode 100644
index 000000000..ac8245a3b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/category/CategoryTrack.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013 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.filtershow.category;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.LinearLayout;
+import com.android.gallery3d.R;
+
+public class CategoryTrack extends LinearLayout {
+
+ private CategoryAdapter mAdapter;
+ private int mElemSize;
+ private DataSetObserver mDataSetObserver = new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ invalidate();
+ }
+ @Override
+ public void onInvalidated() {
+ super.onInvalidated();
+ fillContent();
+ }
+ };
+
+ public CategoryTrack(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.CategoryTrack);
+ mElemSize = a.getDimensionPixelSize(R.styleable.CategoryTrack_iconSize, 0);
+ }
+
+ public void setAdapter(CategoryAdapter adapter) {
+ mAdapter = adapter;
+ mAdapter.registerDataSetObserver(mDataSetObserver);
+ fillContent();
+ }
+
+ public void fillContent() {
+ removeAllViews();
+ mAdapter.setItemWidth(mElemSize);
+ mAdapter.setItemHeight(LayoutParams.MATCH_PARENT);
+ int n = mAdapter.getCount();
+ for (int i = 0; i < n; i++) {
+ View view = mAdapter.getView(i, null, this);
+ addView(view, i);
+ }
+ requestLayout();
+ }
+
+ @Override
+ public void invalidate() {
+ for (int i = 0; i < this.getChildCount(); i++) {
+ View child = getChildAt(i);
+ child.invalidate();
+ }
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/category/CategoryView.java b/src/com/android/gallery3d/filtershow/category/CategoryView.java
new file mode 100644
index 000000000..c456dc207
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/category/CategoryView.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2013 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.filtershow.category;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.view.View;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.ui.SelectionRenderer;
+
+public class CategoryView extends View implements View.OnClickListener {
+
+ private static final String LOGTAG = "CategoryView";
+ public static final int VERTICAL = 0;
+ public static final int HORIZONTAL = 1;
+ private Paint mPaint = new Paint();
+ private Action mAction;
+ private Rect mTextBounds = new Rect();
+ private int mMargin = 16;
+ private int mTextSize = 32;
+ private int mTextColor;
+ private int mBackgroundColor;
+ private Paint mSelectPaint;
+ CategoryAdapter mAdapter;
+ private int mSelectionStroke;
+ private Paint mBorderPaint;
+ private int mBorderStroke;
+ private int mOrientation = VERTICAL;
+
+ public CategoryView(Context context) {
+ super(context);
+ setOnClickListener(this);
+ Resources res = getResources();
+ mBackgroundColor = res.getColor(R.color.filtershow_categoryview_background);
+ mTextColor = res.getColor(R.color.filtershow_categoryview_text);
+ mSelectionStroke = res.getDimensionPixelSize(R.dimen.thumbnail_margin);
+ mTextSize = res.getDimensionPixelSize(R.dimen.category_panel_text_size);
+ mMargin = res.getDimensionPixelOffset(R.dimen.category_panel_margin);
+ mSelectPaint = new Paint();
+ mSelectPaint.setStyle(Paint.Style.FILL);
+ mSelectPaint.setColor(res.getColor(R.color.filtershow_category_selection));
+ mBorderPaint = new Paint(mSelectPaint);
+ mBorderPaint.setColor(Color.BLACK);
+ mBorderStroke = mSelectionStroke / 3;
+ }
+
+ private void computeTextPosition(String text) {
+ if (text == null) {
+ return;
+ }
+ mPaint.setTextSize(mTextSize);
+ if (mOrientation == VERTICAL) {
+ text = text.toUpperCase();
+ // TODO: set this in xml
+ mPaint.setTypeface(Typeface.DEFAULT_BOLD);
+ }
+ mPaint.getTextBounds(text, 0, text.length(), mTextBounds);
+ }
+
+ public void drawText(Canvas canvas, String text) {
+ if (text == null) {
+ return;
+ }
+ float textWidth = mPaint.measureText(text);
+ int x = (int) (canvas.getWidth() - textWidth - mMargin);
+ if (mOrientation == HORIZONTAL) {
+ x = (int) ((canvas.getWidth() - textWidth) / 2.0f);
+ }
+ if (x < 0) {
+ // If the text takes more than the view width,
+ // justify to the left.
+ x = mMargin;
+ }
+ int y = canvas.getHeight() - mMargin;
+ canvas.drawText(text, x, y, mPaint);
+ }
+
+ @Override
+ public CharSequence getContentDescription () {
+ if (mAction != null) {
+ return mAction.getName();
+ }
+ return null;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ canvas.drawColor(mBackgroundColor);
+ if (mAction != null) {
+ mPaint.reset();
+ mPaint.setAntiAlias(true);
+ computeTextPosition(mAction.getName());
+ if (mAction.getImage() == null) {
+ mAction.setImageFrame(new Rect(0, 0, getWidth(), getHeight()), mOrientation);
+ } else {
+ Bitmap bitmap = mAction.getImage();
+ canvas.save();
+ Rect clipRect = new Rect(mSelectionStroke, mSelectionStroke,
+ getWidth() - mSelectionStroke,
+ getHeight() - 2* mMargin - mTextSize);
+ int offsetx = 0;
+ int offsety = 0;
+ if (mOrientation == HORIZONTAL) {
+ canvas.clipRect(clipRect);
+ offsetx = - (bitmap.getWidth() - clipRect.width()) / 2;
+ offsety = - (bitmap.getHeight() - clipRect.height()) / 2;
+ }
+ canvas.drawBitmap(bitmap, offsetx, offsety, mPaint);
+ canvas.restore();
+ if (mAdapter.isSelected(this)) {
+ if (mOrientation == HORIZONTAL) {
+ SelectionRenderer.drawSelection(canvas, 0, 0,
+ getWidth(), getHeight() - mMargin - mTextSize,
+ mSelectionStroke, mSelectPaint, mBorderStroke, mBorderPaint);
+ } else {
+ SelectionRenderer.drawSelection(canvas, 0, 0,
+ Math.min(bitmap.getWidth(), getWidth()),
+ Math.min(bitmap.getHeight(), getHeight()),
+ mSelectionStroke, mSelectPaint, mBorderStroke, mBorderPaint);
+ }
+ }
+ }
+ mPaint.setColor(mBackgroundColor);
+ mPaint.setStyle(Paint.Style.STROKE);
+ mPaint.setStrokeWidth(3);
+ drawText(canvas, mAction.getName());
+ mPaint.setColor(mTextColor);
+ mPaint.setStyle(Paint.Style.FILL);
+ mPaint.setStrokeWidth(1);
+ drawText(canvas, mAction.getName());
+ }
+ }
+
+ public void setAction(Action action, CategoryAdapter adapter) {
+ mAction = action;
+ mAdapter = adapter;
+ invalidate();
+ }
+
+ public FilterRepresentation getRepresentation() {
+ return mAction.getRepresentation();
+ }
+
+ @Override
+ public void onClick(View view) {
+ FilterShowActivity activity = (FilterShowActivity) getContext();
+ activity.showRepresentation(mAction.getRepresentation());
+ mAdapter.setSelected(this);
+ }
+
+ public void setOrientation(int orientation) {
+ mOrientation = orientation;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/category/MainPanel.java b/src/com/android/gallery3d/filtershow/category/MainPanel.java
new file mode 100644
index 000000000..9a64ffbf3
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/category/MainPanel.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2013 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.filtershow.category;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.state.StatePanel;
+
+public class MainPanel extends Fragment {
+
+ private static final String LOGTAG = "MainPanel";
+
+ private LinearLayout mMainView;
+ private ImageButton looksButton;
+ private ImageButton bordersButton;
+ private ImageButton geometryButton;
+ private ImageButton filtersButton;
+
+ public static final String FRAGMENT_TAG = "MainPanel";
+ public static final int LOOKS = 0;
+ public static final int BORDERS = 1;
+ public static final int GEOMETRY = 2;
+ public static final int FILTERS = 3;
+
+ private int mCurrentSelected = -1;
+
+ private void selection(int position, boolean value) {
+ if (value) {
+ FilterShowActivity activity = (FilterShowActivity) getActivity();
+ activity.setCurrentPanel(position);
+ }
+ switch (position) {
+ case LOOKS: {
+ looksButton.setSelected(value);
+ break;
+ }
+ case BORDERS: {
+ bordersButton.setSelected(value);
+ break;
+ }
+ case GEOMETRY: {
+ geometryButton.setSelected(value);
+ break;
+ }
+ case FILTERS: {
+ filtersButton.setSelected(value);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (mMainView != null) {
+ if (mMainView.getParent() != null) {
+ ViewGroup parent = (ViewGroup) mMainView.getParent();
+ parent.removeView(mMainView);
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ mMainView = (LinearLayout) inflater.inflate(
+ R.layout.filtershow_main_panel, null, false);
+
+ looksButton = (ImageButton) mMainView.findViewById(R.id.fxButton);
+ bordersButton = (ImageButton) mMainView.findViewById(R.id.borderButton);
+ geometryButton = (ImageButton) mMainView.findViewById(R.id.geometryButton);
+ filtersButton = (ImageButton) mMainView.findViewById(R.id.colorsButton);
+
+ looksButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showPanel(LOOKS);
+ }
+ });
+ bordersButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showPanel(BORDERS);
+ }
+ });
+ geometryButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showPanel(GEOMETRY);
+ }
+ });
+ filtersButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showPanel(FILTERS);
+ }
+ });
+
+ FilterShowActivity activity = (FilterShowActivity) getActivity();
+ showImageStatePanel(activity.isShowingImageStatePanel());
+ showPanel(activity.getCurrentPanel());
+ return mMainView;
+ }
+
+ private boolean isRightAnimation(int newPos) {
+ if (newPos < mCurrentSelected) {
+ return false;
+ }
+ return true;
+ }
+
+ private void setCategoryFragment(CategoryPanel category, boolean fromRight) {
+ FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
+ if (fromRight) {
+ transaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_right);
+ } else {
+ transaction.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_left);
+ }
+ transaction.replace(R.id.category_panel_container, category, CategoryPanel.FRAGMENT_TAG);
+ transaction.commit();
+ }
+
+ public void loadCategoryLookPanel() {
+ if (mCurrentSelected == LOOKS) {
+ return;
+ }
+ boolean fromRight = isRightAnimation(LOOKS);
+ selection(mCurrentSelected, false);
+ CategoryPanel categoryPanel = new CategoryPanel();
+ categoryPanel.setAdapter(LOOKS);
+ setCategoryFragment(categoryPanel, fromRight);
+ mCurrentSelected = LOOKS;
+ selection(mCurrentSelected, true);
+ }
+
+ public void loadCategoryBorderPanel() {
+ if (mCurrentSelected == BORDERS) {
+ return;
+ }
+ boolean fromRight = isRightAnimation(BORDERS);
+ selection(mCurrentSelected, false);
+ CategoryPanel categoryPanel = new CategoryPanel();
+ categoryPanel.setAdapter(BORDERS);
+ setCategoryFragment(categoryPanel, fromRight);
+ mCurrentSelected = BORDERS;
+ selection(mCurrentSelected, true);
+ }
+
+ public void loadCategoryGeometryPanel() {
+ if (mCurrentSelected == GEOMETRY) {
+ return;
+ }
+ boolean fromRight = isRightAnimation(GEOMETRY);
+ selection(mCurrentSelected, false);
+ CategoryPanel categoryPanel = new CategoryPanel();
+ categoryPanel.setAdapter(GEOMETRY);
+ setCategoryFragment(categoryPanel, fromRight);
+ mCurrentSelected = GEOMETRY;
+ selection(mCurrentSelected, true);
+ }
+
+ public void loadCategoryFiltersPanel() {
+ if (mCurrentSelected == FILTERS) {
+ return;
+ }
+ boolean fromRight = isRightAnimation(FILTERS);
+ selection(mCurrentSelected, false);
+ CategoryPanel categoryPanel = new CategoryPanel();
+ categoryPanel.setAdapter(FILTERS);
+ setCategoryFragment(categoryPanel, fromRight);
+ mCurrentSelected = FILTERS;
+ selection(mCurrentSelected, true);
+ }
+
+ public void showPanel(int currentPanel) {
+ switch (currentPanel) {
+ case LOOKS: {
+ loadCategoryLookPanel();
+ break;
+ }
+ case BORDERS: {
+ loadCategoryBorderPanel();
+ break;
+ }
+ case GEOMETRY: {
+ loadCategoryGeometryPanel();
+ break;
+ }
+ case FILTERS: {
+ loadCategoryFiltersPanel();
+ break;
+ }
+ }
+ }
+
+ public void showImageStatePanel(boolean show) {
+ if (mMainView.findViewById(R.id.state_panel_container) == null) {
+ return;
+ }
+ FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
+ final View container = mMainView.findViewById(R.id.state_panel_container);
+ if (show) {
+ container.setVisibility(View.VISIBLE);
+ StatePanel statePanel = new StatePanel();
+ transaction.replace(R.id.state_panel_container, statePanel, StatePanel.FRAGMENT_TAG);
+ } else {
+ container.setVisibility(View.GONE);
+ Fragment statePanel = getChildFragmentManager().findFragmentByTag(StatePanel.FRAGMENT_TAG);
+ if (statePanel != null) {
+ transaction.remove(statePanel);
+ }
+ }
+ transaction.commit();
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java
new file mode 100644
index 000000000..dd4df7dc8
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorGridDialog.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2013 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.filtershow.colorpicker;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class ColorGridDialog extends Dialog {
+ RGBListener mCallback;
+ private static final String LOGTAG = "ColorGridDialog";
+
+ public ColorGridDialog(Context context, final RGBListener cl) {
+ super(context);
+ mCallback = cl;
+ setTitle(R.string.color_pick_title);
+ setContentView(R.layout.filtershow_color_gird);
+ Button sel = (Button) findViewById(R.id.filtershow_cp_custom);
+ ArrayList<Button> b = getButtons((ViewGroup) getWindow().getDecorView());
+ int k = 0;
+ float[] hsv = new float[3];
+
+ for (Button button : b) {
+ if (!button.equals(sel)){
+ hsv[0] = (k % 5) * 360 / 5;
+ hsv[1] = (k / 5) / 3.0f;
+ hsv[2] = (k < 5) ? (k / 4f) : 1;
+ final int c = (Color.HSVToColor(hsv) & 0x00FFFFFF) | 0xAA000000;
+ GradientDrawable sd = ((GradientDrawable) button.getBackground());
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mCallback.setColor(c);
+ dismiss();
+ }
+ });
+ sd.setColor(c);
+ k++;
+ }
+
+ }
+ sel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showColorPicker();
+ ColorGridDialog.this.dismiss();
+ }
+ });
+ }
+
+ private ArrayList<Button> getButtons(ViewGroup vg) {
+ ArrayList<Button> list = new ArrayList<Button>();
+ for (int i = 0; i < vg.getChildCount(); i++) {
+ View v = vg.getChildAt(i);
+ if (v instanceof Button) {
+ list.add((Button) v);
+ } else if (v instanceof ViewGroup) {
+ list.addAll(getButtons((ViewGroup) v));
+ }
+ }
+ return list;
+ }
+
+ public void showColorPicker() {
+ ColorListener cl = new ColorListener() {
+ @Override
+ public void setColor(float[] hsvo) {
+ int c = Color.HSVToColor(hsvo) & 0xFFFFFF;
+ int alpha = (int) (hsvo[3] * 255);
+ c |= alpha << 24;
+ mCallback.setColor(c);
+ }
+ };
+ ColorPickerDialog cpd = new ColorPickerDialog(this.getContext(), cl);
+ cpd.show();
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorListener.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorListener.java
new file mode 100644
index 000000000..5127dad26
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorListener.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2013 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.filtershow.colorpicker;
+
+public interface ColorListener {
+ void setColor(float[] hsvo);
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorOpacityView.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorOpacityView.java
new file mode 100644
index 000000000..2bff501f7
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorOpacityView.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2013 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.filtershow.colorpicker;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class ColorOpacityView extends View implements ColorListener {
+
+ private float mRadius;
+ private float mWidth;
+ private Paint mBarPaint1;
+ private Paint mLinePaint1;
+ private Paint mLinePaint2;
+ private Paint mCheckPaint;
+
+ private float mHeight;
+ private Paint mDotPaint;
+ private int mBgcolor = 0;
+
+ private float mDotRadius;
+ private float mBorder;
+
+ private float[] mHSVO = new float[4];
+ private int mSliderColor;
+ private float mDotX = mBorder;
+ private float mDotY = mBorder;
+ private final static float DOT_SIZE = ColorRectView.DOT_SIZE;
+ public final static float BORDER_SIZE = 20;;
+
+ public ColorOpacityView(Context ctx, AttributeSet attrs) {
+ super(ctx, attrs);
+ DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
+ float mDpToPix = metrics.density;
+ mDotRadius = DOT_SIZE * mDpToPix;
+ mBorder = BORDER_SIZE * mDpToPix;
+ mBarPaint1 = new Paint();
+
+ mDotPaint = new Paint();
+
+ mDotPaint.setStyle(Paint.Style.FILL);
+ mDotPaint.setColor(ctx.getResources().getColor(R.color.slider_dot_color));
+ mSliderColor = ctx.getResources().getColor(R.color.slider_line_color);
+
+ mBarPaint1.setStyle(Paint.Style.FILL);
+
+ mLinePaint1 = new Paint();
+ mLinePaint1.setColor(Color.GRAY);
+ mLinePaint2 = new Paint();
+ mLinePaint2.setColor(mSliderColor);
+ mLinePaint2.setStrokeWidth(4);
+
+ int[] colors = new int[16 * 16];
+ for (int i = 0; i < colors.length; i++) {
+ int y = i / (16 * 8);
+ int x = (i / 8) % 2;
+ colors[i] = (x == y) ? 0xFFAAAAAA : 0xFF444444;
+ }
+ Bitmap bitmap = Bitmap.createBitmap(colors, 16, 16, Bitmap.Config.ARGB_8888);
+ BitmapShader bs = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
+ mCheckPaint = new Paint();
+ mCheckPaint.setShader(bs);
+ }
+
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ float ox = mDotX;
+ float oy = mDotY;
+
+ float x = event.getX();
+ float y = event.getY();
+
+ mDotX = x;
+
+ if (mDotX < mBorder) {
+ mDotX = mBorder;
+ }
+
+ if (mDotX > mWidth - mBorder) {
+ mDotX = mWidth - mBorder;
+ }
+ mHSVO[3] = (mDotX - mBorder) / (mWidth - mBorder * 2);
+ notifyColorListeners(mHSVO);
+ setupButton();
+ invalidate((int) (ox - mDotRadius), (int) (oy - mDotRadius), (int) (ox + mDotRadius),
+ (int) (oy + mDotRadius));
+ invalidate(
+ (int) (mDotX - mDotRadius), (int) (mDotY - mDotRadius), (int) (mDotX + mDotRadius),
+ (int) (mDotY + mDotRadius));
+
+ return true;
+ }
+
+ private void setupButton() {
+ float pos = mHSVO[3] * (mWidth - mBorder * 2);
+ mDotX = pos + mBorder;
+
+ int[] colors3 = new int[] {
+ mSliderColor, mSliderColor, 0x66000000, 0 };
+ RadialGradient g = new RadialGradient(mDotX, mDotY, mDotRadius, colors3, new float[] {
+ 0, .3f, .31f, 1 }, Shader.TileMode.CLAMP);
+ mDotPaint.setShader(g);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mWidth = w;
+ mHeight = h;
+ mDotY = mHeight / 2;
+ updatePaint();
+ setupButton();
+ }
+
+ private void updatePaint() {
+
+ int color2 = Color.HSVToColor(mHSVO);
+ int color1 = color2 & 0xFFFFFF;
+
+ Shader sg = new LinearGradient(
+ mBorder, mBorder, mWidth - mBorder, mBorder, color1, color2, Shader.TileMode.CLAMP);
+ mBarPaint1.setShader(sg);
+
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ canvas.drawColor(mBgcolor);
+ canvas.drawRect(mBorder, mBorder, mWidth - mBorder, mHeight - mBorder, mCheckPaint);
+ canvas.drawRect(mBorder, mBorder, mWidth - mBorder, mHeight - mBorder, mBarPaint1);
+ canvas.drawLine(mDotX, mDotY, mWidth - mBorder, mDotY, mLinePaint1);
+ canvas.drawLine(mBorder, mDotY, mDotX, mDotY, mLinePaint2);
+ if (mDotX != Float.NaN) {
+ canvas.drawCircle(mDotX, mDotY, mDotRadius, mDotPaint);
+ }
+ }
+
+ @Override
+ public void setColor(float[] hsv) {
+ System.arraycopy(hsv, 0, mHSVO, 0, mHSVO.length);
+
+ float oy = mDotY;
+
+ updatePaint();
+ setupButton();
+ invalidate();
+ }
+
+ ArrayList<ColorListener> mColorListeners = new ArrayList<ColorListener>();
+
+ public void notifyColorListeners(float[] hsvo) {
+ for (ColorListener l : mColorListeners) {
+ l.setColor(hsvo);
+ }
+ }
+
+ public void addColorListener(ColorListener l) {
+ mColorListeners.add(l);
+ }
+
+ public void removeColorListener(ColorListener l) {
+ mColorListeners.remove(l);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorPickerDialog.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorPickerDialog.java
new file mode 100644
index 000000000..73a5c907c
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorPickerDialog.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2013 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.filtershow.colorpicker;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ToggleButton;
+
+import com.android.gallery3d.R;
+
+public class ColorPickerDialog extends Dialog implements ColorListener {
+ ToggleButton mSelectedButton;
+ GradientDrawable mSelectRect;
+
+ float[] mHSVO = new float[4];
+
+ public ColorPickerDialog(Context context, final ColorListener cl) {
+ super(context);
+
+ setContentView(R.layout.filtershow_color_picker);
+ ColorValueView csv = (ColorValueView) findViewById(R.id.colorValueView);
+ ColorRectView cwv = (ColorRectView) findViewById(R.id.colorRectView);
+ ColorOpacityView cvv = (ColorOpacityView) findViewById(R.id.colorOpacityView);
+ float[] hsvo = new float[] {
+ 123, .9f, 1, 1 };
+
+ mSelectRect = (GradientDrawable) getContext()
+ .getResources().getDrawable(R.drawable.filtershow_color_picker_roundrect);
+ Button selButton = (Button) findViewById(R.id.btnSelect);
+ selButton.setCompoundDrawablesWithIntrinsicBounds(null, null, mSelectRect, null);
+ Button sel = (Button) findViewById(R.id.btnSelect);
+
+ sel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ColorPickerDialog.this.dismiss();
+ if (cl != null) {
+ cl.setColor(mHSVO);
+ }
+ }
+ });
+
+ cwv.setColor(hsvo);
+ cvv.setColor(hsvo);
+ csv.setColor(hsvo);
+ csv.addColorListener(cwv);
+ cwv.addColorListener(csv);
+ csv.addColorListener(cvv);
+ cwv.addColorListener(cvv);
+ cvv.addColorListener(cwv);
+ cvv.addColorListener(csv);
+ cvv.addColorListener(this);
+ csv.addColorListener(this);
+ cwv.addColorListener(this);
+
+ }
+
+ void toggleClick(ToggleButton v, int[] buttons, boolean isChecked) {
+ int id = v.getId();
+ if (!isChecked) {
+ mSelectedButton = null;
+ return;
+ }
+ for (int i = 0; i < buttons.length; i++) {
+ if (id != buttons[i]) {
+ ToggleButton b = (ToggleButton) findViewById(buttons[i]);
+ b.setChecked(false);
+ }
+ }
+ mSelectedButton = v;
+
+ float[] hsv = (float[]) v.getTag();
+
+ ColorValueView csv = (ColorValueView) findViewById(R.id.colorValueView);
+ ColorRectView cwv = (ColorRectView) findViewById(R.id.colorRectView);
+ ColorOpacityView cvv = (ColorOpacityView) findViewById(R.id.colorOpacityView);
+ cwv.setColor(hsv);
+ cvv.setColor(hsv);
+ csv.setColor(hsv);
+ }
+
+ @Override
+ public void setColor(float[] hsvo) {
+ System.arraycopy(hsvo, 0, mHSVO, 0, mHSVO.length);
+ int color = Color.HSVToColor(hsvo);
+ mSelectRect.setColor(color);
+ setButtonColor(mSelectedButton, hsvo);
+ }
+
+ private void setButtonColor(ToggleButton button, float[] hsv) {
+ if (button == null) {
+ return;
+ }
+ int color = Color.HSVToColor(hsv);
+ button.setBackgroundColor(color);
+ float[] fg = new float[] {
+ (hsv[0] + 180) % 360,
+ hsv[1],
+ (hsv[2] > .5f) ? .1f : .9f
+ };
+ button.setTextColor(Color.HSVToColor(fg));
+ button.setTag(hsv);
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorRectView.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorRectView.java
new file mode 100644
index 000000000..07d7c7126
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorRectView.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2013 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.filtershow.colorpicker;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.SweepGradient;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class ColorRectView extends View implements ColorListener {
+ private float mDpToPix;
+ private float mRadius = 80;
+ private float mCtrY = 100;
+ private Paint mWheelPaint1;
+ private Paint mWheelPaint2;
+ private Paint mWheelPaint3;
+ private float mCtrX = 100;
+ private Paint mDotPaint;
+ private float mDotRadus;
+ private float mBorder;
+ private int mBgcolor = 0;
+ private float mDotX = Float.NaN;
+ private float mDotY;
+ private int mSliderColor = 0xFF33B5E5;
+ private float[] mHSVO = new float[4];
+ private int[] mColors = new int[] {
+ 0xFFFF0000,// red
+ 0xFFFFFF00,// yellow
+ 0xFF00FF00,// green
+ 0xFF00FFFF,// cyan
+ 0xFF0000FF,// blue
+ 0xFFFF00FF,// magenta
+ 0xFFFF0000,// red
+ };
+ private int mWidth;
+ private int mHeight;
+ public final static float DOT_SIZE = 20;
+ public final static float BORDER_SIZE = 10;
+
+ public ColorRectView(Context ctx, AttributeSet attrs) {
+ super(ctx, attrs);
+
+ DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
+ mDpToPix = metrics.density;
+ mDotRadus = DOT_SIZE * mDpToPix;
+ mBorder = BORDER_SIZE * mDpToPix;
+
+ mWheelPaint1 = new Paint();
+ mWheelPaint2 = new Paint();
+ mWheelPaint3 = new Paint();
+ mDotPaint = new Paint();
+
+ mDotPaint.setStyle(Paint.Style.FILL);
+ mDotPaint.setColor(ctx.getResources().getColor(R.color.slider_dot_color));
+ mSliderColor = ctx.getResources().getColor(R.color.slider_line_color);
+ mWheelPaint1.setStyle(Paint.Style.FILL);
+ mWheelPaint2.setStyle(Paint.Style.FILL);
+ mWheelPaint3.setStyle(Paint.Style.FILL);
+ }
+
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+
+ invalidate((int) (mDotX - mDotRadus), (int) (mDotY - mDotRadus), (int) (mDotX + mDotRadus),
+ (int) (mDotY + mDotRadus));
+ float x = event.getX();
+ float y = event.getY();
+
+ x = Math.max(Math.min(x, mWidth - mBorder), mBorder);
+ y = Math.max(Math.min(y, mHeight - mBorder), mBorder);
+ mDotX = x;
+ mDotY = y;
+ float sat = 1 - (mDotY - mBorder) / (mHeight - 2 * mBorder);
+ if (sat > 1) {
+ sat = 1;
+ }
+
+ double hue = Math.PI * 2 * (mDotX - mBorder) / (mHeight - 2 * mBorder);
+ mHSVO[0] = ((float) Math.toDegrees(hue) + 360) % 360;
+ mHSVO[1] = sat;
+ notifyColorListeners(mHSVO);
+ updateDotPaint();
+ invalidate((int) (mDotX - mDotRadus), (int) (mDotY - mDotRadus), (int) (mDotX + mDotRadus),
+ (int) (mDotY + mDotRadus));
+
+ return true;
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mWidth = w;
+ mHeight = h;
+ mCtrY = h / 2f;
+ mCtrX = w / 2f;
+ mRadius = Math.min(mCtrY, mCtrX) - 2 * mBorder;
+ setUpColorPanel();
+ }
+
+ private void setUpColorPanel() {
+ float val = mHSVO[2];
+ int v = 0xFF000000 | 0x10101 * (int) (val * 0xFF);
+ int[] colors = new int[] {
+ 0x0000000, v };
+ int[] colors2 = new int[] {
+ 0x0000000, 0xFF000000 };
+ int[] wheelColor = new int[mColors.length];
+ float[] hsv = new float[3];
+ for (int i = 0; i < wheelColor.length; i++) {
+ Color.colorToHSV(mColors[i], hsv);
+ hsv[2] = mHSVO[2];
+ wheelColor[i] = Color.HSVToColor(hsv);
+ }
+ updateDot();
+ updateDotPaint();
+ SweepGradient sg = new SweepGradient(mCtrX, mCtrY, wheelColor, null);
+ LinearGradient lg = new LinearGradient(
+ mBorder, 0, mWidth - mBorder, 0, wheelColor, null, Shader.TileMode.CLAMP);
+
+ mWheelPaint1.setShader(lg);
+ LinearGradient rg = new LinearGradient(
+ 0, mBorder, 0, mHeight - mBorder, colors, null, Shader.TileMode.CLAMP);
+ mWheelPaint2.setShader(rg);
+ LinearGradient rg2 = new LinearGradient(
+ 0, mBorder, 0, mHeight - mBorder, colors2, null, Shader.TileMode.CLAMP);
+ mWheelPaint3.setShader(rg2);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ canvas.drawColor(mBgcolor);
+ RectF rect = new RectF();
+ rect.left = mBorder;
+ rect.right = mWidth - mBorder;
+ rect.top = mBorder;
+ rect.bottom = mHeight - mBorder;
+
+ canvas.drawRect(rect, mWheelPaint1);
+ canvas.drawRect(rect, mWheelPaint3);
+ canvas.drawRect(rect, mWheelPaint2);
+
+ if (mDotX != Float.NaN) {
+
+ canvas.drawCircle(mDotX, mDotY, mDotRadus, mDotPaint);
+ }
+ }
+
+ private void updateDot() {
+
+ double hue = mHSVO[0];
+ double sat = mHSVO[1];
+
+ mDotX = (float) (mBorder + (mHeight - 2 * mBorder) * Math.toRadians(hue) / (Math.PI * 2));
+ mDotY = (float) ((1 - sat) * (mHeight - 2 * mBorder) + mBorder);
+
+ }
+
+ private void updateDotPaint() {
+ int[] colors3 = new int[] {
+ mSliderColor, mSliderColor, 0x66000000, 0 };
+ RadialGradient g = new RadialGradient(mDotX, mDotY, mDotRadus, colors3, new float[] {
+ 0, .3f, .31f, 1 }, Shader.TileMode.CLAMP);
+ mDotPaint.setShader(g);
+
+ }
+
+ @Override
+ public void setColor(float[] hsvo) {
+ System.arraycopy(hsvo, 0, mHSVO, 0, mHSVO.length);
+
+ setUpColorPanel();
+ invalidate();
+
+ updateDot();
+ updateDotPaint();
+
+ }
+
+ ArrayList<ColorListener> mColorListeners = new ArrayList<ColorListener>();
+
+ public void notifyColorListeners(float[] hsv) {
+ for (ColorListener l : mColorListeners) {
+ l.setColor(hsv);
+ }
+ }
+
+ public void addColorListener(ColorListener l) {
+ mColorListeners.add(l);
+ }
+
+ public void removeColorListener(ColorListener l) {
+ mColorListeners.remove(l);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/ColorValueView.java b/src/com/android/gallery3d/filtershow/colorpicker/ColorValueView.java
new file mode 100644
index 000000000..13cb44bad
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/ColorValueView.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2013 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.filtershow.colorpicker;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class ColorValueView extends View implements ColorListener {
+
+ private float mRadius;
+ private float mWidth;
+ private Paint mBarPaint1;
+ private Paint mLinePaint1;
+ private Paint mLinePaint2;
+ private float mHeight;
+ private int mBgcolor = 0;
+ private Paint mDotPaint;
+ private float dotRadus;
+ private float mBorder;
+
+ private float[] mHSVO = new float[4];
+ private int mSliderColor;
+ private float mDotX;
+ private float mDotY = mBorder;
+ private final static float DOT_SIZE = ColorRectView.DOT_SIZE;
+ private final static float BORDER_SIZE = ColorRectView.DOT_SIZE;
+
+ public ColorValueView(Context ctx, AttributeSet attrs) {
+ super(ctx, attrs);
+ DisplayMetrics metrics = ctx.getResources().getDisplayMetrics();
+ float mDpToPix = metrics.density;
+ dotRadus = DOT_SIZE * mDpToPix;
+ mBorder = BORDER_SIZE * mDpToPix;
+
+ mBarPaint1 = new Paint();
+
+ mDotPaint = new Paint();
+
+ mDotPaint.setStyle(Paint.Style.FILL);
+ mDotPaint.setColor(ctx.getResources().getColor(R.color.slider_dot_color));
+
+ mBarPaint1.setStyle(Paint.Style.FILL);
+
+ mLinePaint1 = new Paint();
+ mLinePaint1.setColor(Color.GRAY);
+ mLinePaint2 = new Paint();
+ mSliderColor = ctx.getResources().getColor(R.color.slider_line_color);
+ mLinePaint2.setColor(mSliderColor);
+ mLinePaint2.setStrokeWidth(4);
+ }
+
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ float ox = mDotX;
+ float oy = mDotY;
+
+ float x = event.getX();
+ float y = event.getY();
+
+ mDotY = y;
+
+ if (mDotY < mBorder) {
+ mDotY = mBorder;
+ }
+
+ if (mDotY > mHeight - mBorder) {
+ mDotY = mHeight - mBorder;
+ }
+ mHSVO[2] = (mDotY - mBorder) / (mHeight - mBorder * 2);
+ notifyColorListeners(mHSVO);
+ setupButton();
+ invalidate((int) (ox - dotRadus), (int) (oy - dotRadus), (int) (ox + dotRadus),
+ (int) (oy + dotRadus));
+ invalidate((int) (mDotX - dotRadus), (int) (mDotY - dotRadus), (int) (mDotX + dotRadus),
+ (int) (mDotY + dotRadus));
+
+ return true;
+ }
+
+ private void setupButton() {
+ float pos = mHSVO[2] * (mHeight - mBorder * 2);
+ mDotY = pos + mBorder;
+
+ int[] colors3 = new int[] {
+ mSliderColor, mSliderColor, 0x66000000, 0 };
+ RadialGradient g = new RadialGradient(mDotX, mDotY, dotRadus, colors3, new float[] {
+ 0, .3f, .31f, 1 }, Shader.TileMode.CLAMP);
+ mDotPaint.setShader(g);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mWidth = w;
+ mHeight = h;
+ mDotX = mWidth / 2;
+ updatePaint();
+ setupButton();
+ }
+
+ private void updatePaint() {
+ float[] hsv = new float[] {
+ mHSVO[0], mHSVO[1], 0f };
+ int color1 = Color.HSVToColor(hsv);
+ hsv[2] = 1;
+ int color2 = Color.HSVToColor(hsv);
+
+ Shader sg = new LinearGradient(mBorder, mBorder, mBorder, mHeight - mBorder, color1, color2,
+ Shader.TileMode.CLAMP);
+ mBarPaint1.setShader(sg);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ canvas.drawColor(mBgcolor);
+ canvas.drawRect(mBorder, mBorder, mWidth - mBorder, mHeight - mBorder, mBarPaint1);
+ canvas.drawLine(mDotX, mDotY, mDotX, mHeight - mBorder, mLinePaint2);
+ canvas.drawLine(mDotX, mBorder, mDotX, mDotY, mLinePaint1);
+ if (mDotX != Float.NaN) {
+ canvas.drawCircle(mDotX, mDotY, dotRadus, mDotPaint);
+ }
+ }
+
+ @Override
+ public void setColor(float[] hsvo) {
+ System.arraycopy(hsvo, 0, mHSVO, 0, mHSVO.length);
+
+ float oy = mDotY;
+ updatePaint();
+ setupButton();
+ invalidate();
+
+ }
+
+ ArrayList<ColorListener> mColorListeners = new ArrayList<ColorListener>();
+
+ public void notifyColorListeners(float[] hsv) {
+ for (ColorListener l : mColorListeners) {
+ l.setColor(hsv);
+ }
+ }
+
+ public void addColorListener(ColorListener l) {
+ mColorListeners.add(l);
+ }
+
+ public void removeColorListener(ColorListener l) {
+ mColorListeners.remove(l);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/colorpicker/RGBListener.java b/src/com/android/gallery3d/filtershow/colorpicker/RGBListener.java
new file mode 100644
index 000000000..147fb91a4
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/colorpicker/RGBListener.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2013 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.filtershow.colorpicker;
+
+public interface RGBListener {
+ void setColor(int hsv);
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/ActionSlider.java b/src/com/android/gallery3d/filtershow/controller/ActionSlider.java
new file mode 100644
index 000000000..f80a1cacb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ActionSlider.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2013 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.filtershow.controller;
+
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public class ActionSlider extends TitledSlider {
+ private static final String LOGTAG = "ActionSlider";
+ ImageButton mLeftButton;
+ ImageButton mRightButton;
+ public ActionSlider() {
+ mLayoutID = R.layout.filtershow_control_action_slider;
+ }
+
+ @Override
+ public void setUp(ViewGroup container, Parameter parameter, Editor editor) {
+ super.setUp(container, parameter, editor);
+ mLeftButton = (ImageButton) mTopView.findViewById(R.id.leftActionButton);
+ mLeftButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ ((ParameterActionAndInt) mParameter).fireLeftAction();
+ }
+ });
+
+ mRightButton = (ImageButton) mTopView.findViewById(R.id.rightActionButton);
+ mRightButton.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ ((ParameterActionAndInt) mParameter).fireRightAction();
+ }
+ });
+ updateUI();
+ }
+
+ @Override
+ public void updateUI() {
+ super.updateUI();
+ if (mLeftButton != null) {
+ int iconId = ((ParameterActionAndInt) mParameter).getLeftIcon();
+ mLeftButton.setImageResource(iconId);
+ }
+ if (mRightButton != null) {
+ int iconId = ((ParameterActionAndInt) mParameter).getRightIcon();
+ mRightButton.setImageResource(iconId);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/BasicParameterInt.java b/src/com/android/gallery3d/filtershow/controller/BasicParameterInt.java
new file mode 100644
index 000000000..92145e9be
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/BasicParameterInt.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2013 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.filtershow.controller;
+
+import android.util.Log;
+
+public class BasicParameterInt implements ParameterInteger {
+ protected String mParameterName;
+ protected Control mControl;
+ protected int mMaximum = 100;
+ protected int mMinimum = 0;
+ protected int mDefaultValue;
+ protected int mValue;
+ public final int ID;
+ protected FilterView mEditor;
+ private final String LOGTAG = "BasicParameterInt";
+
+ @Override
+ public void copyFrom(Parameter src) {
+ if (!(src instanceof BasicParameterInt)) {
+ throw new IllegalArgumentException(src.getClass().getName());
+ }
+ BasicParameterInt p = (BasicParameterInt) src;
+ mMaximum = p.mMaximum;
+ mMinimum = p.mMinimum;
+ mDefaultValue = p.mDefaultValue;
+ mValue = p.mValue;
+ }
+
+ public BasicParameterInt(int id, int value) {
+ ID = id;
+ mValue = value;
+ }
+
+ public BasicParameterInt(int id, int value, int min, int max) {
+ ID = id;
+ mValue = value;
+ mMinimum = min;
+ mMaximum = max;
+ }
+
+ @Override
+ public String getParameterName() {
+ return mParameterName;
+ }
+
+ @Override
+ public String getParameterType() {
+ return sParameterType;
+ }
+
+ @Override
+ public String getValueString() {
+ return mParameterName + mValue;
+ }
+
+ @Override
+ public void setController(Control control) {
+ mControl = control;
+ }
+
+ @Override
+ public int getMaximum() {
+ return mMaximum;
+ }
+
+ @Override
+ public int getMinimum() {
+ return mMinimum;
+ }
+
+ @Override
+ public int getDefaultValue() {
+ return mDefaultValue;
+ }
+
+ @Override
+ public int getValue() {
+ return mValue;
+ }
+
+ @Override
+ public void setValue(int value) {
+ mValue = value;
+ if (mEditor != null) {
+ mEditor.commitLocalRepresentation();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getValueString();
+ }
+
+ @Override
+ public void setFilterView(FilterView editor) {
+ mEditor = editor;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java b/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java
new file mode 100644
index 000000000..fb9f95e97
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/BasicParameterStyle.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2013 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.filtershow.controller;
+
+import android.content.Context;
+
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+
+public class BasicParameterStyle implements ParameterStyles {
+ protected String mParameterName;
+ protected int mSelectedStyle;
+ protected int mNumberOfStyles;
+ protected int mDefaultStyle = 0;
+ protected Control mControl;
+ protected FilterView mEditor;
+ public final int ID;
+ private final String LOGTAG = "BasicParameterStyle";
+
+ @Override
+ public void copyFrom(Parameter src) {
+ if (!(src instanceof BasicParameterStyle)) {
+ throw new IllegalArgumentException(src.getClass().getName());
+ }
+ BasicParameterStyle p = (BasicParameterStyle) src;
+ mNumberOfStyles = p.mNumberOfStyles;
+ mSelectedStyle = p.mSelectedStyle;
+ mDefaultStyle = p.mDefaultStyle;
+ }
+
+ public BasicParameterStyle(int id, int numberOfStyles) {
+ ID = id;
+ mNumberOfStyles = numberOfStyles;
+ }
+
+ @Override
+ public String getParameterName() {
+ return mParameterName;
+ }
+
+ @Override
+ public String getParameterType() {
+ return sParameterType;
+ }
+
+ @Override
+ public String getValueString() {
+ return mParameterName + mSelectedStyle;
+ }
+
+ @Override
+ public void setController(Control control) {
+ mControl = control;
+ }
+
+ @Override
+ public int getNumberOfStyles() {
+ return mNumberOfStyles;
+ }
+
+ @Override
+ public int getDefaultSelected() {
+ return mDefaultStyle;
+ }
+
+ @Override
+ public int getSelected() {
+ return mSelectedStyle;
+ }
+
+ @Override
+ public void setSelected(int selectedStyle) {
+ mSelectedStyle = selectedStyle;
+ if (mEditor != null) {
+ mEditor.commitLocalRepresentation();
+ }
+ }
+
+ @Override
+ public void getIcon(int index, RenderingRequestCaller caller) {
+ mEditor.computeIcon(index, caller);
+ }
+
+ @Override
+ public String getStyleTitle(int index, Context context) {
+ return "";
+ }
+
+ @Override
+ public String toString() {
+ return getValueString();
+ }
+
+ @Override
+ public void setFilterView(FilterView editor) {
+ mEditor = editor;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/BasicSlider.java b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java
new file mode 100644
index 000000000..9d8278d52
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/BasicSlider.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2013 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.filtershow.controller;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public class BasicSlider implements Control {
+ private SeekBar mSeekBar;
+ private ParameterInteger mParameter;
+ Editor mEditor;
+
+ @Override
+ public void setUp(ViewGroup container, Parameter parameter, Editor editor) {
+ container.removeAllViews();
+ mEditor = editor;
+ Context context = container.getContext();
+ mParameter = (ParameterInteger) parameter;
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ LinearLayout lp = (LinearLayout) inflater.inflate(
+ R.layout.filtershow_seekbar, container, true);
+ mSeekBar = (SeekBar) lp.findViewById(R.id.primarySeekBar);
+
+ updateUI();
+ mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (mParameter != null) {
+ mParameter.setValue(progress + mParameter.getMinimum());
+ mEditor.commitLocalRepresentation();
+
+ }
+ }
+ });
+ }
+
+ @Override
+ public View getTopView() {
+ return mSeekBar;
+ }
+
+ @Override
+ public void setPrameter(Parameter parameter) {
+ mParameter = (ParameterInteger) parameter;
+ if (mSeekBar != null) {
+ updateUI();
+ }
+ }
+
+ @Override
+ public void updateUI() {
+ mSeekBar.setMax(mParameter.getMaximum() - mParameter.getMinimum());
+ mSeekBar.setProgress(mParameter.getValue() - mParameter.getMinimum());
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/Control.java b/src/com/android/gallery3d/filtershow/controller/Control.java
new file mode 100644
index 000000000..43422904c
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/Control.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013 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.filtershow.controller;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public interface Control {
+ public void setUp(ViewGroup container, Parameter parameter, Editor editor);
+
+ public View getTopView();
+
+ public void setPrameter(Parameter parameter);
+
+ public void updateUI();
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/FilterView.java b/src/com/android/gallery3d/filtershow/controller/FilterView.java
new file mode 100644
index 000000000..9ca81dc35
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/FilterView.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 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.filtershow.controller;
+
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+
+public interface FilterView {
+ public void computeIcon(int index, RenderingRequestCaller caller);
+
+ public void commitLocalRepresentation();
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/Parameter.java b/src/com/android/gallery3d/filtershow/controller/Parameter.java
new file mode 100644
index 000000000..8f4d5c0a5
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/Parameter.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2013 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.filtershow.controller;
+
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public interface Parameter {
+ String getParameterName();
+
+ String getParameterType();
+
+ String getValueString();
+
+ public void setController(Control c);
+
+ public void setFilterView(FilterView editor);
+
+ public void copyFrom(Parameter src);
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java b/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java
new file mode 100644
index 000000000..8a05c3aa6
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ParameterActionAndInt.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2013 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.filtershow.controller;
+
+public interface ParameterActionAndInt extends ParameterInteger {
+ static String sParameterType = "ParameterActionAndInt";
+
+ public void fireLeftAction();
+
+ public int getLeftIcon();
+
+ public void fireRightAction();
+
+ public int getRightIcon();
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterInteger.java b/src/com/android/gallery3d/filtershow/controller/ParameterInteger.java
new file mode 100644
index 000000000..0bfd20135
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ParameterInteger.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 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.filtershow.controller;
+
+public interface ParameterInteger extends Parameter {
+ static String sParameterType = "ParameterInteger";
+
+ int getMaximum();
+
+ int getMinimum();
+
+ int getDefaultValue();
+
+ int getValue();
+
+ void setValue(int value);
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterSet.java b/src/com/android/gallery3d/filtershow/controller/ParameterSet.java
new file mode 100644
index 000000000..6b50a4d0b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ParameterSet.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2013 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.filtershow.controller;
+
+public interface ParameterSet {
+ int getNumberOfParameters();
+
+ Parameter getFilterParameter(int index);
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/ParameterStyles.java b/src/com/android/gallery3d/filtershow/controller/ParameterStyles.java
new file mode 100644
index 000000000..7d250a0bf
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/ParameterStyles.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2013 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.filtershow.controller;
+
+import android.content.Context;
+
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+
+public interface ParameterStyles extends Parameter {
+ public static String sParameterType = "ParameterStyles";
+
+ int getNumberOfStyles();
+
+ int getDefaultSelected();
+
+ int getSelected();
+
+ void setSelected(int value);
+
+ void getIcon(int index, RenderingRequestCaller caller);
+
+ String getStyleTitle(int index, Context context);
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/StyleChooser.java b/src/com/android/gallery3d/filtershow/controller/StyleChooser.java
new file mode 100644
index 000000000..fb613abc7
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/StyleChooser.java
@@ -0,0 +1,88 @@
+package com.android.gallery3d.filtershow.controller;
+
+import android.app.ActionBar.LayoutParams;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequest;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+import com.android.gallery3d.filtershow.editors.Editor;
+
+import java.util.Vector;
+
+public class StyleChooser implements Control {
+ private final String LOGTAG = "StyleChooser";
+ protected ParameterStyles mParameter;
+ protected LinearLayout mLinearLayout;
+ protected Editor mEditor;
+ private View mTopView;
+ private Vector<ImageButton> mIconButton = new Vector<ImageButton>();
+ protected int mLayoutID = R.layout.filtershow_control_style_chooser;
+
+ @Override
+ public void setUp(ViewGroup container, Parameter parameter, Editor editor) {
+ container.removeAllViews();
+ mEditor = editor;
+ Context context = container.getContext();
+ mParameter = (ParameterStyles) parameter;
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mTopView = inflater.inflate(mLayoutID, container, true);
+ mLinearLayout = (LinearLayout) mTopView.findViewById(R.id.listStyles);
+ mTopView.setVisibility(View.VISIBLE);
+ int n = mParameter.getNumberOfStyles();
+ mIconButton.clear();
+ LayoutParams lp = new LayoutParams(120, 120);
+ for (int i = 0; i < n; i++) {
+ final ImageButton button = new ImageButton(context);
+ button.setScaleType(ScaleType.CENTER_CROP);
+ button.setLayoutParams(lp);
+ button.setBackgroundResource(android.R.color.transparent);
+ mIconButton.add(button);
+ final int buttonNo = i;
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ mParameter.setSelected(buttonNo);
+ }
+ });
+ mLinearLayout.addView(button);
+ mParameter.getIcon(i, new RenderingRequestCaller() {
+ @Override
+ public void available(RenderingRequest request) {
+ Bitmap bmap = request.getBitmap();
+ if (bmap == null) {
+ return;
+ }
+ button.setImageBitmap(bmap);
+ }
+ });
+ }
+ }
+
+ @Override
+ public View getTopView() {
+ return mTopView;
+ }
+
+ @Override
+ public void setPrameter(Parameter parameter) {
+ mParameter = (ParameterStyles) parameter;
+ updateUI();
+ }
+
+ @Override
+ public void updateUI() {
+ if (mParameter == null) {
+ return;
+ }
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/controller/TitledSlider.java b/src/com/android/gallery3d/filtershow/controller/TitledSlider.java
new file mode 100644
index 000000000..f29442bb9
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/controller/TitledSlider.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013 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.filtershow.controller;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.Editor;
+
+public class TitledSlider implements Control {
+ private final String LOGTAG = "ParametricEditor";
+ private SeekBar mSeekBar;
+ private TextView mControlName;
+ private TextView mControlValue;
+ protected ParameterInteger mParameter;
+ Editor mEditor;
+ View mTopView;
+ protected int mLayoutID = R.layout.filtershow_control_title_slider;
+
+ @Override
+ public void setUp(ViewGroup container, Parameter parameter, Editor editor) {
+ container.removeAllViews();
+ mEditor = editor;
+ Context context = container.getContext();
+ mParameter = (ParameterInteger) parameter;
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mTopView = inflater.inflate(mLayoutID, container, true);
+ mTopView.setVisibility(View.VISIBLE);
+ mSeekBar = (SeekBar) mTopView.findViewById(R.id.controlValueSeekBar);
+ mControlName = (TextView) mTopView.findViewById(R.id.controlName);
+ mControlValue = (TextView) mTopView.findViewById(R.id.controlValue);
+ updateUI();
+ mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (mParameter != null) {
+ mParameter.setValue(progress + mParameter.getMinimum());
+ if (mControlName != null) {
+ mControlName.setText(mParameter.getParameterName());
+ }
+ if (mControlValue != null) {
+ mControlValue.setText(Integer.toString(mParameter.getValue()));
+ }
+ mEditor.commitLocalRepresentation();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void setPrameter(Parameter parameter) {
+ mParameter = (ParameterInteger) parameter;
+ if (mSeekBar != null)
+ updateUI();
+ }
+
+ @Override
+ public void updateUI() {
+ if (mControlName != null && mParameter.getParameterName() != null) {
+ mControlName.setText(mParameter.getParameterName().toUpperCase());
+ }
+ if (mControlValue != null) {
+ mControlValue.setText(
+ Integer.toString(mParameter.getValue()));
+ }
+ mSeekBar.setMax(mParameter.getMaximum() - mParameter.getMinimum());
+ mSeekBar.setProgress(mParameter.getValue() - mParameter.getMinimum());
+ mEditor.commitLocalRepresentation();
+ }
+
+ @Override
+ public View getTopView() {
+ return mTopView;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/BoundedRect.java b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java
new file mode 100644
index 000000000..13b8d6de1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2012 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.filtershow.crop;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+
+import java.util.Arrays;
+
+/**
+ * Maintains invariant that inner rectangle is constrained to be within the
+ * outer, rotated rectangle.
+ */
+public class BoundedRect {
+ private float rot;
+ private RectF outer;
+ private RectF inner;
+ private float[] innerRotated;
+
+ public BoundedRect(float rotation, Rect outerRect, Rect innerRect) {
+ rot = rotation;
+ outer = new RectF(outerRect);
+ inner = new RectF(innerRect);
+ innerRotated = CropMath.getCornersFromRect(inner);
+ rotateInner();
+ if (!isConstrained())
+ reconstrain();
+ }
+
+ public BoundedRect(float rotation, RectF outerRect, RectF innerRect) {
+ rot = rotation;
+ outer = new RectF(outerRect);
+ inner = new RectF(innerRect);
+ innerRotated = CropMath.getCornersFromRect(inner);
+ rotateInner();
+ if (!isConstrained())
+ reconstrain();
+ }
+
+ public void resetTo(float rotation, RectF outerRect, RectF innerRect) {
+ rot = rotation;
+ outer.set(outerRect);
+ inner.set(innerRect);
+ innerRotated = CropMath.getCornersFromRect(inner);
+ rotateInner();
+ if (!isConstrained())
+ reconstrain();
+ }
+
+ /**
+ * Sets inner, and re-constrains it to fit within the rotated bounding rect.
+ */
+ public void setInner(RectF newInner) {
+ if (inner.equals(newInner))
+ return;
+ inner = newInner;
+ innerRotated = CropMath.getCornersFromRect(inner);
+ rotateInner();
+ if (!isConstrained())
+ reconstrain();
+ }
+
+ /**
+ * Sets rotation, and re-constrains inner to fit within the rotated bounding rect.
+ */
+ public void setRotation(float rotation) {
+ if (rotation == rot)
+ return;
+ rot = rotation;
+ innerRotated = CropMath.getCornersFromRect(inner);
+ rotateInner();
+ if (!isConstrained())
+ reconstrain();
+ }
+
+ public void setToInner(RectF r) {
+ r.set(inner);
+ }
+
+ public void setToOuter(RectF r) {
+ r.set(outer);
+ }
+
+ public RectF getInner() {
+ return new RectF(inner);
+ }
+
+ public RectF getOuter() {
+ return new RectF(outer);
+ }
+
+ /**
+ * Tries to move the inner rectangle by (dx, dy). If this would cause it to leave
+ * the bounding rectangle, snaps the inner rectangle to the edge of the bounding
+ * rectangle.
+ */
+ public void moveInner(float dx, float dy) {
+ Matrix m0 = getInverseRotMatrix();
+
+ RectF translatedInner = new RectF(inner);
+ translatedInner.offset(dx, dy);
+
+ float[] translatedInnerCorners = CropMath.getCornersFromRect(translatedInner);
+ float[] outerCorners = CropMath.getCornersFromRect(outer);
+
+ m0.mapPoints(translatedInnerCorners);
+ float[] correction = {
+ 0, 0
+ };
+
+ // find correction vectors for corners that have moved out of bounds
+ for (int i = 0; i < translatedInnerCorners.length; i += 2) {
+ float correctedInnerX = translatedInnerCorners[i] + correction[0];
+ float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
+ if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) {
+ float[] badCorner = {
+ correctedInnerX, correctedInnerY
+ };
+ float[] nearestSide = CropMath.closestSide(badCorner, outerCorners);
+ float[] correctionVec =
+ GeometryMathUtils.shortestVectorFromPointToLine(badCorner, nearestSide);
+ correction[0] += correctionVec[0];
+ correction[1] += correctionVec[1];
+ }
+ }
+
+ for (int i = 0; i < translatedInnerCorners.length; i += 2) {
+ float correctedInnerX = translatedInnerCorners[i] + correction[0];
+ float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
+ if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) {
+ float[] correctionVec = {
+ correctedInnerX, correctedInnerY
+ };
+ CropMath.getEdgePoints(outer, correctionVec);
+ correctionVec[0] -= correctedInnerX;
+ correctionVec[1] -= correctedInnerY;
+ correction[0] += correctionVec[0];
+ correction[1] += correctionVec[1];
+ }
+ }
+
+ // Set correction
+ for (int i = 0; i < translatedInnerCorners.length; i += 2) {
+ float correctedInnerX = translatedInnerCorners[i] + correction[0];
+ float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
+ // update translated corners with correction vectors
+ translatedInnerCorners[i] = correctedInnerX;
+ translatedInnerCorners[i + 1] = correctedInnerY;
+ }
+
+ innerRotated = translatedInnerCorners;
+ // reconstrain to update inner
+ reconstrain();
+ }
+
+ /**
+ * Attempts to resize the inner rectangle. If this would cause it to leave
+ * the bounding rect, clips the inner rectangle to fit.
+ */
+ public void resizeInner(RectF newInner) {
+ Matrix m = getRotMatrix();
+ Matrix m0 = getInverseRotMatrix();
+
+ float[] outerCorners = CropMath.getCornersFromRect(outer);
+ m.mapPoints(outerCorners);
+ float[] oldInnerCorners = CropMath.getCornersFromRect(inner);
+ float[] newInnerCorners = CropMath.getCornersFromRect(newInner);
+ RectF ret = new RectF(newInner);
+
+ for (int i = 0; i < newInnerCorners.length; i += 2) {
+ float[] c = {
+ newInnerCorners[i], newInnerCorners[i + 1]
+ };
+ float[] c0 = Arrays.copyOf(c, 2);
+ m0.mapPoints(c0);
+ if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) {
+ float[] outerSide = CropMath.closestSide(c, outerCorners);
+ float[] pathOfCorner = {
+ newInnerCorners[i], newInnerCorners[i + 1],
+ oldInnerCorners[i], oldInnerCorners[i + 1]
+ };
+ float[] p = GeometryMathUtils.lineIntersect(pathOfCorner, outerSide);
+ if (p == null) {
+ // lines are parallel or not well defined, so don't resize
+ p = new float[2];
+ p[0] = oldInnerCorners[i];
+ p[1] = oldInnerCorners[i + 1];
+ }
+ // relies on corners being in same order as method
+ // getCornersFromRect
+ switch (i) {
+ case 0:
+ case 1:
+ ret.left = (p[0] > ret.left) ? p[0] : ret.left;
+ ret.top = (p[1] > ret.top) ? p[1] : ret.top;
+ break;
+ case 2:
+ case 3:
+ ret.right = (p[0] < ret.right) ? p[0] : ret.right;
+ ret.top = (p[1] > ret.top) ? p[1] : ret.top;
+ break;
+ case 4:
+ case 5:
+ ret.right = (p[0] < ret.right) ? p[0] : ret.right;
+ ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom;
+ break;
+ case 6:
+ case 7:
+ ret.left = (p[0] > ret.left) ? p[0] : ret.left;
+ ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ float[] retCorners = CropMath.getCornersFromRect(ret);
+ m0.mapPoints(retCorners);
+ innerRotated = retCorners;
+ // reconstrain to update inner
+ reconstrain();
+ }
+
+ /**
+ * Attempts to resize the inner rectangle. If this would cause it to leave
+ * the bounding rect, clips the inner rectangle to fit while maintaining
+ * aspect ratio.
+ */
+ public void fixedAspectResizeInner(RectF newInner) {
+ Matrix m = getRotMatrix();
+ Matrix m0 = getInverseRotMatrix();
+
+ float aspectW = inner.width();
+ float aspectH = inner.height();
+ float aspRatio = aspectW / aspectH;
+ float[] corners = CropMath.getCornersFromRect(outer);
+
+ m.mapPoints(corners);
+ float[] oldInnerCorners = CropMath.getCornersFromRect(inner);
+ float[] newInnerCorners = CropMath.getCornersFromRect(newInner);
+
+ // find fixed corner
+ int fixed = -1;
+ if (inner.top == newInner.top) {
+ if (inner.left == newInner.left)
+ fixed = 0; // top left
+ else if (inner.right == newInner.right)
+ fixed = 2; // top right
+ } else if (inner.bottom == newInner.bottom) {
+ if (inner.right == newInner.right)
+ fixed = 4; // bottom right
+ else if (inner.left == newInner.left)
+ fixed = 6; // bottom left
+ }
+ // no fixed corner, return without update
+ if (fixed == -1)
+ return;
+ float widthSoFar = newInner.width();
+ int moved = -1;
+ for (int i = 0; i < newInnerCorners.length; i += 2) {
+ float[] c = {
+ newInnerCorners[i], newInnerCorners[i + 1]
+ };
+ float[] c0 = Arrays.copyOf(c, 2);
+ m0.mapPoints(c0);
+ if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) {
+ moved = i;
+ if (moved == fixed)
+ continue;
+ float[] l2 = CropMath.closestSide(c, corners);
+ float[] l1 = {
+ newInnerCorners[i], newInnerCorners[i + 1],
+ oldInnerCorners[i], oldInnerCorners[i + 1]
+ };
+ float[] p = GeometryMathUtils.lineIntersect(l1, l2);
+ if (p == null) {
+ // lines are parallel or not well defined, so set to old
+ // corner
+ p = new float[2];
+ p[0] = oldInnerCorners[i];
+ p[1] = oldInnerCorners[i + 1];
+ }
+ // relies on corners being in same order as method
+ // getCornersFromRect
+ float fixed_x = oldInnerCorners[fixed];
+ float fixed_y = oldInnerCorners[fixed + 1];
+ float newWidth = Math.abs(fixed_x - p[0]);
+ float newHeight = Math.abs(fixed_y - p[1]);
+ newWidth = Math.max(newWidth, aspRatio * newHeight);
+ if (newWidth < widthSoFar)
+ widthSoFar = newWidth;
+ }
+ }
+
+ float heightSoFar = widthSoFar / aspRatio;
+ RectF ret = new RectF(inner);
+ if (fixed == 0) {
+ ret.right = ret.left + widthSoFar;
+ ret.bottom = ret.top + heightSoFar;
+ } else if (fixed == 2) {
+ ret.left = ret.right - widthSoFar;
+ ret.bottom = ret.top + heightSoFar;
+ } else if (fixed == 4) {
+ ret.left = ret.right - widthSoFar;
+ ret.top = ret.bottom - heightSoFar;
+ } else if (fixed == 6) {
+ ret.right = ret.left + widthSoFar;
+ ret.top = ret.bottom - heightSoFar;
+ }
+ float[] retCorners = CropMath.getCornersFromRect(ret);
+ m0.mapPoints(retCorners);
+ innerRotated = retCorners;
+ // reconstrain to update inner
+ reconstrain();
+ }
+
+ // internal methods
+
+ private boolean isConstrained() {
+ for (int i = 0; i < 8; i += 2) {
+ if (!CropMath.inclusiveContains(outer, innerRotated[i], innerRotated[i + 1]))
+ return false;
+ }
+ return true;
+ }
+
+ private void reconstrain() {
+ // innerRotated has been changed to have incorrect values
+ CropMath.getEdgePoints(outer, innerRotated);
+ Matrix m = getRotMatrix();
+ float[] unrotated = Arrays.copyOf(innerRotated, 8);
+ m.mapPoints(unrotated);
+ inner = CropMath.trapToRect(unrotated);
+ }
+
+ private void rotateInner() {
+ Matrix m = getInverseRotMatrix();
+ m.mapPoints(innerRotated);
+ }
+
+ private Matrix getRotMatrix() {
+ Matrix m = new Matrix();
+ m.setRotate(rot, outer.centerX(), outer.centerY());
+ return m;
+ }
+
+ private Matrix getInverseRotMatrix() {
+ Matrix m = new Matrix();
+ m.setRotate(-rot, outer.centerX(), outer.centerY());
+ return m;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropActivity.java b/src/com/android/gallery3d/filtershow/crop/CropActivity.java
new file mode 100644
index 000000000..0a0c36703
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropActivity.java
@@ -0,0 +1,697 @@
+/*
+ * Copyright (C) 2013 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.filtershow.crop;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Activity for cropping an image.
+ */
+public class CropActivity extends Activity {
+ private static final String LOGTAG = "CropActivity";
+ public static final String CROP_ACTION = "com.android.camera.action.CROP";
+ private CropExtras mCropExtras = null;
+ private LoadBitmapTask mLoadBitmapTask = null;
+
+ private int mOutputX = 0;
+ private int mOutputY = 0;
+ private Bitmap mOriginalBitmap = null;
+ private RectF mOriginalBounds = null;
+ private int mOriginalRotation = 0;
+ private Uri mSourceUri = null;
+ private CropView mCropView = null;
+ private View mSaveButton = null;
+ private boolean finalIOGuard = false;
+
+ private static final int SELECT_PICTURE = 1; // request code for picker
+
+ private static final int DEFAULT_COMPRESS_QUALITY = 90;
+ /**
+ * The maximum bitmap size we allow to be returned through the intent.
+ * Intents have a maximum of 1MB in total size. However, the Bitmap seems to
+ * have some overhead to hit so that we go way below the limit here to make
+ * sure the intent stays below 1MB.We should consider just returning a byte
+ * array instead of a Bitmap instance to avoid overhead.
+ */
+ public static final int MAX_BMAP_IN_INTENT = 750000;
+
+ // Flags
+ private static final int DO_SET_WALLPAPER = 1;
+ private static final int DO_RETURN_DATA = 1 << 1;
+ private static final int DO_EXTRA_OUTPUT = 1 << 2;
+
+ private static final int FLAG_CHECK = DO_SET_WALLPAPER | DO_RETURN_DATA | DO_EXTRA_OUTPUT;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+ setResult(RESULT_CANCELED, new Intent());
+ mCropExtras = getExtrasFromIntent(intent);
+ if (mCropExtras != null && mCropExtras.getShowWhenLocked()) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ }
+
+ setContentView(R.layout.crop_activity);
+ mCropView = (CropView) findViewById(R.id.cropView);
+
+ ActionBar actionBar = getActionBar();
+ actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+ actionBar.setCustomView(R.layout.filtershow_actionbar);
+
+ View mSaveButton = actionBar.getCustomView();
+ mSaveButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ startFinishOutput();
+ }
+ });
+
+ if (intent.getData() != null) {
+ mSourceUri = intent.getData();
+ startLoadBitmap(mSourceUri);
+ } else {
+ pickImage();
+ }
+ }
+
+ private void enableSave(boolean enable) {
+ if (mSaveButton != null) {
+ mSaveButton.setEnabled(enable);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mLoadBitmapTask != null) {
+ mLoadBitmapTask.cancel(false);
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ public void onConfigurationChanged (Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mCropView.configChanged();
+ }
+
+ /**
+ * Opens a selector in Gallery to chose an image for use when none was given
+ * in the CROP intent.
+ */
+ private void pickImage() {
+ Intent intent = new Intent();
+ intent.setType("image/*");
+ intent.setAction(Intent.ACTION_GET_CONTENT);
+ startActivityForResult(Intent.createChooser(intent, getString(R.string.select_image)),
+ SELECT_PICTURE);
+ }
+
+ /**
+ * Callback for pickImage().
+ */
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == RESULT_OK && requestCode == SELECT_PICTURE) {
+ mSourceUri = data.getData();
+ startLoadBitmap(mSourceUri);
+ }
+ }
+
+ /**
+ * Gets screen size metric.
+ */
+ private int getScreenImageSize() {
+ DisplayMetrics outMetrics = new DisplayMetrics();
+ getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
+ return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels);
+ }
+
+ /**
+ * Method that loads a bitmap in an async task.
+ */
+ private void startLoadBitmap(Uri uri) {
+ if (uri != null) {
+ enableSave(false);
+ final View loading = findViewById(R.id.loading);
+ loading.setVisibility(View.VISIBLE);
+ mLoadBitmapTask = new LoadBitmapTask();
+ mLoadBitmapTask.execute(uri);
+ } else {
+ cannotLoadImage();
+ done();
+ }
+ }
+
+ /**
+ * Method called on UI thread with loaded bitmap.
+ */
+ private void doneLoadBitmap(Bitmap bitmap, RectF bounds, int orientation) {
+ final View loading = findViewById(R.id.loading);
+ loading.setVisibility(View.GONE);
+ mOriginalBitmap = bitmap;
+ mOriginalBounds = bounds;
+ mOriginalRotation = orientation;
+ if (bitmap != null && bitmap.getWidth() != 0 && bitmap.getHeight() != 0) {
+ RectF imgBounds = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ mCropView.initialize(bitmap, imgBounds, imgBounds, orientation);
+ if (mCropExtras != null) {
+ int aspectX = mCropExtras.getAspectX();
+ int aspectY = mCropExtras.getAspectY();
+ mOutputX = mCropExtras.getOutputX();
+ mOutputY = mCropExtras.getOutputY();
+ if (mOutputX > 0 && mOutputY > 0) {
+ mCropView.applyAspect(mOutputX, mOutputY);
+
+ }
+ float spotX = mCropExtras.getSpotlightX();
+ float spotY = mCropExtras.getSpotlightY();
+ if (spotX > 0 && spotY > 0) {
+ mCropView.setWallpaperSpotlight(spotX, spotY);
+ }
+ if (aspectX > 0 && aspectY > 0) {
+ mCropView.applyAspect(aspectX, aspectY);
+ }
+ }
+ enableSave(true);
+ } else {
+ Log.w(LOGTAG, "could not load image for cropping");
+ cannotLoadImage();
+ setResult(RESULT_CANCELED, new Intent());
+ done();
+ }
+ }
+
+ /**
+ * Display toast for image loading failure.
+ */
+ private void cannotLoadImage() {
+ CharSequence text = getString(R.string.cannot_load_image);
+ Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+
+ /**
+ * AsyncTask for loading a bitmap into memory.
+ *
+ * @see #startLoadBitmap(Uri)
+ * @see #doneLoadBitmap(Bitmap)
+ */
+ private class LoadBitmapTask extends AsyncTask<Uri, Void, Bitmap> {
+ int mBitmapSize;
+ Context mContext;
+ Rect mOriginalBounds;
+ int mOrientation;
+
+ public LoadBitmapTask() {
+ mBitmapSize = getScreenImageSize();
+ mContext = getApplicationContext();
+ mOriginalBounds = new Rect();
+ mOrientation = 0;
+ }
+
+ @Override
+ protected Bitmap doInBackground(Uri... params) {
+ Uri uri = params[0];
+ Bitmap bmap = ImageLoader.loadConstrainedBitmap(uri, mContext, mBitmapSize,
+ mOriginalBounds, false);
+ mOrientation = ImageLoader.getMetadataRotation(mContext, uri);
+ return bmap;
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap result) {
+ doneLoadBitmap(result, new RectF(mOriginalBounds), mOrientation);
+ }
+ }
+
+ private void startFinishOutput() {
+ if (finalIOGuard) {
+ return;
+ } else {
+ finalIOGuard = true;
+ }
+ enableSave(false);
+ Uri destinationUri = null;
+ int flags = 0;
+ if (mOriginalBitmap != null && mCropExtras != null) {
+ if (mCropExtras.getExtraOutput() != null) {
+ destinationUri = mCropExtras.getExtraOutput();
+ if (destinationUri != null) {
+ flags |= DO_EXTRA_OUTPUT;
+ }
+ }
+ if (mCropExtras.getSetAsWallpaper()) {
+ flags |= DO_SET_WALLPAPER;
+ }
+ if (mCropExtras.getReturnData()) {
+ flags |= DO_RETURN_DATA;
+ }
+ }
+ if (flags == 0) {
+ destinationUri = SaveImage.makeAndInsertUri(this, mSourceUri);
+ if (destinationUri != null) {
+ flags |= DO_EXTRA_OUTPUT;
+ }
+ }
+ if ((flags & FLAG_CHECK) != 0 && mOriginalBitmap != null) {
+ RectF photo = new RectF(0, 0, mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight());
+ RectF crop = getBitmapCrop(photo);
+ startBitmapIO(flags, mOriginalBitmap, mSourceUri, destinationUri, crop,
+ photo, mOriginalBounds,
+ (mCropExtras == null) ? null : mCropExtras.getOutputFormat(), mOriginalRotation);
+ return;
+ }
+ setResult(RESULT_CANCELED, new Intent());
+ done();
+ return;
+ }
+
+ private void startBitmapIO(int flags, Bitmap currentBitmap, Uri sourceUri, Uri destUri,
+ RectF cropBounds, RectF photoBounds, RectF currentBitmapBounds, String format,
+ int rotation) {
+ if (cropBounds == null || photoBounds == null || currentBitmap == null
+ || currentBitmap.getWidth() == 0 || currentBitmap.getHeight() == 0
+ || cropBounds.width() == 0 || cropBounds.height() == 0 || photoBounds.width() == 0
+ || photoBounds.height() == 0) {
+ return; // fail fast
+ }
+ if ((flags & FLAG_CHECK) == 0) {
+ return; // no output options
+ }
+ if ((flags & DO_SET_WALLPAPER) != 0) {
+ Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show();
+ }
+
+ final View loading = findViewById(R.id.loading);
+ loading.setVisibility(View.VISIBLE);
+ BitmapIOTask ioTask = new BitmapIOTask(sourceUri, destUri, format, flags, cropBounds,
+ photoBounds, currentBitmapBounds, rotation, mOutputX, mOutputY);
+ ioTask.execute(currentBitmap);
+ }
+
+ private void doneBitmapIO(boolean success, Intent intent) {
+ final View loading = findViewById(R.id.loading);
+ loading.setVisibility(View.GONE);
+ if (success) {
+ setResult(RESULT_OK, intent);
+ } else {
+ setResult(RESULT_CANCELED, intent);
+ }
+ done();
+ }
+
+ private class BitmapIOTask extends AsyncTask<Bitmap, Void, Boolean> {
+
+ private final WallpaperManager mWPManager;
+ InputStream mInStream = null;
+ OutputStream mOutStream = null;
+ String mOutputFormat = null;
+ Uri mOutUri = null;
+ Uri mInUri = null;
+ int mFlags = 0;
+ RectF mCrop = null;
+ RectF mPhoto = null;
+ RectF mOrig = null;
+ Intent mResultIntent = null;
+ int mRotation = 0;
+
+ // Helper to setup input stream
+ private void regenerateInputStream() {
+ if (mInUri == null) {
+ Log.w(LOGTAG, "cannot read original file, no input URI given");
+ } else {
+ Utils.closeSilently(mInStream);
+ try {
+ mInStream = getContentResolver().openInputStream(mInUri);
+ } catch (FileNotFoundException e) {
+ Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
+ }
+ }
+ }
+
+ public BitmapIOTask(Uri sourceUri, Uri destUri, String outputFormat, int flags,
+ RectF cropBounds, RectF photoBounds, RectF originalBitmapBounds, int rotation,
+ int outputX, int outputY) {
+ mOutputFormat = outputFormat;
+ mOutStream = null;
+ mOutUri = destUri;
+ mInUri = sourceUri;
+ mFlags = flags;
+ mCrop = cropBounds;
+ mPhoto = photoBounds;
+ mOrig = originalBitmapBounds;
+ mWPManager = WallpaperManager.getInstance(getApplicationContext());
+ mResultIntent = new Intent();
+ mRotation = (rotation < 0) ? -rotation : rotation;
+ mRotation %= 360;
+ mRotation = 90 * (int) (mRotation / 90); // now mRotation is a multiple of 90
+ mOutputX = outputX;
+ mOutputY = outputY;
+
+ if ((flags & DO_EXTRA_OUTPUT) != 0) {
+ if (mOutUri == null) {
+ Log.w(LOGTAG, "cannot write file, no output URI given");
+ } else {
+ try {
+ mOutStream = getContentResolver().openOutputStream(mOutUri);
+ } catch (FileNotFoundException e) {
+ Log.w(LOGTAG, "cannot write file: " + mOutUri.toString(), e);
+ }
+ }
+ }
+
+ if ((flags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0) {
+ regenerateInputStream();
+ }
+ }
+
+ @Override
+ protected Boolean doInBackground(Bitmap... params) {
+ boolean failure = false;
+ Bitmap img = params[0];
+
+ // Set extra for crop bounds
+ if (mCrop != null && mPhoto != null && mOrig != null) {
+ RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
+ Matrix m = new Matrix();
+ m.setRotate(mRotation);
+ m.mapRect(trueCrop);
+ if (trueCrop != null) {
+ Rect rounded = new Rect();
+ trueCrop.roundOut(rounded);
+ mResultIntent.putExtra(CropExtras.KEY_CROPPED_RECT, rounded);
+ }
+ }
+
+ // Find the small cropped bitmap that is returned in the intent
+ if ((mFlags & DO_RETURN_DATA) != 0) {
+ assert (img != null);
+ Bitmap ret = getCroppedImage(img, mCrop, mPhoto);
+ if (ret != null) {
+ ret = getDownsampledBitmap(ret, MAX_BMAP_IN_INTENT);
+ }
+ if (ret == null) {
+ Log.w(LOGTAG, "could not downsample bitmap to return in data");
+ failure = true;
+ } else {
+ if (mRotation > 0) {
+ Matrix m = new Matrix();
+ m.setRotate(mRotation);
+ Bitmap tmp = Bitmap.createBitmap(ret, 0, 0, ret.getWidth(),
+ ret.getHeight(), m, true);
+ if (tmp != null) {
+ ret = tmp;
+ }
+ }
+ mResultIntent.putExtra(CropExtras.KEY_DATA, ret);
+ }
+ }
+
+ // Do the large cropped bitmap and/or set the wallpaper
+ if ((mFlags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0 && mInStream != null) {
+ // Find crop bounds (scaled to original image size)
+ RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
+ if (trueCrop == null) {
+ Log.w(LOGTAG, "cannot find crop for full size image");
+ failure = true;
+ return false;
+ }
+ Rect roundedTrueCrop = new Rect();
+ trueCrop.roundOut(roundedTrueCrop);
+
+ if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
+ Log.w(LOGTAG, "crop has bad values for full size image");
+ failure = true;
+ return false;
+ }
+
+ // Attempt to open a region decoder
+ BitmapRegionDecoder decoder = null;
+ try {
+ decoder = BitmapRegionDecoder.newInstance(mInStream, true);
+ } catch (IOException e) {
+ Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
+ }
+
+ Bitmap crop = null;
+ if (decoder != null) {
+ // Do region decoding to get crop bitmap
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ crop = decoder.decodeRegion(roundedTrueCrop, options);
+ decoder.recycle();
+ }
+
+ if (crop == null) {
+ // BitmapRegionDecoder has failed, try to crop in-memory
+ regenerateInputStream();
+ Bitmap fullSize = null;
+ if (mInStream != null) {
+ fullSize = BitmapFactory.decodeStream(mInStream);
+ }
+ if (fullSize != null) {
+ crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
+ roundedTrueCrop.top, roundedTrueCrop.width(),
+ roundedTrueCrop.height());
+ }
+ }
+
+ if (crop == null) {
+ Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
+ failure = true;
+ return false;
+ }
+ if (mOutputX > 0 && mOutputY > 0) {
+ Matrix m = new Matrix();
+ RectF cropRect = new RectF(0, 0, crop.getWidth(), crop.getHeight());
+ if (mRotation > 0) {
+ m.setRotate(mRotation);
+ m.mapRect(cropRect);
+ }
+ RectF returnRect = new RectF(0, 0, mOutputX, mOutputY);
+ m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
+ m.preRotate(mRotation);
+ Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
+ (int) returnRect.height(), Bitmap.Config.ARGB_8888);
+ if (tmp != null) {
+ Canvas c = new Canvas(tmp);
+ c.drawBitmap(crop, m, new Paint());
+ crop = tmp;
+ }
+ } else if (mRotation > 0) {
+ Matrix m = new Matrix();
+ m.setRotate(mRotation);
+ Bitmap tmp = Bitmap.createBitmap(crop, 0, 0, crop.getWidth(),
+ crop.getHeight(), m, true);
+ if (tmp != null) {
+ crop = tmp;
+ }
+ }
+ // Get output compression format
+ CompressFormat cf =
+ convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
+
+ // If we only need to output to a URI, compress straight to file
+ if (mFlags == DO_EXTRA_OUTPUT) {
+ if (mOutStream == null
+ || !crop.compress(cf, DEFAULT_COMPRESS_QUALITY, mOutStream)) {
+ Log.w(LOGTAG, "failed to compress bitmap to file: " + mOutUri.toString());
+ failure = true;
+ } else {
+ mResultIntent.setData(mOutUri);
+ }
+ } else {
+ // Compress to byte array
+ ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
+ if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
+
+ // If we need to output to a Uri, write compressed
+ // bitmap out
+ if ((mFlags & DO_EXTRA_OUTPUT) != 0) {
+ if (mOutStream == null) {
+ Log.w(LOGTAG,
+ "failed to compress bitmap to file: " + mOutUri.toString());
+ failure = true;
+ } else {
+ try {
+ mOutStream.write(tmpOut.toByteArray());
+ mResultIntent.setData(mOutUri);
+ } catch (IOException e) {
+ Log.w(LOGTAG,
+ "failed to compress bitmap to file: "
+ + mOutUri.toString(), e);
+ failure = true;
+ }
+ }
+ }
+
+ // If we need to set to the wallpaper, set it
+ if ((mFlags & DO_SET_WALLPAPER) != 0 && mWPManager != null) {
+ if (mWPManager == null) {
+ Log.w(LOGTAG, "no wallpaper manager");
+ failure = true;
+ } else {
+ try {
+ mWPManager.setStream(new ByteArrayInputStream(tmpOut
+ .toByteArray()));
+ } catch (IOException e) {
+ Log.w(LOGTAG, "cannot write stream to wallpaper", e);
+ failure = true;
+ }
+ }
+ }
+ } else {
+ Log.w(LOGTAG, "cannot compress bitmap");
+ failure = true;
+ }
+ }
+ }
+ return !failure; // True if any of the operations failed
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ Utils.closeSilently(mOutStream);
+ Utils.closeSilently(mInStream);
+ doneBitmapIO(result.booleanValue(), mResultIntent);
+ }
+
+ }
+
+ private void done() {
+ finish();
+ }
+
+ protected static Bitmap getCroppedImage(Bitmap image, RectF cropBounds, RectF photoBounds) {
+ RectF imageBounds = new RectF(0, 0, image.getWidth(), image.getHeight());
+ RectF crop = CropMath.getScaledCropBounds(cropBounds, photoBounds, imageBounds);
+ if (crop == null) {
+ return null;
+ }
+ Rect intCrop = new Rect();
+ crop.roundOut(intCrop);
+ return Bitmap.createBitmap(image, intCrop.left, intCrop.top, intCrop.width(),
+ intCrop.height());
+ }
+
+ protected static Bitmap getDownsampledBitmap(Bitmap image, int max_size) {
+ if (image == null || image.getWidth() == 0 || image.getHeight() == 0 || max_size < 16) {
+ throw new IllegalArgumentException("Bad argument to getDownsampledBitmap()");
+ }
+ int shifts = 0;
+ int size = CropMath.getBitmapSize(image);
+ while (size > max_size) {
+ shifts++;
+ size /= 4;
+ }
+ Bitmap ret = Bitmap.createScaledBitmap(image, image.getWidth() >> shifts,
+ image.getHeight() >> shifts, true);
+ if (ret == null) {
+ return null;
+ }
+ // Handle edge case for rounding.
+ if (CropMath.getBitmapSize(ret) > max_size) {
+ return Bitmap.createScaledBitmap(ret, ret.getWidth() >> 1, ret.getHeight() >> 1, true);
+ }
+ return ret;
+ }
+
+ /**
+ * Gets the crop extras from the intent, or null if none exist.
+ */
+ protected static CropExtras getExtrasFromIntent(Intent intent) {
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ return new CropExtras(extras.getInt(CropExtras.KEY_OUTPUT_X, 0),
+ extras.getInt(CropExtras.KEY_OUTPUT_Y, 0),
+ extras.getBoolean(CropExtras.KEY_SCALE, true) &&
+ extras.getBoolean(CropExtras.KEY_SCALE_UP_IF_NEEDED, false),
+ extras.getInt(CropExtras.KEY_ASPECT_X, 0),
+ extras.getInt(CropExtras.KEY_ASPECT_Y, 0),
+ extras.getBoolean(CropExtras.KEY_SET_AS_WALLPAPER, false),
+ extras.getBoolean(CropExtras.KEY_RETURN_DATA, false),
+ (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT),
+ extras.getString(CropExtras.KEY_OUTPUT_FORMAT),
+ extras.getBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, false),
+ extras.getFloat(CropExtras.KEY_SPOTLIGHT_X),
+ extras.getFloat(CropExtras.KEY_SPOTLIGHT_Y));
+ }
+ return null;
+ }
+
+ protected static CompressFormat convertExtensionToCompressFormat(String extension) {
+ return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
+ }
+
+ protected static String getFileExtension(String requestFormat) {
+ String outputFormat = (requestFormat == null)
+ ? "jpg"
+ : requestFormat;
+ outputFormat = outputFormat.toLowerCase();
+ return (outputFormat.equals("png") || outputFormat.equals("gif"))
+ ? "png" // We don't support gif compression.
+ : "jpg";
+ }
+
+ private RectF getBitmapCrop(RectF imageBounds) {
+ RectF crop = mCropView.getCrop();
+ RectF photo = mCropView.getPhoto();
+ if (crop == null || photo == null) {
+ Log.w(LOGTAG, "could not get crop");
+ return null;
+ }
+ RectF scaledCrop = CropMath.getScaledCropBounds(crop, photo, imageBounds);
+ return scaledCrop;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java b/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java
new file mode 100644
index 000000000..b0d324cbb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2013 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.filtershow.crop;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.drawable.Drawable;
+
+public abstract class CropDrawingUtils {
+
+ public static void drawRuleOfThird(Canvas canvas, RectF bounds) {
+ Paint p = new Paint();
+ p.setStyle(Paint.Style.STROKE);
+ p.setColor(Color.argb(128, 255, 255, 255));
+ p.setStrokeWidth(2);
+ float stepX = bounds.width() / 3.0f;
+ float stepY = bounds.height() / 3.0f;
+ float x = bounds.left + stepX;
+ float y = bounds.top + stepY;
+ for (int i = 0; i < 2; i++) {
+ canvas.drawLine(x, bounds.top, x, bounds.bottom, p);
+ x += stepX;
+ }
+ for (int j = 0; j < 2; j++) {
+ canvas.drawLine(bounds.left, y, bounds.right, y, p);
+ y += stepY;
+ }
+ }
+
+ public static void drawCropRect(Canvas canvas, RectF bounds) {
+ Paint p = new Paint();
+ p.setStyle(Paint.Style.STROKE);
+ p.setColor(Color.WHITE);
+ p.setStrokeWidth(3);
+ canvas.drawRect(bounds, p);
+ }
+
+ public static void drawIndicator(Canvas canvas, Drawable indicator, int indicatorSize,
+ float centerX, float centerY) {
+ int left = (int) centerX - indicatorSize / 2;
+ int top = (int) centerY - indicatorSize / 2;
+ indicator.setBounds(left, top, left + indicatorSize, top + indicatorSize);
+ indicator.draw(canvas);
+ }
+
+ public static void drawIndicators(Canvas canvas, Drawable cropIndicator, int indicatorSize,
+ RectF bounds, boolean fixedAspect, int selection) {
+ boolean notMoving = (selection == CropObject.MOVE_NONE);
+ if (fixedAspect) {
+ if ((selection == CropObject.TOP_LEFT) || notMoving) {
+ drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.top);
+ }
+ if ((selection == CropObject.TOP_RIGHT) || notMoving) {
+ drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.top);
+ }
+ if ((selection == CropObject.BOTTOM_LEFT) || notMoving) {
+ drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.bottom);
+ }
+ if ((selection == CropObject.BOTTOM_RIGHT) || notMoving) {
+ drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.bottom);
+ }
+ } else {
+ if (((selection & CropObject.MOVE_TOP) != 0) || notMoving) {
+ drawIndicator(canvas, cropIndicator, indicatorSize, bounds.centerX(), bounds.top);
+ }
+ if (((selection & CropObject.MOVE_BOTTOM) != 0) || notMoving) {
+ drawIndicator(canvas, cropIndicator, indicatorSize, bounds.centerX(), bounds.bottom);
+ }
+ if (((selection & CropObject.MOVE_LEFT) != 0) || notMoving) {
+ drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.centerY());
+ }
+ if (((selection & CropObject.MOVE_RIGHT) != 0) || notMoving) {
+ drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.centerY());
+ }
+ }
+ }
+
+ public static void drawWallpaperSelectionFrame(Canvas canvas, RectF cropBounds, float spotX,
+ float spotY, Paint p, Paint shadowPaint) {
+ float sx = cropBounds.width() * spotX;
+ float sy = cropBounds.height() * spotY;
+ float cx = cropBounds.centerX();
+ float cy = cropBounds.centerY();
+ RectF r1 = new RectF(cx - sx / 2, cy - sy / 2, cx + sx / 2, cy + sy / 2);
+ float temp = sx;
+ sx = sy;
+ sy = temp;
+ RectF r2 = new RectF(cx - sx / 2, cy - sy / 2, cx + sx / 2, cy + sy / 2);
+ canvas.save();
+ canvas.clipRect(cropBounds);
+ canvas.clipRect(r1, Region.Op.DIFFERENCE);
+ canvas.clipRect(r2, Region.Op.DIFFERENCE);
+ canvas.drawPaint(shadowPaint);
+ canvas.restore();
+ Path path = new Path();
+ path.moveTo(r1.left, r1.top);
+ path.lineTo(r1.right, r1.top);
+ path.moveTo(r1.left, r1.top);
+ path.lineTo(r1.left, r1.bottom);
+ path.moveTo(r1.left, r1.bottom);
+ path.lineTo(r1.right, r1.bottom);
+ path.moveTo(r1.right, r1.top);
+ path.lineTo(r1.right, r1.bottom);
+ path.moveTo(r2.left, r2.top);
+ path.lineTo(r2.right, r2.top);
+ path.moveTo(r2.right, r2.top);
+ path.lineTo(r2.right, r2.bottom);
+ path.moveTo(r2.left, r2.bottom);
+ path.lineTo(r2.right, r2.bottom);
+ path.moveTo(r2.left, r2.top);
+ path.lineTo(r2.left, r2.bottom);
+ canvas.drawPath(path, p);
+ }
+
+ public static void drawShadows(Canvas canvas, Paint p, RectF innerBounds, RectF outerBounds) {
+ canvas.drawRect(outerBounds.left, outerBounds.top, innerBounds.right, innerBounds.top, p);
+ canvas.drawRect(innerBounds.right, outerBounds.top, outerBounds.right, innerBounds.bottom,
+ p);
+ canvas.drawRect(innerBounds.left, innerBounds.bottom, outerBounds.right,
+ outerBounds.bottom, p);
+ canvas.drawRect(outerBounds.left, innerBounds.top, innerBounds.left, outerBounds.bottom, p);
+ }
+
+ public static Matrix getBitmapToDisplayMatrix(RectF imageBounds, RectF displayBounds) {
+ Matrix m = new Matrix();
+ CropDrawingUtils.setBitmapToDisplayMatrix(m, imageBounds, displayBounds);
+ return m;
+ }
+
+ public static boolean setBitmapToDisplayMatrix(Matrix m, RectF imageBounds,
+ RectF displayBounds) {
+ m.reset();
+ return m.setRectToRect(imageBounds, displayBounds, Matrix.ScaleToFit.CENTER);
+ }
+
+ public static boolean setImageToScreenMatrix(Matrix dst, RectF image,
+ RectF screen, int rotation) {
+ RectF rotatedImage = new RectF();
+ dst.setRotate(rotation, image.centerX(), image.centerY());
+ if (!dst.mapRect(rotatedImage, image)) {
+ return false; // fails for rotations that are not multiples of 90
+ // degrees
+ }
+ boolean rToR = dst.setRectToRect(rotatedImage, screen, Matrix.ScaleToFit.CENTER);
+ boolean rot = dst.preRotate(rotation, image.centerX(), image.centerY());
+ return rToR && rot;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropExtras.java b/src/com/android/gallery3d/filtershow/crop/CropExtras.java
new file mode 100644
index 000000000..60fe9af53
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropExtras.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 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.filtershow.crop;
+
+import android.net.Uri;
+
+public class CropExtras {
+
+ public static final String KEY_CROPPED_RECT = "cropped-rect";
+ 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_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded";
+ public static final String KEY_ASPECT_X = "aspectX";
+ public static final String KEY_ASPECT_Y = "aspectY";
+ public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper";
+ public static final String KEY_RETURN_DATA = "return-data";
+ public static final String KEY_DATA = "data";
+ public static final String KEY_SPOTLIGHT_X = "spotlightX";
+ public static final String KEY_SPOTLIGHT_Y = "spotlightY";
+ public static final String KEY_SHOW_WHEN_LOCKED = "showWhenLocked";
+ public static final String KEY_OUTPUT_FORMAT = "outputFormat";
+
+ private int mOutputX = 0;
+ private int mOutputY = 0;
+ private boolean mScaleUp = true;
+ private int mAspectX = 0;
+ private int mAspectY = 0;
+ private boolean mSetAsWallpaper = false;
+ private boolean mReturnData = false;
+ private Uri mExtraOutput = null;
+ private String mOutputFormat = null;
+ private boolean mShowWhenLocked = false;
+ private float mSpotlightX = 0;
+ private float mSpotlightY = 0;
+
+ public CropExtras(int outputX, int outputY, boolean scaleUp, int aspectX, int aspectY,
+ boolean setAsWallpaper, boolean returnData, Uri extraOutput, String outputFormat,
+ boolean showWhenLocked, float spotlightX, float spotlightY) {
+ mOutputX = outputX;
+ mOutputY = outputY;
+ mScaleUp = scaleUp;
+ mAspectX = aspectX;
+ mAspectY = aspectY;
+ mSetAsWallpaper = setAsWallpaper;
+ mReturnData = returnData;
+ mExtraOutput = extraOutput;
+ mOutputFormat = outputFormat;
+ mShowWhenLocked = showWhenLocked;
+ mSpotlightX = spotlightX;
+ mSpotlightY = spotlightY;
+ }
+
+ public CropExtras(CropExtras c) {
+ this(c.mOutputX, c.mOutputY, c.mScaleUp, c.mAspectX, c.mAspectY, c.mSetAsWallpaper,
+ c.mReturnData, c.mExtraOutput, c.mOutputFormat, c.mShowWhenLocked,
+ c.mSpotlightX, c.mSpotlightY);
+ }
+
+ public int getOutputX() {
+ return mOutputX;
+ }
+
+ public int getOutputY() {
+ return mOutputY;
+ }
+
+ public boolean getScaleUp() {
+ return mScaleUp;
+ }
+
+ public int getAspectX() {
+ return mAspectX;
+ }
+
+ public int getAspectY() {
+ return mAspectY;
+ }
+
+ public boolean getSetAsWallpaper() {
+ return mSetAsWallpaper;
+ }
+
+ public boolean getReturnData() {
+ return mReturnData;
+ }
+
+ public Uri getExtraOutput() {
+ return mExtraOutput;
+ }
+
+ public String getOutputFormat() {
+ return mOutputFormat;
+ }
+
+ public boolean getShowWhenLocked() {
+ return mShowWhenLocked;
+ }
+
+ public float getSpotlightX() {
+ return mSpotlightX;
+ }
+
+ public float getSpotlightY() {
+ return mSpotlightY;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropMath.java b/src/com/android/gallery3d/filtershow/crop/CropMath.java
new file mode 100644
index 000000000..02c65310e
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropMath.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2012 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.filtershow.crop;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+
+import java.util.Arrays;
+
+public class CropMath {
+
+ /**
+ * Gets a float array of the 2D coordinates representing a rectangles
+ * corners.
+ * The order of the corners in the float array is:
+ * 0------->1
+ * ^ |
+ * | v
+ * 3<-------2
+ *
+ * @param r the rectangle to get the corners of
+ * @return the float array of corners (8 floats)
+ */
+
+ public static float[] getCornersFromRect(RectF r) {
+ float[] corners = {
+ r.left, r.top,
+ r.right, r.top,
+ r.right, r.bottom,
+ r.left, r.bottom
+ };
+ return corners;
+ }
+
+ /**
+ * Returns true iff point (x, y) is within or on the rectangle's bounds.
+ * RectF's "contains" function treats points on the bottom and right bound
+ * as not being contained.
+ *
+ * @param r the rectangle
+ * @param x the x value of the point
+ * @param y the y value of the point
+ * @return
+ */
+ public static boolean inclusiveContains(RectF r, float x, float y) {
+ return !(x > r.right || x < r.left || y > r.bottom || y < r.top);
+ }
+
+ /**
+ * Takes an array of 2D coordinates representing corners and returns the
+ * smallest rectangle containing those coordinates.
+ *
+ * @param array array of 2D coordinates
+ * @return smallest rectangle containing coordinates
+ */
+ public static RectF trapToRect(float[] array) {
+ RectF r = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
+ Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
+ for (int i = 1; i < array.length; i += 2) {
+ float x = array[i - 1];
+ float y = array[i];
+ r.left = (x < r.left) ? x : r.left;
+ r.top = (y < r.top) ? y : r.top;
+ r.right = (x > r.right) ? x : r.right;
+ r.bottom = (y > r.bottom) ? y : r.bottom;
+ }
+ r.sort();
+ return r;
+ }
+
+ /**
+ * If edge point [x, y] in array [x0, y0, x1, y1, ...] is outside of the
+ * image bound rectangle, clamps it to the edge of the rectangle.
+ *
+ * @param imageBound the rectangle to clamp edge points to.
+ * @param array an array of points to clamp to the rectangle, gets set to
+ * the clamped values.
+ */
+ public static void getEdgePoints(RectF imageBound, float[] array) {
+ if (array.length < 2)
+ return;
+ for (int x = 0; x < array.length; x += 2) {
+ array[x] = GeometryMathUtils.clamp(array[x], imageBound.left, imageBound.right);
+ array[x + 1] = GeometryMathUtils.clamp(array[x + 1], imageBound.top, imageBound.bottom);
+ }
+ }
+
+ /**
+ * Takes a point and the corners of a rectangle and returns the two corners
+ * representing the side of the rectangle closest to the point.
+ *
+ * @param point the point which is being checked
+ * @param corners the corners of the rectangle
+ * @return two corners representing the side of the rectangle
+ */
+ public static float[] closestSide(float[] point, float[] corners) {
+ int len = corners.length;
+ float oldMag = Float.POSITIVE_INFINITY;
+ float[] bestLine = null;
+ for (int i = 0; i < len; i += 2) {
+ float[] line = {
+ corners[i], corners[(i + 1) % len],
+ corners[(i + 2) % len], corners[(i + 3) % len]
+ };
+ float mag = GeometryMathUtils.vectorLength(
+ GeometryMathUtils.shortestVectorFromPointToLine(point, line));
+ if (mag < oldMag) {
+ oldMag = mag;
+ bestLine = line;
+ }
+ }
+ return bestLine;
+ }
+
+ /**
+ * Checks if a given point is within a rotated rectangle.
+ *
+ * @param point 2D point to check
+ * @param bound rectangle to rotate
+ * @param rot angle of rotation about rectangle center
+ * @return true if point is within rotated rectangle
+ */
+ public static boolean pointInRotatedRect(float[] point, RectF bound, float rot) {
+ Matrix m = new Matrix();
+ float[] p = Arrays.copyOf(point, 2);
+ m.setRotate(rot, bound.centerX(), bound.centerY());
+ Matrix m0 = new Matrix();
+ if (!m.invert(m0))
+ return false;
+ m0.mapPoints(p);
+ return inclusiveContains(bound, p[0], p[1]);
+ }
+
+ /**
+ * Checks if a given point is within a rotated rectangle.
+ *
+ * @param point 2D point to check
+ * @param rotatedRect corners of a rotated rectangle
+ * @param center center of the rotated rectangle
+ * @return true if point is within rotated rectangle
+ */
+ public static boolean pointInRotatedRect(float[] point, float[] rotatedRect, float[] center) {
+ RectF unrotated = new RectF();
+ float angle = getUnrotated(rotatedRect, center, unrotated);
+ return pointInRotatedRect(point, unrotated, angle);
+ }
+
+ /**
+ * Resizes rectangle to have a certain aspect ratio (center remains
+ * stationary).
+ *
+ * @param r rectangle to resize
+ * @param w new width aspect
+ * @param h new height aspect
+ */
+ public static void fixAspectRatio(RectF r, float w, float h) {
+ float scale = Math.min(r.width() / w, r.height() / h);
+ float centX = r.centerX();
+ float centY = r.centerY();
+ float hw = scale * w / 2;
+ float hh = scale * h / 2;
+ r.set(centX - hw, centY - hh, centX + hw, centY + hh);
+ }
+
+ /**
+ * Resizes rectangle to have a certain aspect ratio (center remains
+ * stationary) while constraining it to remain within the original rect.
+ *
+ * @param r rectangle to resize
+ * @param w new width aspect
+ * @param h new height aspect
+ */
+ public static void fixAspectRatioContained(RectF r, float w, float h) {
+ float origW = r.width();
+ float origH = r.height();
+ float origA = origW / origH;
+ float a = w / h;
+ float finalW = origW;
+ float finalH = origH;
+ if (origA < a) {
+ finalH = origW / a;
+ r.top = r.centerY() - finalH / 2;
+ r.bottom = r.top + finalH;
+ } else {
+ finalW = origH * a;
+ r.left = r.centerX() - finalW / 2;
+ r.right = r.left + finalW;
+ }
+ }
+
+ /**
+ * Stretches/Scales/Translates photoBounds to match displayBounds, and
+ * and returns an equivalent stretched/scaled/translated cropBounds or null
+ * if the mapping is invalid.
+ * @param cropBounds cropBounds to transform
+ * @param photoBounds original bounds containing crop bounds
+ * @param displayBounds final bounds for crop
+ * @return the stretched/scaled/translated crop bounds that fit within displayBounds
+ */
+ public static RectF getScaledCropBounds(RectF cropBounds, RectF photoBounds,
+ RectF displayBounds) {
+ Matrix m = new Matrix();
+ m.setRectToRect(photoBounds, displayBounds, Matrix.ScaleToFit.FILL);
+ RectF trueCrop = new RectF(cropBounds);
+ if (!m.mapRect(trueCrop)) {
+ return null;
+ }
+ return trueCrop;
+ }
+
+ /**
+ * Returns the size of a bitmap in bytes.
+ * @param bmap bitmap whose size to check
+ * @return bitmap size in bytes
+ */
+ public static int getBitmapSize(Bitmap bmap) {
+ return bmap.getRowBytes() * bmap.getHeight();
+ }
+
+ /**
+ * Constrains rotation to be in [0, 90, 180, 270] rounding down.
+ * @param rotation any rotation value, in degrees
+ * @return integer rotation in [0, 90, 180, 270]
+ */
+ public static int constrainedRotation(float rotation) {
+ int r = (int) ((rotation % 360) / 90);
+ r = (r < 0) ? (r + 4) : r;
+ return r * 90;
+ }
+
+ private static float getUnrotated(float[] rotatedRect, float[] center, RectF unrotated) {
+ float dy = rotatedRect[1] - rotatedRect[3];
+ float dx = rotatedRect[0] - rotatedRect[2];
+ float angle = (float) (Math.atan(dy / dx) * 180 / Math.PI);
+ Matrix m = new Matrix();
+ m.setRotate(-angle, center[0], center[1]);
+ float[] unrotatedRect = new float[rotatedRect.length];
+ m.mapPoints(unrotatedRect, rotatedRect);
+ unrotated.set(trapToRect(unrotatedRect));
+ return angle;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropObject.java b/src/com/android/gallery3d/filtershow/crop/CropObject.java
new file mode 100644
index 000000000..b98ed1bfd
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropObject.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2013 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.filtershow.crop;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+
+public class CropObject {
+ private BoundedRect mBoundedRect;
+ private float mAspectWidth = 1;
+ private float mAspectHeight = 1;
+ private boolean mFixAspectRatio = false;
+ private float mRotation = 0;
+ private float mTouchTolerance = 45;
+ private float mMinSideSize = 20;
+
+ public static final int MOVE_NONE = 0;
+ // Sides
+ public static final int MOVE_LEFT = 1;
+ public static final int MOVE_TOP = 2;
+ public static final int MOVE_RIGHT = 4;
+ public static final int MOVE_BOTTOM = 8;
+ public static final int MOVE_BLOCK = 16;
+
+ // Corners
+ public static final int TOP_LEFT = MOVE_TOP | MOVE_LEFT;
+ public static final int TOP_RIGHT = MOVE_TOP | MOVE_RIGHT;
+ public static final int BOTTOM_RIGHT = MOVE_BOTTOM | MOVE_RIGHT;
+ public static final int BOTTOM_LEFT = MOVE_BOTTOM | MOVE_LEFT;
+
+ private int mMovingEdges = MOVE_NONE;
+
+ public CropObject(Rect outerBound, Rect innerBound, int outerAngle) {
+ mBoundedRect = new BoundedRect(outerAngle % 360, outerBound, innerBound);
+ }
+
+ public CropObject(RectF outerBound, RectF innerBound, int outerAngle) {
+ mBoundedRect = new BoundedRect(outerAngle % 360, outerBound, innerBound);
+ }
+
+ public void resetBoundsTo(RectF inner, RectF outer) {
+ mBoundedRect.resetTo(0, outer, inner);
+ }
+
+ public void getInnerBounds(RectF r) {
+ mBoundedRect.setToInner(r);
+ }
+
+ public void getOuterBounds(RectF r) {
+ mBoundedRect.setToOuter(r);
+ }
+
+ public RectF getInnerBounds() {
+ return mBoundedRect.getInner();
+ }
+
+ public RectF getOuterBounds() {
+ return mBoundedRect.getOuter();
+ }
+
+ public int getSelectState() {
+ return mMovingEdges;
+ }
+
+ public boolean isFixedAspect() {
+ return mFixAspectRatio;
+ }
+
+ public void rotateOuter(int angle) {
+ mRotation = angle % 360;
+ mBoundedRect.setRotation(mRotation);
+ clearSelectState();
+ }
+
+ public boolean setInnerAspectRatio(float width, float height) {
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException("Width and Height must be greater than zero");
+ }
+ RectF inner = mBoundedRect.getInner();
+ CropMath.fixAspectRatioContained(inner, width, height);
+ if (inner.width() < mMinSideSize || inner.height() < mMinSideSize) {
+ return false;
+ }
+ mAspectWidth = width;
+ mAspectHeight = height;
+ mFixAspectRatio = true;
+ mBoundedRect.setInner(inner);
+ clearSelectState();
+ return true;
+ }
+
+ public void setTouchTolerance(float tolerance) {
+ if (tolerance <= 0) {
+ throw new IllegalArgumentException("Tolerance must be greater than zero");
+ }
+ mTouchTolerance = tolerance;
+ }
+
+ public void setMinInnerSideSize(float minSide) {
+ if (minSide <= 0) {
+ throw new IllegalArgumentException("Min dide must be greater than zero");
+ }
+ mMinSideSize = minSide;
+ }
+
+ public void unsetAspectRatio() {
+ mFixAspectRatio = false;
+ clearSelectState();
+ }
+
+ public boolean hasSelectedEdge() {
+ return mMovingEdges != MOVE_NONE;
+ }
+
+ public static boolean checkCorner(int selected) {
+ return selected == TOP_LEFT || selected == TOP_RIGHT || selected == BOTTOM_RIGHT
+ || selected == BOTTOM_LEFT;
+ }
+
+ public static boolean checkEdge(int selected) {
+ return selected == MOVE_LEFT || selected == MOVE_TOP || selected == MOVE_RIGHT
+ || selected == MOVE_BOTTOM;
+ }
+
+ public static boolean checkBlock(int selected) {
+ return selected == MOVE_BLOCK;
+ }
+
+ public static boolean checkValid(int selected) {
+ return selected == MOVE_NONE || checkBlock(selected) || checkEdge(selected)
+ || checkCorner(selected);
+ }
+
+ public void clearSelectState() {
+ mMovingEdges = MOVE_NONE;
+ }
+
+ public int wouldSelectEdge(float x, float y) {
+ int edgeSelected = calculateSelectedEdge(x, y);
+ if (edgeSelected != MOVE_NONE && edgeSelected != MOVE_BLOCK) {
+ return edgeSelected;
+ }
+ return MOVE_NONE;
+ }
+
+ public boolean selectEdge(int edge) {
+ if (!checkValid(edge)) {
+ // temporary
+ throw new IllegalArgumentException("bad edge selected");
+ // return false;
+ }
+ if ((mFixAspectRatio && !checkCorner(edge)) && !checkBlock(edge) && edge != MOVE_NONE) {
+ // temporary
+ throw new IllegalArgumentException("bad corner selected");
+ // return false;
+ }
+ mMovingEdges = edge;
+ return true;
+ }
+
+ public boolean selectEdge(float x, float y) {
+ int edgeSelected = calculateSelectedEdge(x, y);
+ if (mFixAspectRatio) {
+ edgeSelected = fixEdgeToCorner(edgeSelected);
+ }
+ if (edgeSelected == MOVE_NONE) {
+ return false;
+ }
+ return selectEdge(edgeSelected);
+ }
+
+ public boolean moveCurrentSelection(float dX, float dY) {
+ if (mMovingEdges == MOVE_NONE) {
+ return false;
+ }
+ RectF crop = mBoundedRect.getInner();
+
+ float minWidthHeight = mMinSideSize;
+
+ int movingEdges = mMovingEdges;
+ if (movingEdges == MOVE_BLOCK) {
+ mBoundedRect.moveInner(dX, dY);
+ return true;
+ } else {
+ float dx = 0;
+ float dy = 0;
+
+ if ((movingEdges & MOVE_LEFT) != 0) {
+ dx = Math.min(crop.left + dX, crop.right - minWidthHeight) - crop.left;
+ }
+ if ((movingEdges & MOVE_TOP) != 0) {
+ dy = Math.min(crop.top + dY, crop.bottom - minWidthHeight) - crop.top;
+ }
+ if ((movingEdges & MOVE_RIGHT) != 0) {
+ dx = Math.max(crop.right + dX, crop.left + minWidthHeight)
+ - crop.right;
+ }
+ if ((movingEdges & MOVE_BOTTOM) != 0) {
+ dy = Math.max(crop.bottom + dY, crop.top + minWidthHeight)
+ - crop.bottom;
+ }
+
+ if (mFixAspectRatio) {
+ float[] l1 = {
+ crop.left, crop.bottom
+ };
+ float[] l2 = {
+ crop.right, crop.top
+ };
+ if (movingEdges == TOP_LEFT || movingEdges == BOTTOM_RIGHT) {
+ l1[1] = crop.top;
+ l2[1] = crop.bottom;
+ }
+ float[] b = {
+ l1[0] - l2[0], l1[1] - l2[1]
+ };
+ float[] disp = {
+ dx, dy
+ };
+ float[] bUnit = GeometryMathUtils.normalize(b);
+ float sp = GeometryMathUtils.scalarProjection(disp, bUnit);
+ dx = sp * bUnit[0];
+ dy = sp * bUnit[1];
+ RectF newCrop = fixedCornerResize(crop, movingEdges, dx, dy);
+
+ mBoundedRect.fixedAspectResizeInner(newCrop);
+ } else {
+ if ((movingEdges & MOVE_LEFT) != 0) {
+ crop.left += dx;
+ }
+ if ((movingEdges & MOVE_TOP) != 0) {
+ crop.top += dy;
+ }
+ if ((movingEdges & MOVE_RIGHT) != 0) {
+ crop.right += dx;
+ }
+ if ((movingEdges & MOVE_BOTTOM) != 0) {
+ crop.bottom += dy;
+ }
+ mBoundedRect.resizeInner(crop);
+ }
+ }
+ return true;
+ }
+
+ // Helper methods
+
+ private int calculateSelectedEdge(float x, float y) {
+ RectF cropped = mBoundedRect.getInner();
+
+ float left = Math.abs(x - cropped.left);
+ float right = Math.abs(x - cropped.right);
+ float top = Math.abs(y - cropped.top);
+ float bottom = Math.abs(y - cropped.bottom);
+
+ int edgeSelected = MOVE_NONE;
+ // Check left or right.
+ if ((left <= mTouchTolerance) && ((y + mTouchTolerance) >= cropped.top)
+ && ((y - mTouchTolerance) <= cropped.bottom) && (left < right)) {
+ edgeSelected |= MOVE_LEFT;
+ }
+ else if ((right <= mTouchTolerance) && ((y + mTouchTolerance) >= cropped.top)
+ && ((y - mTouchTolerance) <= cropped.bottom)) {
+ edgeSelected |= MOVE_RIGHT;
+ }
+
+ // Check top or bottom.
+ if ((top <= mTouchTolerance) && ((x + mTouchTolerance) >= cropped.left)
+ && ((x - mTouchTolerance) <= cropped.right) && (top < bottom)) {
+ edgeSelected |= MOVE_TOP;
+ }
+ else if ((bottom <= mTouchTolerance) && ((x + mTouchTolerance) >= cropped.left)
+ && ((x - mTouchTolerance) <= cropped.right)) {
+ edgeSelected |= MOVE_BOTTOM;
+ }
+ return edgeSelected;
+ }
+
+ private static RectF fixedCornerResize(RectF r, int moving_corner, float dx, float dy) {
+ RectF newCrop = null;
+ // Fix opposite corner in place and move sides
+ if (moving_corner == BOTTOM_RIGHT) {
+ newCrop = new RectF(r.left, r.top, r.left + r.width() + dx, r.top + r.height()
+ + dy);
+ } else if (moving_corner == BOTTOM_LEFT) {
+ newCrop = new RectF(r.right - r.width() + dx, r.top, r.right, r.top + r.height()
+ + dy);
+ } else if (moving_corner == TOP_LEFT) {
+ newCrop = new RectF(r.right - r.width() + dx, r.bottom - r.height() + dy,
+ r.right, r.bottom);
+ } else if (moving_corner == TOP_RIGHT) {
+ newCrop = new RectF(r.left, r.bottom - r.height() + dy, r.left
+ + r.width() + dx, r.bottom);
+ }
+ return newCrop;
+ }
+
+ private static int fixEdgeToCorner(int moving_edges) {
+ if (moving_edges == MOVE_LEFT) {
+ moving_edges |= MOVE_TOP;
+ }
+ if (moving_edges == MOVE_TOP) {
+ moving_edges |= MOVE_LEFT;
+ }
+ if (moving_edges == MOVE_RIGHT) {
+ moving_edges |= MOVE_BOTTOM;
+ }
+ if (moving_edges == MOVE_BOTTOM) {
+ moving_edges |= MOVE_RIGHT;
+ }
+ return moving_edges;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropView.java b/src/com/android/gallery3d/filtershow/crop/CropView.java
new file mode 100644
index 000000000..bbb7cfd4c
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropView.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2013 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.filtershow.crop;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.DashPathEffect;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.NinePatchDrawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.R;
+
+
+public class CropView extends View {
+ private static final String LOGTAG = "CropView";
+
+ private RectF mImageBounds = new RectF();
+ private RectF mScreenBounds = new RectF();
+ private RectF mScreenImageBounds = new RectF();
+ private RectF mScreenCropBounds = new RectF();
+ private Rect mShadowBounds = new Rect();
+
+ private Bitmap mBitmap;
+ private Paint mPaint = new Paint();
+
+ private NinePatchDrawable mShadow;
+ private CropObject mCropObj = null;
+ private Drawable mCropIndicator;
+ private int mIndicatorSize;
+ private int mRotation = 0;
+ private boolean mMovingBlock = false;
+ private Matrix mDisplayMatrix = null;
+ private Matrix mDisplayMatrixInverse = null;
+ private boolean mDirty = false;
+
+ private float mPrevX = 0;
+ private float mPrevY = 0;
+ private float mSpotX = 0;
+ private float mSpotY = 0;
+ private boolean mDoSpot = false;
+
+ private int mShadowMargin = 15;
+ private int mMargin = 32;
+ private int mOverlayShadowColor = 0xCF000000;
+ private int mOverlayWPShadowColor = 0x5F000000;
+ private int mWPMarkerColor = 0x7FFFFFFF;
+ private int mMinSideSize = 90;
+ private int mTouchTolerance = 40;
+ private float mDashOnLength = 20;
+ private float mDashOffLength = 10;
+
+ private enum Mode {
+ NONE, MOVE
+ }
+
+ private Mode mState = Mode.NONE;
+
+ public CropView(Context context) {
+ super(context);
+ setup(context);
+ }
+
+ public CropView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setup(context);
+ }
+
+ public CropView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setup(context);
+ }
+
+ private void setup(Context context) {
+ Resources rsc = context.getResources();
+ mShadow = (NinePatchDrawable) rsc.getDrawable(R.drawable.geometry_shadow);
+ mCropIndicator = rsc.getDrawable(R.drawable.camera_crop);
+ mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size);
+ mShadowMargin = (int) rsc.getDimension(R.dimen.shadow_margin);
+ mMargin = (int) rsc.getDimension(R.dimen.preview_margin);
+ mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side);
+ mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance);
+ mOverlayShadowColor = (int) rsc.getColor(R.color.crop_shadow_color);
+ mOverlayWPShadowColor = (int) rsc.getColor(R.color.crop_shadow_wp_color);
+ mWPMarkerColor = (int) rsc.getColor(R.color.crop_wp_markers);
+ mDashOnLength = rsc.getDimension(R.dimen.wp_selector_dash_length);
+ mDashOffLength = rsc.getDimension(R.dimen.wp_selector_off_length);
+ }
+
+ public void initialize(Bitmap image, RectF newCropBounds, RectF newPhotoBounds, int rotation) {
+ mBitmap = image;
+ if (mCropObj != null) {
+ RectF crop = mCropObj.getInnerBounds();
+ RectF containing = mCropObj.getOuterBounds();
+ if (crop != newCropBounds || containing != newPhotoBounds
+ || mRotation != rotation) {
+ mRotation = rotation;
+ mCropObj.resetBoundsTo(newCropBounds, newPhotoBounds);
+ clearDisplay();
+ }
+ } else {
+ mRotation = rotation;
+ mCropObj = new CropObject(newPhotoBounds, newCropBounds, 0);
+ clearDisplay();
+ }
+ }
+
+ public RectF getCrop() {
+ return mCropObj.getInnerBounds();
+ }
+
+ public RectF getPhoto() {
+ return mCropObj.getOuterBounds();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+ if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
+ return true;
+ }
+ float[] touchPoint = {
+ x, y
+ };
+ mDisplayMatrixInverse.mapPoints(touchPoint);
+ x = touchPoint[0];
+ y = touchPoint[1];
+ switch (event.getActionMasked()) {
+ case (MotionEvent.ACTION_DOWN):
+ if (mState == Mode.NONE) {
+ if (!mCropObj.selectEdge(x, y)) {
+ mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK);
+ }
+ mPrevX = x;
+ mPrevY = y;
+ mState = Mode.MOVE;
+ }
+ break;
+ case (MotionEvent.ACTION_UP):
+ if (mState == Mode.MOVE) {
+ mCropObj.selectEdge(CropObject.MOVE_NONE);
+ mMovingBlock = false;
+ mPrevX = x;
+ mPrevY = y;
+ mState = Mode.NONE;
+ }
+ break;
+ case (MotionEvent.ACTION_MOVE):
+ if (mState == Mode.MOVE) {
+ float dx = x - mPrevX;
+ float dy = y - mPrevY;
+ mCropObj.moveCurrentSelection(dx, dy);
+ mPrevX = x;
+ mPrevY = y;
+ }
+ break;
+ default:
+ break;
+ }
+ invalidate();
+ return true;
+ }
+
+ private void reset() {
+ Log.w(LOGTAG, "crop reset called");
+ mState = Mode.NONE;
+ mCropObj = null;
+ mRotation = 0;
+ mMovingBlock = false;
+ clearDisplay();
+ }
+
+ private void clearDisplay() {
+ mDisplayMatrix = null;
+ mDisplayMatrixInverse = null;
+ invalidate();
+ }
+
+ protected void configChanged() {
+ mDirty = true;
+ }
+
+ public void applyFreeAspect() {
+ mCropObj.unsetAspectRatio();
+ invalidate();
+ }
+
+ public void applyOriginalAspect() {
+ RectF outer = mCropObj.getOuterBounds();
+ float w = outer.width();
+ float h = outer.height();
+ if (w > 0 && h > 0) {
+ applyAspect(w, h);
+ mCropObj.resetBoundsTo(outer, outer);
+ } else {
+ Log.w(LOGTAG, "failed to set aspect ratio original");
+ }
+ }
+
+ public void applySquareAspect() {
+ applyAspect(1, 1);
+ }
+
+ public void applyAspect(float x, float y) {
+ if (x <= 0 || y <= 0) {
+ throw new IllegalArgumentException("Bad arguments to applyAspect");
+ }
+ // If we are rotated by 90 degrees from horizontal, swap x and y
+ if (((mRotation < 0) ? -mRotation : mRotation) % 180 == 90) {
+ float tmp = x;
+ x = y;
+ y = tmp;
+ }
+ if (!mCropObj.setInnerAspectRatio(x, y)) {
+ Log.w(LOGTAG, "failed to set aspect ratio");
+ }
+ invalidate();
+ }
+
+ public void setWallpaperSpotlight(float spotlightX, float spotlightY) {
+ mSpotX = spotlightX;
+ mSpotY = spotlightY;
+ if (mSpotX > 0 && mSpotY > 0) {
+ mDoSpot = true;
+ }
+ }
+
+ public void unsetWallpaperSpotlight() {
+ mDoSpot = false;
+ }
+
+ /**
+ * Rotates first d bits in integer x to the left some number of times.
+ */
+ private int bitCycleLeft(int x, int times, int d) {
+ int mask = (1 << d) - 1;
+ int mout = x & mask;
+ times %= d;
+ int hi = mout >> (d - times);
+ int low = (mout << times) & mask;
+ int ret = x & ~mask;
+ ret |= low;
+ ret |= hi;
+ return ret;
+ }
+
+ /**
+ * Find the selected edge or corner in screen coordinates.
+ */
+ private int decode(int movingEdges, float rotation) {
+ int rot = CropMath.constrainedRotation(rotation);
+ switch (rot) {
+ case 90:
+ return bitCycleLeft(movingEdges, 1, 4);
+ case 180:
+ return bitCycleLeft(movingEdges, 2, 4);
+ case 270:
+ return bitCycleLeft(movingEdges, 3, 4);
+ default:
+ return movingEdges;
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ if (mBitmap == null) {
+ return;
+ }
+ if (mDirty) {
+ mDirty = false;
+ clearDisplay();
+ }
+
+ mImageBounds = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
+ mScreenBounds = new RectF(0, 0, canvas.getWidth(), canvas.getHeight());
+ mScreenBounds.inset(mMargin, mMargin);
+
+ // If crop object doesn't exist, create it and update it from master
+ // state
+ if (mCropObj == null) {
+ reset();
+ mCropObj = new CropObject(mImageBounds, mImageBounds, 0);
+ }
+
+ // If display matrix doesn't exist, create it and its dependencies
+ if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
+ mDisplayMatrix = new Matrix();
+ mDisplayMatrix.reset();
+ if (!CropDrawingUtils.setImageToScreenMatrix(mDisplayMatrix, mImageBounds, mScreenBounds,
+ mRotation)) {
+ Log.w(LOGTAG, "failed to get screen matrix");
+ mDisplayMatrix = null;
+ return;
+ }
+ mDisplayMatrixInverse = new Matrix();
+ mDisplayMatrixInverse.reset();
+ if (!mDisplayMatrix.invert(mDisplayMatrixInverse)) {
+ Log.w(LOGTAG, "could not invert display matrix");
+ mDisplayMatrixInverse = null;
+ return;
+ }
+ // Scale min side and tolerance by display matrix scale factor
+ mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize));
+ mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance));
+ }
+
+ mScreenImageBounds.set(mImageBounds);
+
+ // Draw background shadow
+ if (mDisplayMatrix.mapRect(mScreenImageBounds)) {
+ int margin = (int) mDisplayMatrix.mapRadius(mShadowMargin);
+ mScreenImageBounds.roundOut(mShadowBounds);
+ mShadowBounds.set(mShadowBounds.left - margin, mShadowBounds.top -
+ margin, mShadowBounds.right + margin, mShadowBounds.bottom + margin);
+ mShadow.setBounds(mShadowBounds);
+ mShadow.draw(canvas);
+ }
+
+ mPaint.setAntiAlias(true);
+ mPaint.setFilterBitmap(true);
+ // Draw actual bitmap
+ canvas.drawBitmap(mBitmap, mDisplayMatrix, mPaint);
+
+ mCropObj.getInnerBounds(mScreenCropBounds);
+
+ if (mDisplayMatrix.mapRect(mScreenCropBounds)) {
+
+ // Draw overlay shadows
+ Paint p = new Paint();
+ p.setColor(mOverlayShadowColor);
+ p.setStyle(Paint.Style.FILL);
+ CropDrawingUtils.drawShadows(canvas, p, mScreenCropBounds, mScreenImageBounds);
+
+ // Draw crop rect and markers
+ CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds);
+ if (!mDoSpot) {
+ CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds);
+ } else {
+ Paint wpPaint = new Paint();
+ wpPaint.setColor(mWPMarkerColor);
+ wpPaint.setStrokeWidth(3);
+ wpPaint.setStyle(Paint.Style.STROKE);
+ wpPaint.setPathEffect(new DashPathEffect(new float[]
+ {mDashOnLength, mDashOnLength + mDashOffLength}, 0));
+ p.setColor(mOverlayWPShadowColor);
+ CropDrawingUtils.drawWallpaperSelectionFrame(canvas, mScreenCropBounds,
+ mSpotX, mSpotY, wpPaint, p);
+ }
+ CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize,
+ mScreenCropBounds, mCropObj.isFixedAspect(), decode(mCropObj.getSelectState(), mRotation));
+ }
+
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java b/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java
new file mode 100644
index 000000000..e18d3104f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/data/FilterStackDBHelper.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2013 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.filtershow.data;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class FilterStackDBHelper extends SQLiteOpenHelper {
+
+ public static final int DATABASE_VERSION = 1;
+ public static final String DATABASE_NAME = "filterstacks.db";
+ private static final String SQL_CREATE_TABLE = "CREATE TABLE ";
+
+ public static interface FilterStack {
+ /** The row uid */
+ public static final String _ID = "_id";
+ /** The table name */
+ public static final String TABLE = "filterstack";
+ /** The stack name */
+ public static final String STACK_ID = "stack_id";
+ /** A serialized stack of filters. */
+ public static final String FILTER_STACK= "stack";
+ }
+
+ private static final String[][] CREATE_FILTER_STACK = {
+ { FilterStack._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" },
+ { FilterStack.STACK_ID, "TEXT" },
+ { FilterStack.FILTER_STACK, "BLOB" },
+ };
+
+ public FilterStackDBHelper(Context context, String name, int version) {
+ super(context, name, null, version);
+ }
+
+ public FilterStackDBHelper(Context context, String name) {
+ this(context, name, DATABASE_VERSION);
+ }
+
+ public FilterStackDBHelper(Context context) {
+ this(context, DATABASE_NAME);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ createTable(db, FilterStack.TABLE, CREATE_FILTER_STACK);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ dropTable(db, FilterStack.TABLE);
+ onCreate(db);
+ }
+
+ protected static void createTable(SQLiteDatabase db, String table, String[][] columns) {
+ StringBuilder create = new StringBuilder(SQL_CREATE_TABLE);
+ create.append(table).append('(');
+ boolean first = true;
+ for (String[] column : columns) {
+ if (!first) {
+ create.append(',');
+ }
+ first = false;
+ for (String val : column) {
+ create.append(val).append(' ');
+ }
+ }
+ create.append(')');
+ db.beginTransaction();
+ try {
+ db.execSQL(create.toString());
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ protected static void dropTable(SQLiteDatabase db, String table) {
+ db.beginTransaction();
+ try {
+ db.execSQL("drop table if exists " + table);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/data/FilterStackSource.java b/src/com/android/gallery3d/filtershow/data/FilterStackSource.java
new file mode 100644
index 000000000..d283771b4
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/data/FilterStackSource.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2013 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.filtershow.data;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.gallery3d.filtershow.data.FilterStackDBHelper.FilterStack;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FilterStackSource {
+ private static final String LOGTAG = "FilterStackSource";
+
+ private SQLiteDatabase database = null;
+ private final FilterStackDBHelper dbHelper;
+
+ public FilterStackSource(Context context) {
+ dbHelper = new FilterStackDBHelper(context);
+ }
+
+ public void open() {
+ try {
+ database = dbHelper.getWritableDatabase();
+ } catch (SQLiteException e) {
+ Log.w(LOGTAG, "could not open database", e);
+ }
+ }
+
+ public void close() {
+ database = null;
+ dbHelper.close();
+ }
+
+ public boolean insertStack(String stackName, byte[] stackBlob) {
+ boolean ret = true;
+ ContentValues val = new ContentValues();
+ val.put(FilterStack.STACK_ID, stackName);
+ val.put(FilterStack.FILTER_STACK, stackBlob);
+ database.beginTransaction();
+ try {
+ ret = (-1 != database.insert(FilterStack.TABLE, null, val));
+ database.setTransactionSuccessful();
+ } finally {
+ database.endTransaction();
+ }
+ return ret;
+ }
+
+ public void updateStackName(int id, String stackName) {
+ ContentValues val = new ContentValues();
+ val.put(FilterStack.STACK_ID, stackName);
+ database.beginTransaction();
+ try {
+ database.update(FilterStack.TABLE, val, FilterStack._ID + " = ?",
+ new String[] { "" + id});
+ database.setTransactionSuccessful();
+ } finally {
+ database.endTransaction();
+ }
+ }
+
+ public boolean removeStack(int id) {
+ boolean ret = true;
+ database.beginTransaction();
+ try {
+ ret = (0 != database.delete(FilterStack.TABLE, FilterStack._ID + " = ?",
+ new String[] { "" + id }));
+ database.setTransactionSuccessful();
+ } finally {
+ database.endTransaction();
+ }
+ return ret;
+ }
+
+ public void removeAllStacks() {
+ database.beginTransaction();
+ try {
+ database.delete(FilterStack.TABLE, null, null);
+ database.setTransactionSuccessful();
+ } finally {
+ database.endTransaction();
+ }
+ }
+
+ public byte[] getStack(String stackName) {
+ byte[] ret = null;
+ Cursor c = null;
+ database.beginTransaction();
+ try {
+ c = database.query(FilterStack.TABLE,
+ new String[] { FilterStack.FILTER_STACK },
+ FilterStack.STACK_ID + " = ?",
+ new String[] { stackName }, null, null, null, null);
+ if (c != null && c.moveToFirst() && !c.isNull(0)) {
+ ret = c.getBlob(0);
+ }
+ database.setTransactionSuccessful();
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ database.endTransaction();
+ }
+ return ret;
+ }
+
+ public ArrayList<FilterUserPresetRepresentation> getAllUserPresets() {
+ ArrayList<FilterUserPresetRepresentation> ret =
+ new ArrayList<FilterUserPresetRepresentation>();
+
+ Cursor c = null;
+ database.beginTransaction();
+ try {
+ c = database.query(FilterStack.TABLE,
+ new String[] { FilterStack._ID,
+ FilterStack.STACK_ID,
+ FilterStack.FILTER_STACK },
+ null, null, null, null, null, null);
+ if (c != null) {
+ boolean loopCheck = c.moveToFirst();
+ while (loopCheck) {
+ int id = c.getInt(0);
+ String name = (c.isNull(1)) ? null : c.getString(1);
+ byte[] b = (c.isNull(2)) ? null : c.getBlob(2);
+ String json = new String(b);
+
+ ImagePreset preset = new ImagePreset();
+ preset.readJsonFromString(json);
+ FilterUserPresetRepresentation representation =
+ new FilterUserPresetRepresentation(name, preset, id);
+ ret.add(representation);
+ loopCheck = c.moveToNext();
+ }
+ }
+ database.setTransactionSuccessful();
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ database.endTransaction();
+ }
+
+ return ret;
+ }
+
+ public List<Pair<String, byte[]>> getAllStacks() {
+ List<Pair<String, byte[]>> ret = new ArrayList<Pair<String, byte[]>>();
+ Cursor c = null;
+ database.beginTransaction();
+ try {
+ c = database.query(FilterStack.TABLE,
+ new String[] { FilterStack.STACK_ID, FilterStack.FILTER_STACK },
+ null, null, null, null, null, null);
+ if (c != null) {
+ boolean loopCheck = c.moveToFirst();
+ while (loopCheck) {
+ String name = (c.isNull(0)) ? null : c.getString(0);
+ byte[] b = (c.isNull(1)) ? null : c.getBlob(1);
+ ret.add(new Pair<String, byte[]>(name, b));
+ loopCheck = c.moveToNext();
+ }
+ }
+ database.setTransactionSuccessful();
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ database.endTransaction();
+ }
+ if (ret.size() <= 0) {
+ return null;
+ }
+ return ret;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/data/UserPresetsManager.java b/src/com/android/gallery3d/filtershow/data/UserPresetsManager.java
new file mode 100644
index 000000000..114cd3ebc
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/data/UserPresetsManager.java
@@ -0,0 +1,149 @@
+package com.android.gallery3d.filtershow.data;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+import java.util.ArrayList;
+
+public class UserPresetsManager implements Handler.Callback {
+
+ private static final String LOGTAG = "UserPresetsManager";
+
+ private FilterShowActivity mActivity;
+ private HandlerThread mHandlerThread = null;
+ private Handler mProcessingHandler = null;
+ private FilterStackSource mUserPresets;
+
+ private static final int LOAD = 1;
+ private static final int LOAD_RESULT = 2;
+ private static final int SAVE = 3;
+ private static final int DELETE = 4;
+ private static final int UPDATE = 5;
+
+ private ArrayList<FilterUserPresetRepresentation> mRepresentations;
+
+ private final Handler mResultHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case LOAD_RESULT:
+ resultLoad(msg);
+ break;
+ }
+ }
+ };
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case LOAD:
+ processLoad();
+ return true;
+ case SAVE:
+ processSave(msg);
+ return true;
+ case DELETE:
+ processDelete(msg);
+ return true;
+ case UPDATE:
+ processUpdate(msg);
+ return true;
+ }
+ return false;
+ }
+
+ public UserPresetsManager(FilterShowActivity context) {
+ mActivity = context;
+ mHandlerThread = new HandlerThread(LOGTAG,
+ android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ mHandlerThread.start();
+ mProcessingHandler = new Handler(mHandlerThread.getLooper(), this);
+ mUserPresets = new FilterStackSource(mActivity);
+ mUserPresets.open();
+ }
+
+ public ArrayList<FilterUserPresetRepresentation> getRepresentations() {
+ return mRepresentations;
+ }
+
+ public void load() {
+ Message msg = mProcessingHandler.obtainMessage(LOAD);
+ mProcessingHandler.sendMessage(msg);
+ }
+
+ public void close() {
+ mUserPresets.close();
+ mHandlerThread.quit();
+ }
+
+ static class SaveOperation {
+ String json;
+ String name;
+ }
+
+ public void save(ImagePreset preset) {
+ Message msg = mProcessingHandler.obtainMessage(SAVE);
+ SaveOperation op = new SaveOperation();
+ op.json = preset.getJsonString(mActivity.getString(R.string.saved));
+ op.name= mActivity.getString(R.string.filtershow_new_preset);
+ msg.obj = op;
+ mProcessingHandler.sendMessage(msg);
+ }
+
+ public void delete(int id) {
+ Message msg = mProcessingHandler.obtainMessage(DELETE);
+ msg.arg1 = id;
+ mProcessingHandler.sendMessage(msg);
+ }
+
+ static class UpdateOperation {
+ int id;
+ String name;
+ }
+
+ public void update(FilterUserPresetRepresentation representation) {
+ Message msg = mProcessingHandler.obtainMessage(UPDATE);
+ UpdateOperation op = new UpdateOperation();
+ op.id = representation.getId();
+ op.name = representation.getName();
+ msg.obj = op;
+ mProcessingHandler.sendMessage(msg);
+ }
+
+ private void processLoad() {
+ ArrayList<FilterUserPresetRepresentation> list = mUserPresets.getAllUserPresets();
+ Message msg = mResultHandler.obtainMessage(LOAD_RESULT);
+ msg.obj = list;
+ mResultHandler.sendMessage(msg);
+ }
+
+ private void resultLoad(Message msg) {
+ mRepresentations =
+ (ArrayList<FilterUserPresetRepresentation>) msg.obj;
+ mActivity.updateUserPresetsFromManager();
+ }
+
+ private void processSave(Message msg) {
+ SaveOperation op = (SaveOperation) msg.obj;
+ mUserPresets.insertStack(op.name, op.json.getBytes());
+ processLoad();
+ }
+
+ private void processDelete(Message msg) {
+ int id = msg.arg1;
+ mUserPresets.removeStack(id);
+ processLoad();
+ }
+
+ private void processUpdate(Message msg) {
+ UpdateOperation op = (UpdateOperation) msg.obj;
+ mUserPresets.updateStackName(op.id, op.name);
+ processLoad();
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/BasicEditor.java b/src/com/android/gallery3d/filtershow/editors/BasicEditor.java
new file mode 100644
index 000000000..af694d811
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/BasicEditor.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2013 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.filtershow.editors;
+
+import android.content.Context;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.controller.FilterView;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.controller.ParameterInteger;
+import com.android.gallery3d.filtershow.filters.FilterBasicRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+
+
+/**
+ * The basic editor that all the one parameter filters
+ */
+public class BasicEditor extends ParametricEditor implements ParameterInteger {
+ public static int ID = R.id.basicEditor;
+ private final String LOGTAG = "BasicEditor";
+
+ public BasicEditor() {
+ super(ID, R.layout.filtershow_default_editor, R.id.basicEditor);
+ }
+
+ protected BasicEditor(int id) {
+ super(id, R.layout.filtershow_default_editor, R.id.basicEditor);
+ }
+
+ protected BasicEditor(int id, int layoutID, int viewID) {
+ super(id, layoutID, viewID);
+ }
+
+ @Override
+ public void reflectCurrentFilter() {
+ super.reflectCurrentFilter();
+ if (getLocalRepresentation() != null && getLocalRepresentation() instanceof FilterBasicRepresentation) {
+ FilterBasicRepresentation interval = (FilterBasicRepresentation) getLocalRepresentation();
+ updateText();
+ }
+ }
+
+ private FilterBasicRepresentation getBasicRepresentation() {
+ FilterRepresentation tmpRep = getLocalRepresentation();
+ if (tmpRep != null && tmpRep instanceof FilterBasicRepresentation) {
+ return (FilterBasicRepresentation) tmpRep;
+
+ }
+ return null;
+ }
+
+ @Override
+ public int getMaximum() {
+ FilterBasicRepresentation rep = getBasicRepresentation();
+ if (rep == null) {
+ return 0;
+ }
+ return rep.getMaximum();
+ }
+
+ @Override
+ public int getMinimum() {
+ FilterBasicRepresentation rep = getBasicRepresentation();
+ if (rep == null) {
+ return 0;
+ }
+ return rep.getMinimum();
+ }
+
+ @Override
+ public int getDefaultValue() {
+ return 0;
+ }
+
+ @Override
+ public int getValue() {
+ FilterBasicRepresentation rep = getBasicRepresentation();
+ if (rep == null) {
+ return 0;
+ }
+ return rep.getValue();
+ }
+
+ @Override
+ public String getValueString() {
+ return null;
+ }
+
+ @Override
+ public void setValue(int value) {
+ FilterBasicRepresentation rep = getBasicRepresentation();
+ if (rep == null) {
+ return;
+ }
+ rep.setValue(value);
+ commitLocalRepresentation();
+ }
+
+ @Override
+ public String getParameterName() {
+ FilterBasicRepresentation rep = getBasicRepresentation();
+ return mContext.getString(rep.getTextId());
+ }
+
+ @Override
+ public String getParameterType() {
+ return sParameterType;
+ }
+
+ @Override
+ public void setController(Control c) {
+ }
+
+ @Override
+ public void setFilterView(FilterView editor) {
+
+ }
+
+ @Override
+ public void copyFrom(Parameter src) {
+
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/Editor.java b/src/com/android/gallery3d/filtershow/editors/Editor.java
new file mode 100644
index 000000000..a9e56e0c1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/Editor.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2012 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.filtershow.editors;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageShow;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Base class for Editors Must contain a mImageShow and a top level view
+ */
+public class Editor implements OnSeekBarChangeListener, SwapButton.SwapButtonListener {
+ protected Context mContext;
+ protected View mView;
+ protected ImageShow mImageShow;
+ protected FrameLayout mFrameLayout;
+ protected SeekBar mSeekBar;
+ Button mEditTitle;
+ protected Button mFilterTitle;
+ protected int mID;
+ private final String LOGTAG = "Editor";
+ protected boolean mChangesGeometry = false;
+ protected FilterRepresentation mLocalRepresentation = null;
+ protected byte mShowParameter = SHOW_VALUE_UNDEFINED;
+ private Button mButton;
+ public static byte SHOW_VALUE_UNDEFINED = -1;
+ public static byte SHOW_VALUE_OFF = 0;
+ public static byte SHOW_VALUE_INT = 1;
+
+ public static void hackFixStrings(Menu menu) {
+ int count = menu.size();
+ for (int i = 0; i < count; i++) {
+ MenuItem item = menu.getItem(i);
+ item.setTitle(item.getTitle().toString().toUpperCase());
+ }
+ }
+
+ public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
+ return effectName.toUpperCase() + " " + parameterValue;
+ }
+
+ protected Editor(int id) {
+ mID = id;
+ }
+
+ public int getID() {
+ return mID;
+ }
+
+ public byte showParameterValue() {
+ return mShowParameter;
+ }
+
+ public boolean showsSeekBar() {
+ return true;
+ }
+
+ public void setUpEditorUI(View actionButton, View editControl,
+ Button editTitle, Button stateButton) {
+ mEditTitle = editTitle;
+ mFilterTitle = stateButton;
+ mButton = editTitle;
+ setMenuIcon(true);
+ setUtilityPanelUI(actionButton, editControl);
+ }
+
+ public boolean showsPopupIndicator() {
+ return true;
+ }
+
+ /**
+ * @param actionButton the would be the area for menu etc
+ * @param editControl this is the black area for sliders etc
+ */
+ public void setUtilityPanelUI(View actionButton, View editControl) {
+
+ AttributeSet aset;
+ Context context = editControl.getContext();
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ LinearLayout lp = (LinearLayout) inflater.inflate(
+ R.layout.filtershow_seekbar, (ViewGroup) editControl, true);
+ mSeekBar = (SeekBar) lp.findViewById(R.id.primarySeekBar);
+ mSeekBar.setOnSeekBarChangeListener(this);
+
+ if (showsSeekBar()) {
+ mSeekBar.setOnSeekBarChangeListener(this);
+ mSeekBar.setVisibility(View.VISIBLE);
+ } else {
+ mSeekBar.setVisibility(View.INVISIBLE);
+ }
+
+ if (mButton != null) {
+ if (showsPopupIndicator()) {
+ mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0,
+ R.drawable.filtershow_menu_marker, 0);
+ } else {
+ mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ }
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar sbar, int progress, boolean arg2) {
+
+ }
+
+ public void setPanel() {
+
+ }
+
+ public void createEditor(Context context,FrameLayout frameLayout) {
+ mContext = context;
+ mFrameLayout = frameLayout;
+ mLocalRepresentation = null;
+ }
+
+ protected void unpack(int viewid, int layoutid) {
+
+ if (mView == null) {
+ mView = mFrameLayout.findViewById(viewid);
+ if (mView == null) {
+ LayoutInflater inflater = (LayoutInflater) mContext.getSystemService
+ (Context.LAYOUT_INFLATER_SERVICE);
+ mView = inflater.inflate(layoutid, mFrameLayout, false);
+ mFrameLayout.addView(mView, mView.getLayoutParams());
+ }
+ }
+ mImageShow = findImageShow(mView);
+ }
+
+ private ImageShow findImageShow(View view) {
+ if (view instanceof ImageShow) {
+ return (ImageShow) view;
+ }
+ if (!(view instanceof ViewGroup)) {
+ return null;
+ }
+ ViewGroup vg = (ViewGroup) view;
+ int n = vg.getChildCount();
+ for (int i = 0; i < n; i++) {
+ View v = vg.getChildAt(i);
+ if (v instanceof ImageShow) {
+ return (ImageShow) v;
+ } else if (v instanceof ViewGroup) {
+ return findImageShow(v);
+ }
+ }
+ return null;
+ }
+
+ public View getTopLevelView() {
+ return mView;
+ }
+
+ public ImageShow getImageShow() {
+ return mImageShow;
+ }
+
+ public void setVisibility(int visible) {
+ mView.setVisibility(visible);
+ }
+
+ public FilterRepresentation getLocalRepresentation() {
+ if (mLocalRepresentation == null) {
+ ImagePreset preset = MasterImage.getImage().getPreset();
+ FilterRepresentation filterRepresentation = MasterImage.getImage().getCurrentFilterRepresentation();
+ mLocalRepresentation = preset.getFilterRepresentationCopyFrom(filterRepresentation);
+ if (mShowParameter == SHOW_VALUE_UNDEFINED && filterRepresentation != null) {
+ boolean show = filterRepresentation.showParameterValue();
+ mShowParameter = show ? SHOW_VALUE_INT : SHOW_VALUE_OFF;
+ }
+
+ }
+ return mLocalRepresentation;
+ }
+
+ /**
+ * Call this to update the preset in MasterImage with the current representation
+ * returned by getLocalRepresentation. This causes the preview bitmap to be
+ * regenerated.
+ */
+ public void commitLocalRepresentation() {
+ commitLocalRepresentation(getLocalRepresentation());
+ }
+
+ /**
+ * Call this to update the preset in MasterImage with a given representation.
+ * This causes the preview bitmap to be regenerated.
+ */
+ public void commitLocalRepresentation(FilterRepresentation rep) {
+ ArrayList<FilterRepresentation> filter = new ArrayList<FilterRepresentation>(1);
+ filter.add(rep);
+ commitLocalRepresentation(filter);
+ }
+
+ /**
+ * Call this to update the preset in MasterImage with a collection of FilterRepresnations.
+ * This causes the preview bitmap to be regenerated.
+ */
+ public void commitLocalRepresentation(Collection<FilterRepresentation> reps) {
+ ImagePreset preset = MasterImage.getImage().getPreset();
+ preset.updateFilterRepresentations(reps);
+ if (mButton != null) {
+ updateText();
+ }
+ if (mChangesGeometry) {
+ // Regenerate both the filtered and the geometry-only bitmaps
+ MasterImage.getImage().updatePresets(true);
+ } else {
+ // Regenerate only the filtered bitmap.
+ MasterImage.getImage().invalidateFiltersOnly();
+ }
+ preset.fillImageStateAdapter(MasterImage.getImage().getState());
+ }
+
+ /**
+ * This is called in response to a click to apply and leave the editor.
+ */
+ public void finalApplyCalled() {
+ commitLocalRepresentation();
+ }
+
+ protected void updateText() {
+ String s = "";
+ if (mLocalRepresentation != null) {
+ s = mContext.getString(mLocalRepresentation.getTextId());
+ }
+ mButton.setText(calculateUserMessage(mContext, s, ""));
+ }
+
+ /**
+ * called after the filter is set and the select is called
+ */
+ public void reflectCurrentFilter() {
+ mLocalRepresentation = null;
+ FilterRepresentation representation = getLocalRepresentation();
+ if (representation != null && mFilterTitle != null && representation.getTextId() != 0) {
+ String text = mContext.getString(representation.getTextId()).toUpperCase();
+ mFilterTitle.setText(text);
+ updateText();
+ }
+ }
+
+ public boolean useUtilityPanel() {
+ return true;
+ }
+
+ public void openUtilityPanel(LinearLayout mAccessoryViewList) {
+ setMenuIcon(false);
+ if (mImageShow != null) {
+ mImageShow.openUtilityPanel(mAccessoryViewList);
+ }
+ }
+
+ protected void setMenuIcon(boolean on) {
+ mEditTitle.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ 0, 0, on ? R.drawable.filtershow_menu_marker : 0, 0);
+ }
+
+ protected void createMenu(int[] strId, View button) {
+ PopupMenu pmenu = new PopupMenu(mContext, button);
+ Menu menu = pmenu.getMenu();
+ for (int i = 0; i < strId.length; i++) {
+ menu.add(Menu.NONE, Menu.FIRST + i, 0, mContext.getString(strId[i]));
+ }
+ setMenuIcon(true);
+
+ }
+
+ public Control[] getControls() {
+ return null;
+ }
+ @Override
+ public void onStartTrackingTouch(SeekBar arg0) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar arg0) {
+
+ }
+
+ @Override
+ public void swapLeft(MenuItem item) {
+
+ }
+
+ @Override
+ public void swapRight(MenuItem item) {
+
+ }
+
+ public void detach() {
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorChanSat.java b/src/com/android/gallery3d/filtershow/editors/EditorChanSat.java
new file mode 100644
index 000000000..7e31f09ae
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorChanSat.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2013 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.filtershow.editors;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.BasicParameterStyle;
+import com.android.gallery3d.filtershow.controller.FilterView;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.filters.FilterChanSatRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequest;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+
+public class EditorChanSat extends ParametricEditor implements OnSeekBarChangeListener, FilterView {
+ public static final int ID = R.id.editorChanSat;
+ private final String LOGTAG = "EditorGrunge";
+ private SwapButton mButton;
+ private final Handler mHandler = new Handler();
+
+ int[] mMenuStrings = {
+ R.string.editor_chan_sat_main,
+ R.string.editor_chan_sat_red,
+ R.string.editor_chan_sat_yellow,
+ R.string.editor_chan_sat_green,
+ R.string.editor_chan_sat_cyan,
+ R.string.editor_chan_sat_blue,
+ R.string.editor_chan_sat_magenta
+ };
+
+ String mCurrentlyEditing = null;
+
+ public EditorChanSat() {
+ super(ID, R.layout.filtershow_default_editor, R.id.basicEditor);
+ }
+
+ @Override
+ public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
+ FilterRepresentation rep = getLocalRepresentation();
+ if (rep == null || !(rep instanceof FilterChanSatRepresentation)) {
+ return "";
+ }
+ FilterChanSatRepresentation csrep = (FilterChanSatRepresentation) rep;
+ int mode = csrep.getParameterMode();
+ String paramString;
+
+ paramString = mContext.getString(mMenuStrings[mode]);
+
+ int val = csrep.getCurrentParameter();
+ return paramString + ((val > 0) ? " +" : " ") + val;
+ }
+
+ @Override
+ public void openUtilityPanel(final LinearLayout accessoryViewList) {
+ mButton = (SwapButton) accessoryViewList.findViewById(R.id.applyEffect);
+ mButton.setText(mContext.getString(R.string.editor_chan_sat_main));
+
+ final PopupMenu popupMenu = new PopupMenu(mImageShow.getActivity(), mButton);
+
+ popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_chan_sat, popupMenu.getMenu());
+
+ popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ selectMenuItem(item);
+ return true;
+ }
+ });
+ mButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ popupMenu.show();
+ }
+ });
+ mButton.setListener(this);
+
+ FilterChanSatRepresentation csrep = getChanSatRep();
+ String menuString = mContext.getString(mMenuStrings[0]);
+ switchToMode(csrep, FilterChanSatRepresentation.MODE_MASTER, menuString);
+
+ }
+
+ public int getParameterIndex(int id) {
+ switch (id) {
+ case R.id.editor_chan_sat_main:
+ return FilterChanSatRepresentation.MODE_MASTER;
+ case R.id.editor_chan_sat_red:
+ return FilterChanSatRepresentation.MODE_RED;
+ case R.id.editor_chan_sat_yellow:
+ return FilterChanSatRepresentation.MODE_YELLOW;
+ case R.id.editor_chan_sat_green:
+ return FilterChanSatRepresentation.MODE_GREEN;
+ case R.id.editor_chan_sat_cyan:
+ return FilterChanSatRepresentation.MODE_CYAN;
+ case R.id.editor_chan_sat_blue:
+ return FilterChanSatRepresentation.MODE_BLUE;
+ case R.id.editor_chan_sat_magenta:
+ return FilterChanSatRepresentation.MODE_MAGENTA;
+ }
+ return -1;
+ }
+
+ @Override
+ public void detach() {
+ mButton.setListener(null);
+ mButton.setOnClickListener(null);
+ }
+
+ private void updateSeekBar(FilterChanSatRepresentation rep) {
+ mControl.updateUI();
+ }
+
+ @Override
+ protected Parameter getParameterToEdit(FilterRepresentation rep) {
+ if (rep instanceof FilterChanSatRepresentation) {
+ FilterChanSatRepresentation csrep = (FilterChanSatRepresentation) rep;
+ Parameter param = csrep.getFilterParameter(csrep.getParameterMode());
+ if (param instanceof BasicParameterStyle) {
+ param.setFilterView(EditorChanSat.this);
+ }
+ return param;
+ }
+ return null;
+ }
+
+ private FilterChanSatRepresentation getChanSatRep() {
+ FilterRepresentation rep = getLocalRepresentation();
+ if (rep != null
+ && rep instanceof FilterChanSatRepresentation) {
+ FilterChanSatRepresentation csrep = (FilterChanSatRepresentation) rep;
+ return csrep;
+ }
+ return null;
+ }
+
+ @Override
+ public void computeIcon(int n, RenderingRequestCaller caller) {
+ FilterChanSatRepresentation rep = getChanSatRep();
+ if (rep == null) return;
+ rep = (FilterChanSatRepresentation) rep.copy();
+ ImagePreset preset = new ImagePreset();
+ preset.addFilter(rep);
+ Bitmap src = MasterImage.getImage().getThumbnailBitmap();
+ RenderingRequest.post(null, src, preset, RenderingRequest.STYLE_ICON_RENDERING,
+ caller);
+ }
+
+ protected void selectMenuItem(MenuItem item) {
+ if (getLocalRepresentation() != null
+ && getLocalRepresentation() instanceof FilterChanSatRepresentation) {
+ FilterChanSatRepresentation csrep =
+ (FilterChanSatRepresentation) getLocalRepresentation();
+
+ switchToMode(csrep, getParameterIndex(item.getItemId()), item.getTitle().toString());
+
+ }
+ }
+
+ protected void switchToMode(FilterChanSatRepresentation csrep, int mode, String title) {
+ csrep.setParameterMode(mode);
+ mCurrentlyEditing = title;
+ mButton.setText(mCurrentlyEditing);
+ {
+ Parameter param = getParameterToEdit(csrep);
+
+ control(param, mEditControl);
+ }
+ updateSeekBar(csrep);
+ mView.invalidate();
+ }
+
+ @Override
+ public void swapLeft(MenuItem item) {
+ super.swapLeft(item);
+ mButton.setTranslationX(0);
+ mButton.animate().translationX(mButton.getWidth()).setDuration(SwapButton.ANIM_DURATION);
+ Runnable updateButton = new Runnable() {
+ @Override
+ public void run() {
+ mButton.animate().cancel();
+ mButton.setTranslationX(0);
+ }
+ };
+ mHandler.postDelayed(updateButton, SwapButton.ANIM_DURATION);
+ selectMenuItem(item);
+ }
+
+ @Override
+ public void swapRight(MenuItem item) {
+ super.swapRight(item);
+ mButton.setTranslationX(0);
+ mButton.animate().translationX(-mButton.getWidth()).setDuration(SwapButton.ANIM_DURATION);
+ Runnable updateButton = new Runnable() {
+ @Override
+ public void run() {
+ mButton.animate().cancel();
+ mButton.setTranslationX(0);
+ }
+ };
+ mHandler.postDelayed(updateButton, SwapButton.ANIM_DURATION);
+ selectMenuItem(item);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorCrop.java b/src/com/android/gallery3d/filtershow/editors/EditorCrop.java
new file mode 100644
index 000000000..511d4ff87
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorCrop.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2013 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.filtershow.editors;
+
+import android.content.Context;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterCropRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageCrop;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorCrop extends Editor implements EditorInfo {
+ public static final String TAG = EditorCrop.class.getSimpleName();
+ public static final int ID = R.id.editorCrop;
+
+ // Holder for an aspect ratio it's string id
+ protected static final class AspectInfo {
+ int mAspectX;
+ int mAspectY;
+ int mStringId;
+ AspectInfo(int stringID, int x, int y) {
+ mStringId = stringID;
+ mAspectX = x;
+ mAspectY = y;
+ }
+ };
+
+ // Mapping from menu id to aspect ratio
+ protected static final SparseArray<AspectInfo> sAspects;
+ static {
+ sAspects = new SparseArray<AspectInfo>();
+ sAspects.put(R.id.crop_menu_1to1, new AspectInfo(R.string.aspect1to1_effect, 1, 1));
+ sAspects.put(R.id.crop_menu_4to3, new AspectInfo(R.string.aspect4to3_effect, 4, 3));
+ sAspects.put(R.id.crop_menu_3to4, new AspectInfo(R.string.aspect3to4_effect, 3, 4));
+ sAspects.put(R.id.crop_menu_5to7, new AspectInfo(R.string.aspect5to7_effect, 5, 7));
+ sAspects.put(R.id.crop_menu_7to5, new AspectInfo(R.string.aspect7to5_effect, 7, 5));
+ sAspects.put(R.id.crop_menu_none, new AspectInfo(R.string.aspectNone_effect, 0, 0));
+ sAspects.put(R.id.crop_menu_original, new AspectInfo(R.string.aspectOriginal_effect, 0, 0));
+ }
+
+ protected ImageCrop mImageCrop;
+ private String mAspectString = "";
+
+ public EditorCrop() {
+ super(ID);
+ mChangesGeometry = true;
+ }
+
+ @Override
+ public void createEditor(Context context, FrameLayout frameLayout) {
+ super.createEditor(context, frameLayout);
+ if (mImageCrop == null) {
+ mImageCrop = new ImageCrop(context);
+ }
+ mView = mImageShow = mImageCrop;
+ mImageCrop.setEditor(this);
+ }
+
+ @Override
+ public void reflectCurrentFilter() {
+ MasterImage master = MasterImage.getImage();
+ master.setCurrentFilterRepresentation(master.getPreset()
+ .getFilterWithSerializationName(FilterCropRepresentation.SERIALIZATION_NAME));
+ super.reflectCurrentFilter();
+ FilterRepresentation rep = getLocalRepresentation();
+ if (rep == null || rep instanceof FilterCropRepresentation) {
+ mImageCrop.setFilterCropRepresentation((FilterCropRepresentation) rep);
+ } else {
+ Log.w(TAG, "Could not reflect current filter, not of type: "
+ + FilterCropRepresentation.class.getSimpleName());
+ }
+ mImageCrop.invalidate();
+ }
+
+ @Override
+ public void finalApplyCalled() {
+ commitLocalRepresentation(mImageCrop.getFinalRepresentation());
+ }
+
+ @Override
+ public void openUtilityPanel(final LinearLayout accessoryViewList) {
+ Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+ view.setText(mContext.getString(R.string.crop));
+ view.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ showPopupMenu(accessoryViewList);
+ }
+ });
+ }
+
+ private void changeCropAspect(int itemId) {
+ AspectInfo info = sAspects.get(itemId);
+ if (info == null) {
+ throw new IllegalArgumentException("Invalid resource ID: " + itemId);
+ }
+ if (itemId == R.id.crop_menu_original) {
+ mImageCrop.applyOriginalAspect();
+ } else if (itemId == R.id.crop_menu_none) {
+ mImageCrop.applyFreeAspect();
+ } else {
+ mImageCrop.applyAspect(info.mAspectX, info.mAspectY);
+ }
+ setAspectString(mContext.getString(info.mStringId));
+ }
+
+ private void showPopupMenu(LinearLayout accessoryViewList) {
+ final Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+ final PopupMenu popupMenu = new PopupMenu(mImageShow.getActivity(), button);
+ popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_crop, popupMenu.getMenu());
+ popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ changeCropAspect(item.getItemId());
+ return true;
+ }
+ });
+ popupMenu.show();
+ }
+
+ @Override
+ public boolean showsSeekBar() {
+ return false;
+ }
+
+ @Override
+ public int getTextId() {
+ return R.string.crop;
+ }
+
+ @Override
+ public int getOverlayId() {
+ return R.drawable.filtershow_button_geometry_crop;
+ }
+
+ @Override
+ public boolean getOverlayOnly() {
+ return true;
+ }
+
+ private void setAspectString(String s) {
+ mAspectString = s;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorCurves.java b/src/com/android/gallery3d/filtershow/editors/EditorCurves.java
new file mode 100644
index 000000000..83fbced79
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorCurves.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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.filtershow.editors;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterCurvesRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageCurves;
+
+public class EditorCurves extends Editor {
+ public static final int ID = R.id.imageCurves;
+ ImageCurves mImageCurves;
+
+ public EditorCurves() {
+ super(ID);
+ }
+
+ @Override
+ public void createEditor(Context context, FrameLayout frameLayout) {
+ super.createEditor(context, frameLayout);
+ mView = mImageShow = mImageCurves = new ImageCurves(context);
+ mImageCurves.setEditor(this);
+ }
+
+ @Override
+ public void reflectCurrentFilter() {
+ super.reflectCurrentFilter();
+ FilterRepresentation rep = getLocalRepresentation();
+ if (rep != null && getLocalRepresentation() instanceof FilterCurvesRepresentation) {
+ FilterCurvesRepresentation drawRep = (FilterCurvesRepresentation) rep;
+ mImageCurves.setFilterDrawRepresentation(drawRep);
+ }
+ }
+
+ @Override
+ public boolean showsSeekBar() {
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorDraw.java b/src/com/android/gallery3d/filtershow/editors/EditorDraw.java
new file mode 100644
index 000000000..4b09051e2
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorDraw.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2013 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.filtershow.editors;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager.LayoutParams;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.SeekBar;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.colorpicker.ColorGridDialog;
+import com.android.gallery3d.filtershow.colorpicker.RGBListener;
+import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.ImageFilterDraw;
+import com.android.gallery3d.filtershow.imageshow.ImageDraw;
+
+public class EditorDraw extends Editor {
+ private static final String LOGTAG = "EditorDraw";
+ public static final int ID = R.id.editorDraw;
+ public ImageDraw mImageDraw;
+
+ public EditorDraw() {
+ super(ID);
+ }
+
+ @Override
+ public void createEditor(Context context, FrameLayout frameLayout) {
+ super.createEditor(context, frameLayout);
+ mView = mImageShow = mImageDraw = new ImageDraw(context);
+ mImageDraw.setEditor(this);
+
+ }
+
+ @Override
+ public void reflectCurrentFilter() {
+ super.reflectCurrentFilter();
+ FilterRepresentation rep = getLocalRepresentation();
+
+ if (rep != null && getLocalRepresentation() instanceof FilterDrawRepresentation) {
+ FilterDrawRepresentation drawRep = (FilterDrawRepresentation) getLocalRepresentation();
+ mImageDraw.setFilterDrawRepresentation(drawRep);
+ }
+ }
+
+ @Override
+ public void openUtilityPanel(final LinearLayout accessoryViewList) {
+ Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+ view.setText(mContext.getString(R.string.draw_style));
+ view.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View arg0) {
+ showPopupMenu(accessoryViewList);
+ }
+ });
+ }
+
+ @Override
+ public boolean showsSeekBar() {
+ return false;
+ }
+
+ private void showPopupMenu(LinearLayout accessoryViewList) {
+ final Button button = (Button) accessoryViewList.findViewById(
+ R.id.applyEffect);
+ if (button == null) {
+ return;
+ }
+ final PopupMenu popupMenu = new PopupMenu(mImageShow.getActivity(), button);
+ popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_draw, popupMenu.getMenu());
+ popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ ImageFilterDraw filter = (ImageFilterDraw) mImageShow.getCurrentFilter();
+ if (item.getItemId() == R.id.draw_menu_color) {
+ showColorGrid(item);
+ } else if (item.getItemId() == R.id.draw_menu_size) {
+ showSizeDialog(item);
+ } else if (item.getItemId() == R.id.draw_menu_style_brush_marker) {
+ ImageDraw idraw = (ImageDraw) mImageShow;
+ idraw.setStyle(ImageFilterDraw.BRUSH_STYLE_MARKER);
+ } else if (item.getItemId() == R.id.draw_menu_style_brush_spatter) {
+ ImageDraw idraw = (ImageDraw) mImageShow;
+ idraw.setStyle(ImageFilterDraw.BRUSH_STYLE_SPATTER);
+ } else if (item.getItemId() == R.id.draw_menu_style_line) {
+ ImageDraw idraw = (ImageDraw) mImageShow;
+ idraw.setStyle(ImageFilterDraw.SIMPLE_STYLE);
+ } else if (item.getItemId() == R.id.draw_menu_clear) {
+ ImageDraw idraw = (ImageDraw) mImageShow;
+ idraw.resetParameter();
+ commitLocalRepresentation();
+ }
+ mView.invalidate();
+ return true;
+ }
+ });
+ popupMenu.show();
+ }
+
+ public void showSizeDialog(final MenuItem item) {
+ FilterShowActivity ctx = mImageShow.getActivity();
+ final Dialog dialog = new Dialog(ctx);
+ dialog.setTitle(R.string.draw_size_title);
+ dialog.setContentView(R.layout.filtershow_draw_size);
+ final SeekBar bar = (SeekBar) dialog.findViewById(R.id.sizeSeekBar);
+ ImageDraw idraw = (ImageDraw) mImageShow;
+ bar.setProgress(idraw.getSize());
+ Button button = (Button) dialog.findViewById(R.id.sizeAcceptButton);
+ button.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View arg0) {
+ int p = bar.getProgress();
+ ImageDraw idraw = (ImageDraw) mImageShow;
+ idraw.setSize(p + 1);
+ dialog.dismiss();
+ }
+ });
+ dialog.show();
+ }
+
+ public void showColorGrid(final MenuItem item) {
+ RGBListener cl = new RGBListener() {
+ @Override
+ public void setColor(int rgb) {
+ ImageDraw idraw = (ImageDraw) mImageShow;
+ idraw.setColor(rgb);
+ }
+ };
+ ColorGridDialog cpd = new ColorGridDialog(mImageShow.getActivity(), cl);
+ cpd.show();
+ LayoutParams params = cpd.getWindow().getAttributes();
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorGrad.java b/src/com/android/gallery3d/filtershow/editors/EditorGrad.java
new file mode 100644
index 000000000..f427ccbd8
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorGrad.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2013 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.filtershow.editors;
+
+import android.content.Context;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.ToggleButton;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.controller.FilterView;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.controller.ParameterActionAndInt;
+import com.android.gallery3d.filtershow.filters.FilterGradRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageGrad;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorGrad extends ParametricEditor
+ implements OnSeekBarChangeListener, ParameterActionAndInt {
+ private static final String LOGTAG = "EditorGrad";
+ public static final int ID = R.id.editorGrad;
+ PopupMenu mPopupMenu;
+ ToggleButton mAddModeButton;
+ String mEffectName = "";
+ private static final int MODE_BRIGHTNESS = FilterGradRepresentation.PARAM_BRIGHTNESS;
+ private static final int MODE_SATURATION = FilterGradRepresentation.PARAM_SATURATION;
+ private static final int MODE_CONTRAST = FilterGradRepresentation.PARAM_CONTRAST;
+ private static final int ADD_ICON = R.drawable.ic_grad_add;
+ private static final int DEL_ICON = R.drawable.ic_grad_del;
+ private int mSliderMode = MODE_BRIGHTNESS;
+ ImageGrad mImageGrad;
+
+ public EditorGrad() {
+ super(ID, R.layout.filtershow_grad_editor, R.id.gradEditor);
+ }
+
+ @Override
+ public void createEditor(Context context, FrameLayout frameLayout) {
+ super.createEditor(context, frameLayout);
+ mImageGrad = (ImageGrad) mImageShow;
+ mImageGrad.setEditor(this);
+
+ }
+
+ public void clearAddMode() {
+ mAddModeButton.setChecked(false);
+ FilterRepresentation tmpRep = getLocalRepresentation();
+ if (tmpRep instanceof FilterGradRepresentation) {
+ updateMenuItems((FilterGradRepresentation) tmpRep);
+ }
+ }
+
+ @Override
+ public void reflectCurrentFilter() {
+ super.reflectCurrentFilter();
+ FilterRepresentation tmpRep = getLocalRepresentation();
+ if (tmpRep instanceof FilterGradRepresentation) {
+ FilterGradRepresentation rep = (FilterGradRepresentation) tmpRep;
+ boolean f = rep.showParameterValue();
+
+ mImageGrad.setRepresentation(rep);
+ }
+ }
+
+ public void updateSeekBar(FilterGradRepresentation rep) {
+ mControl.updateUI();
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar sbar, int progress, boolean arg2) {
+ FilterRepresentation tmpRep = getLocalRepresentation();
+ if (tmpRep instanceof FilterGradRepresentation) {
+ FilterGradRepresentation rep = (FilterGradRepresentation) tmpRep;
+ int min = rep.getParameterMin(mSliderMode);
+ int value = progress + min;
+ rep.setParameter(mSliderMode, value);
+ mView.invalidate();
+ commitLocalRepresentation();
+ }
+ }
+
+ @Override
+ public void openUtilityPanel(final LinearLayout accessoryViewList) {
+ Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+ view.setText(mContext.getString(R.string.editor_grad_brightness));
+ view.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ showPopupMenu(accessoryViewList);
+ }
+ });
+
+ setUpPopupMenu(view);
+ setEffectName();
+ }
+
+ private void updateMenuItems(FilterGradRepresentation rep) {
+ int n = rep.getNumberOfBands();
+ }
+
+ public void setEffectName() {
+ if (mPopupMenu != null) {
+ MenuItem item = mPopupMenu.getMenu().findItem(R.id.editor_grad_brightness);
+ mEffectName = item.getTitle().toString();
+ }
+ }
+
+ private void showPopupMenu(LinearLayout accessoryViewList) {
+ Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+ if (button == null) {
+ return;
+ }
+
+ if (mPopupMenu == null) {
+ setUpPopupMenu(button);
+ }
+ mPopupMenu.show();
+ }
+
+ private void setUpPopupMenu(Button button) {
+ mPopupMenu = new PopupMenu(mImageShow.getActivity(), button);
+ mPopupMenu.getMenuInflater()
+ .inflate(R.menu.filtershow_menu_grad, mPopupMenu.getMenu());
+ FilterGradRepresentation rep = (FilterGradRepresentation) getLocalRepresentation();
+ if (rep == null) {
+ return;
+ }
+ updateMenuItems(rep);
+ hackFixStrings(mPopupMenu.getMenu());
+ setEffectName();
+ updateText();
+
+ mPopupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ FilterRepresentation tmpRep = getLocalRepresentation();
+
+ if (tmpRep instanceof FilterGradRepresentation) {
+ FilterGradRepresentation rep = (FilterGradRepresentation) tmpRep;
+ int cmdID = item.getItemId();
+ switch (cmdID) {
+ case R.id.editor_grad_brightness:
+ mSliderMode = MODE_BRIGHTNESS;
+ mEffectName = item.getTitle().toString();
+ break;
+ case R.id.editor_grad_contrast:
+ mSliderMode = MODE_CONTRAST;
+ mEffectName = item.getTitle().toString();
+ break;
+ case R.id.editor_grad_saturation:
+ mSliderMode = MODE_SATURATION;
+ mEffectName = item.getTitle().toString();
+ break;
+ }
+ updateMenuItems(rep);
+ updateSeekBar(rep);
+
+ commitLocalRepresentation();
+ mView.invalidate();
+ }
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
+ FilterGradRepresentation rep = getGradRepresentation();
+ if (rep == null) {
+ return mEffectName;
+ }
+ int val = rep.getParameter(mSliderMode);
+ return mEffectName.toUpperCase() + ((val > 0) ? " +" : " ") + val;
+ }
+
+ private FilterGradRepresentation getGradRepresentation() {
+ FilterRepresentation tmpRep = getLocalRepresentation();
+ if (tmpRep instanceof FilterGradRepresentation) {
+ return (FilterGradRepresentation) tmpRep;
+ }
+ return null;
+ }
+
+ @Override
+ public int getMaximum() {
+ FilterGradRepresentation rep = getGradRepresentation();
+ if (rep == null) {
+ return 0;
+ }
+ return rep.getParameterMax(mSliderMode);
+ }
+
+ @Override
+ public int getMinimum() {
+ FilterGradRepresentation rep = getGradRepresentation();
+ if (rep == null) {
+ return 0;
+ }
+ return rep.getParameterMin(mSliderMode);
+ }
+
+ @Override
+ public int getDefaultValue() {
+ return 0;
+ }
+
+ @Override
+ public int getValue() {
+ FilterGradRepresentation rep = getGradRepresentation();
+ if (rep == null) {
+ return 0;
+ }
+ return rep.getParameter(mSliderMode);
+ }
+
+ @Override
+ public String getValueString() {
+ return null;
+ }
+
+ @Override
+ public void setValue(int value) {
+ FilterGradRepresentation rep = getGradRepresentation();
+ if (rep == null) {
+ return;
+ }
+ rep.setParameter(mSliderMode, value);
+ }
+
+ @Override
+ public String getParameterName() {
+ return mEffectName;
+ }
+
+ @Override
+ public String getParameterType() {
+ return sParameterType;
+ }
+
+ @Override
+ public void setController(Control c) {
+
+ }
+
+ @Override
+ public void fireLeftAction() {
+ FilterGradRepresentation rep = getGradRepresentation();
+ if (rep == null) {
+ return;
+ }
+ rep.addBand(MasterImage.getImage().getOriginalBounds());
+ updateMenuItems(rep);
+ updateSeekBar(rep);
+
+ commitLocalRepresentation();
+ mView.invalidate();
+ }
+
+ @Override
+ public int getLeftIcon() {
+ return ADD_ICON;
+ }
+
+ @Override
+ public void fireRightAction() {
+ FilterGradRepresentation rep = getGradRepresentation();
+ if (rep == null) {
+ return;
+ }
+ rep.deleteCurrentBand();
+
+ updateMenuItems(rep);
+ updateSeekBar(rep);
+ commitLocalRepresentation();
+ mView.invalidate();
+ }
+
+ @Override
+ public int getRightIcon() {
+ return DEL_ICON;
+ }
+
+ @Override
+ public void setFilterView(FilterView editor) {
+
+ }
+
+ @Override
+ public void copyFrom(Parameter src) {
+
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorInfo.java b/src/com/android/gallery3d/filtershow/editors/EditorInfo.java
new file mode 100644
index 000000000..75afe49c2
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorInfo.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2013 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.filtershow.editors;
+
+public interface EditorInfo {
+ public int getTextId();
+ public int getOverlayId();
+ public boolean getOverlayOnly();
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorMirror.java b/src/com/android/gallery3d/filtershow/editors/EditorMirror.java
new file mode 100644
index 000000000..d6d9ee75d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorMirror.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2013 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.filtershow.editors;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageMirror;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorMirror extends Editor implements EditorInfo {
+ public static final String TAG = EditorMirror.class.getSimpleName();
+ public static final int ID = R.id.editorFlip;
+ ImageMirror mImageMirror;
+
+ public EditorMirror() {
+ super(ID);
+ mChangesGeometry = true;
+ }
+
+ @Override
+ public void createEditor(Context context, FrameLayout frameLayout) {
+ super.createEditor(context, frameLayout);
+ if (mImageMirror == null) {
+ mImageMirror = new ImageMirror(context);
+ }
+ mView = mImageShow = mImageMirror;
+ mImageMirror.setEditor(this);
+ }
+
+ @Override
+ public void reflectCurrentFilter() {
+ MasterImage master = MasterImage.getImage();
+ master.setCurrentFilterRepresentation(master.getPreset()
+ .getFilterWithSerializationName(FilterMirrorRepresentation.SERIALIZATION_NAME));
+ super.reflectCurrentFilter();
+ FilterRepresentation rep = getLocalRepresentation();
+ if (rep == null || rep instanceof FilterMirrorRepresentation) {
+ mImageMirror.setFilterMirrorRepresentation((FilterMirrorRepresentation) rep);
+ } else {
+ Log.w(TAG, "Could not reflect current filter, not of type: "
+ + FilterMirrorRepresentation.class.getSimpleName());
+ }
+ mImageMirror.invalidate();
+ }
+
+ @Override
+ public void openUtilityPanel(final LinearLayout accessoryViewList) {
+ final Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+ button.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ mImageMirror.flip();
+ }
+ });
+ }
+
+ @Override
+ public void finalApplyCalled() {
+ commitLocalRepresentation(mImageMirror.getFinalRepresentation());
+ }
+
+ @Override
+ public int getTextId() {
+ return R.string.mirror;
+ }
+
+ @Override
+ public int getOverlayId() {
+ return R.drawable.filtershow_button_geometry_flip;
+ }
+
+ @Override
+ public boolean getOverlayOnly() {
+ return true;
+ }
+
+ @Override
+ public boolean showsSeekBar() {
+ return false;
+ }
+
+ @Override
+ public boolean showsPopupIndicator() {
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorPanel.java b/src/com/android/gallery3d/filtershow/editors/EditorPanel.java
new file mode 100644
index 000000000..bc4ca6ab6
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorPanel.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2013 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.filtershow.editors;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.history.HistoryManager;
+import com.android.gallery3d.filtershow.category.MainPanel;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.state.StatePanel;
+
+public class EditorPanel extends Fragment {
+
+ private static final String LOGTAG = "EditorPanel";
+
+ private LinearLayout mMainView;
+ private Editor mEditor;
+ private int mEditorID;
+
+ public void setEditor(int editor) {
+ mEditorID = editor;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ FilterShowActivity filterShowActivity = (FilterShowActivity) activity;
+ mEditor = filterShowActivity.getEditor(mEditorID);
+ }
+
+ public void cancelCurrentFilter() {
+ MasterImage masterImage = MasterImage.getImage();
+ HistoryManager adapter = masterImage.getHistory();
+
+ int position = adapter.undo();
+ masterImage.onHistoryItemClick(position);
+ ((FilterShowActivity)getActivity()).invalidateViews();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ FilterShowActivity activity = (FilterShowActivity) getActivity();
+ if (mMainView != null) {
+ if (mMainView.getParent() != null) {
+ ViewGroup parent = (ViewGroup) mMainView.getParent();
+ parent.removeView(mMainView);
+ }
+ showImageStatePanel(activity.isShowingImageStatePanel());
+ return mMainView;
+ }
+ mMainView = (LinearLayout) inflater.inflate(R.layout.filtershow_editor_panel, null);
+
+ View actionControl = mMainView.findViewById(R.id.panelAccessoryViewList);
+ View editControl = mMainView.findViewById(R.id.controlArea);
+ ImageButton cancelButton = (ImageButton) mMainView.findViewById(R.id.cancelFilter);
+ ImageButton applyButton = (ImageButton) mMainView.findViewById(R.id.applyFilter);
+ Button editTitle = (Button) mMainView.findViewById(R.id.applyEffect);
+ cancelButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ cancelCurrentFilter();
+ FilterShowActivity activity = (FilterShowActivity) getActivity();
+ activity.backToMain();
+ }
+ });
+
+ Button toggleState = (Button) mMainView.findViewById(R.id.toggle_state);
+ mEditor = activity.getEditor(mEditorID);
+ if (mEditor != null) {
+ mEditor.setUpEditorUI(actionControl, editControl, editTitle, toggleState);
+ mEditor.reflectCurrentFilter();
+ if (mEditor.useUtilityPanel()) {
+ mEditor.openUtilityPanel((LinearLayout) actionControl);
+ }
+ }
+ applyButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ FilterShowActivity activity = (FilterShowActivity) getActivity();
+ mEditor.finalApplyCalled();
+ activity.backToMain();
+ }
+ });
+
+ showImageStatePanel(activity.isShowingImageStatePanel());
+ return mMainView;
+ }
+
+ @Override
+ public void onDetach() {
+ if (mEditor != null) {
+ mEditor.detach();
+ }
+ super.onDetach();
+ }
+
+ public void showImageStatePanel(boolean show) {
+ if (mMainView.findViewById(R.id.state_panel_container) == null) {
+ return;
+ }
+ FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
+ Fragment panel = getActivity().getSupportFragmentManager().findFragmentByTag(
+ MainPanel.FRAGMENT_TAG);
+ if (panel == null || panel instanceof MainPanel) {
+ transaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
+ }
+ if (show) {
+ StatePanel statePanel = new StatePanel();
+ transaction.replace(R.id.state_panel_container, statePanel, StatePanel.FRAGMENT_TAG);
+ } else {
+ Fragment statePanel = getChildFragmentManager().findFragmentByTag(StatePanel.FRAGMENT_TAG);
+ if (statePanel != null) {
+ transaction.remove(statePanel);
+ }
+ }
+ transaction.commit();
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorRedEye.java b/src/com/android/gallery3d/filtershow/editors/EditorRedEye.java
new file mode 100644
index 000000000..b0e88dd44
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorRedEye.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2012 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.filtershow.editors;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterRedEyeRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageRedEye;
+
+/**
+ * The editor with no slider for filters without UI
+ */
+public class EditorRedEye extends Editor {
+ public static int ID = R.id.editorRedEye;
+ private final String LOGTAG = "EditorRedEye";
+ ImageRedEye mImageRedEyes;
+
+ public EditorRedEye() {
+ super(ID);
+ }
+
+ protected EditorRedEye(int id) {
+ super(id);
+ }
+
+ @Override
+ public void createEditor(Context context, FrameLayout frameLayout) {
+ super.createEditor(context, frameLayout);
+ mView = mImageShow = mImageRedEyes= new ImageRedEye(context);
+ mImageRedEyes.setEditor(this);
+ }
+
+ @Override
+ public void reflectCurrentFilter() {
+ super.reflectCurrentFilter();
+ FilterRepresentation rep = getLocalRepresentation();
+ if (rep != null && getLocalRepresentation() instanceof FilterRedEyeRepresentation) {
+ FilterRedEyeRepresentation redEyeRep = (FilterRedEyeRepresentation) rep;
+
+ mImageRedEyes.setRepresentation(redEyeRep);
+ }
+ }
+
+ @Override
+ public boolean showsSeekBar() {
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorRotate.java b/src/com/android/gallery3d/filtershow/editors/EditorRotate.java
new file mode 100644
index 000000000..9452bf0c0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorRotate.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2013 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.filtershow.editors;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageRotate;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorRotate extends Editor implements EditorInfo {
+ public static final String TAG = EditorRotate.class.getSimpleName();
+ public static final int ID = R.id.editorRotate;
+ ImageRotate mImageRotate;
+
+ public EditorRotate() {
+ super(ID);
+ mChangesGeometry = true;
+ }
+
+ @Override
+ public void createEditor(Context context, FrameLayout frameLayout) {
+ super.createEditor(context, frameLayout);
+ if (mImageRotate == null) {
+ mImageRotate = new ImageRotate(context);
+ }
+ mView = mImageShow = mImageRotate;
+ mImageRotate.setEditor(this);
+ }
+
+ @Override
+ public void reflectCurrentFilter() {
+ MasterImage master = MasterImage.getImage();
+ master.setCurrentFilterRepresentation(master.getPreset()
+ .getFilterWithSerializationName(FilterRotateRepresentation.SERIALIZATION_NAME));
+ super.reflectCurrentFilter();
+ FilterRepresentation rep = getLocalRepresentation();
+ if (rep == null || rep instanceof FilterRotateRepresentation) {
+ mImageRotate.setFilterRotateRepresentation((FilterRotateRepresentation) rep);
+ } else {
+ Log.w(TAG, "Could not reflect current filter, not of type: "
+ + FilterRotateRepresentation.class.getSimpleName());
+ }
+ mImageRotate.invalidate();
+ }
+
+ @Override
+ public void openUtilityPanel(final LinearLayout accessoryViewList) {
+ final Button button = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+ button.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ mImageRotate.rotate();
+ String displayVal = mContext.getString(getTextId()) + " "
+ + mImageRotate.getLocalValue();
+ button.setText(displayVal);
+ }
+ });
+ }
+
+ @Override
+ public void finalApplyCalled() {
+ commitLocalRepresentation(mImageRotate.getFinalRepresentation());
+ }
+
+ @Override
+ public int getTextId() {
+ return R.string.rotate;
+ }
+
+ @Override
+ public int getOverlayId() {
+ return R.drawable.filtershow_button_geometry_rotate;
+ }
+
+ @Override
+ public boolean getOverlayOnly() {
+ return true;
+ }
+
+ @Override
+ public boolean showsSeekBar() {
+ return false;
+ }
+
+ @Override
+ public boolean showsPopupIndicator() {
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorStraighten.java b/src/com/android/gallery3d/filtershow/editors/EditorStraighten.java
new file mode 100644
index 000000000..ff84ba8f9
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorStraighten.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2013 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.filtershow.editors;
+
+import android.content.Context;
+import android.util.Log;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageStraighten;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class EditorStraighten extends Editor implements EditorInfo {
+ public static final String TAG = EditorStraighten.class.getSimpleName();
+ public static final int ID = R.id.editorStraighten;
+ ImageStraighten mImageStraighten;
+
+ public EditorStraighten() {
+ super(ID);
+ mShowParameter = SHOW_VALUE_INT;
+ mChangesGeometry = true;
+ }
+
+ @Override
+ public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
+ String apply = context.getString(R.string.apply_effect);
+ apply += " " + effectName;
+ return apply.toUpperCase();
+ }
+
+ @Override
+ public void createEditor(Context context, FrameLayout frameLayout) {
+ super.createEditor(context, frameLayout);
+ if (mImageStraighten == null) {
+ mImageStraighten = new ImageStraighten(context);
+ }
+ mView = mImageShow = mImageStraighten;
+ mImageStraighten.setEditor(this);
+ }
+
+ @Override
+ public void reflectCurrentFilter() {
+ MasterImage master = MasterImage.getImage();
+ master.setCurrentFilterRepresentation(master.getPreset().getFilterWithSerializationName(
+ FilterStraightenRepresentation.SERIALIZATION_NAME));
+ super.reflectCurrentFilter();
+ FilterRepresentation rep = getLocalRepresentation();
+ if (rep == null || rep instanceof FilterStraightenRepresentation) {
+ mImageStraighten
+ .setFilterStraightenRepresentation((FilterStraightenRepresentation) rep);
+ } else {
+ Log.w(TAG, "Could not reflect current filter, not of type: "
+ + FilterStraightenRepresentation.class.getSimpleName());
+ }
+ mImageStraighten.invalidate();
+ }
+
+ @Override
+ public void finalApplyCalled() {
+ commitLocalRepresentation(mImageStraighten.getFinalRepresentation());
+ }
+
+ @Override
+ public int getTextId() {
+ return R.string.straighten;
+ }
+
+ @Override
+ public int getOverlayId() {
+ return R.drawable.filtershow_button_geometry_straighten;
+ }
+
+ @Override
+ public boolean getOverlayOnly() {
+ return true;
+ }
+
+ @Override
+ public boolean showsSeekBar() {
+ return false;
+ }
+
+ @Override
+ public boolean showsPopupIndicator() {
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java b/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java
new file mode 100644
index 000000000..9376fbef0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorTinyPlanet.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.filtershow.editors;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterTinyPlanetRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageTinyPlanet;
+
+public class EditorTinyPlanet extends BasicEditor {
+ public static final int ID = R.id.tinyPlanetEditor;
+ private static final String LOGTAG = "EditorTinyPlanet";
+ ImageTinyPlanet mImageTinyPlanet;
+
+ public EditorTinyPlanet() {
+ super(ID, R.layout.filtershow_tiny_planet_editor, R.id.imageTinyPlanet);
+ }
+
+ @Override
+ public void createEditor(Context context, FrameLayout frameLayout) {
+ super.createEditor(context, frameLayout);
+ mImageTinyPlanet = (ImageTinyPlanet) mImageShow;
+ mImageTinyPlanet.setEditor(this);
+ }
+
+ @Override
+ public void reflectCurrentFilter() {
+ super.reflectCurrentFilter();
+ FilterRepresentation rep = getLocalRepresentation();
+ if (rep != null && rep instanceof FilterTinyPlanetRepresentation) {
+ FilterTinyPlanetRepresentation drawRep = (FilterTinyPlanetRepresentation) rep;
+ mImageTinyPlanet.setRepresentation(drawRep);
+ }
+ }
+
+ public void updateUI() {
+ if (mControl != null) {
+ mControl.updateUI();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorVignette.java b/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
new file mode 100644
index 000000000..7127b2188
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorVignette.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2012 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.filtershow.editors;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterVignetteRepresentation;
+import com.android.gallery3d.filtershow.imageshow.ImageVignette;
+
+public class EditorVignette extends ParametricEditor {
+ public static final int ID = R.id.vignetteEditor;
+ private static final String LOGTAG = "EditorVignettePlanet";
+ ImageVignette mImageVignette;
+
+ public EditorVignette() {
+ super(ID, R.layout.filtershow_vignette_editor, R.id.imageVignette);
+ }
+
+ @Override
+ public void createEditor(Context context, FrameLayout frameLayout) {
+ super.createEditor(context, frameLayout);
+ mImageVignette = (ImageVignette) mImageShow;
+ mImageVignette.setEditor(this);
+ }
+
+ @Override
+ public void reflectCurrentFilter() {
+ super.reflectCurrentFilter();
+
+ FilterRepresentation rep = getLocalRepresentation();
+ if (rep != null && getLocalRepresentation() instanceof FilterVignetteRepresentation) {
+ FilterVignetteRepresentation drawRep = (FilterVignetteRepresentation) rep;
+ mImageVignette.setRepresentation(drawRep);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorZoom.java b/src/com/android/gallery3d/filtershow/editors/EditorZoom.java
new file mode 100644
index 000000000..ea8e3d140
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/EditorZoom.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2012 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.filtershow.editors;
+
+import com.android.gallery3d.R;
+
+public class EditorZoom extends BasicEditor {
+ public static final int ID = R.id.imageZoom;
+
+ public EditorZoom() {
+ super(ID, R.layout.filtershow_zoom_editor,R.id.imageZoom);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/ImageOnlyEditor.java b/src/com/android/gallery3d/filtershow/editors/ImageOnlyEditor.java
new file mode 100644
index 000000000..d4e66edf8
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/ImageOnlyEditor.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 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.filtershow.editors;
+
+import android.content.Context;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.imageshow.ImageShow;
+
+/**
+ * The editor with no slider for filters without UI
+ */
+public class ImageOnlyEditor extends Editor {
+ public final static int ID = R.id.imageOnlyEditor;
+ private final String LOGTAG = "ImageOnlyEditor";
+
+ public ImageOnlyEditor() {
+ super(ID);
+ }
+
+ protected ImageOnlyEditor(int id) {
+ super(id);
+ }
+
+ public boolean useUtilityPanel() {
+ return false;
+ }
+
+ @Override
+ public void createEditor(Context context, FrameLayout frameLayout) {
+ super.createEditor(context, frameLayout);
+ mView = mImageShow = new ImageShow(context);
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/ParametricEditor.java b/src/com/android/gallery3d/filtershow/editors/ParametricEditor.java
new file mode 100644
index 000000000..9ec858ca5
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/ParametricEditor.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2013 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.filtershow.editors;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.ActionSlider;
+import com.android.gallery3d.filtershow.controller.BasicSlider;
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.controller.ParameterActionAndInt;
+import com.android.gallery3d.filtershow.controller.ParameterInteger;
+import com.android.gallery3d.filtershow.controller.ParameterStyles;
+import com.android.gallery3d.filtershow.controller.StyleChooser;
+import com.android.gallery3d.filtershow.controller.TitledSlider;
+import com.android.gallery3d.filtershow.filters.FilterBasicRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+
+public class ParametricEditor extends Editor {
+ private int mLayoutID;
+ private int mViewID;
+ public static int ID = R.id.editorParametric;
+ private final String LOGTAG = "ParametricEditor";
+ protected Control mControl;
+ public static final int MINIMUM_WIDTH = 600;
+ public static final int MINIMUM_HEIGHT = 800;
+ View mActionButton;
+ View mEditControl;
+ static HashMap<String, Class> portraitMap = new HashMap<String, Class>();
+ static HashMap<String, Class> landscapeMap = new HashMap<String, Class>();
+ static {
+ portraitMap.put(ParameterInteger.sParameterType, BasicSlider.class);
+ landscapeMap.put(ParameterInteger.sParameterType, TitledSlider.class);
+ portraitMap.put(ParameterActionAndInt.sParameterType, ActionSlider.class);
+ landscapeMap.put(ParameterActionAndInt.sParameterType, ActionSlider.class);
+ portraitMap.put(ParameterStyles.sParameterType, StyleChooser.class);
+ landscapeMap.put(ParameterStyles.sParameterType, StyleChooser.class);
+ }
+
+ static Constructor getConstructor(Class cl) {
+ try {
+ return cl.getConstructor(Context.class, ViewGroup.class);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public ParametricEditor() {
+ super(ID);
+ }
+
+ protected ParametricEditor(int id) {
+ super(id);
+ }
+
+ protected ParametricEditor(int id, int layoutID, int viewID) {
+ super(id);
+ mLayoutID = layoutID;
+ mViewID = viewID;
+ }
+
+ @Override
+ public String calculateUserMessage(Context context, String effectName, Object parameterValue) {
+ String apply = "";
+
+ if (mShowParameter == SHOW_VALUE_INT & useCompact(context)) {
+ if (getLocalRepresentation() instanceof FilterBasicRepresentation) {
+ FilterBasicRepresentation interval = (FilterBasicRepresentation) getLocalRepresentation();
+ apply += " " + effectName.toUpperCase() + " " + interval.getStateRepresentation();
+ } else {
+ apply += " " + effectName.toUpperCase() + " " + parameterValue;
+ }
+ } else {
+ apply += " " + effectName.toUpperCase();
+ }
+ return apply;
+ }
+
+ @Override
+ public void createEditor(Context context, FrameLayout frameLayout) {
+ super.createEditor(context, frameLayout);
+ unpack(mViewID, mLayoutID);
+ }
+
+ @Override
+ public void reflectCurrentFilter() {
+ super.reflectCurrentFilter();
+ if (getLocalRepresentation() != null
+ && getLocalRepresentation() instanceof FilterBasicRepresentation) {
+ FilterBasicRepresentation interval = (FilterBasicRepresentation) getLocalRepresentation();
+ mControl.setPrameter(interval);
+ }
+ }
+
+ @Override
+ public Control[] getControls() {
+ BasicSlider slider = new BasicSlider();
+ return new Control[] {
+ slider
+ };
+ }
+
+ // TODO: need a better way to decide which representation
+ static boolean useCompact(Context context) {
+ WindowManager w = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE));
+ Point size = new Point();
+ w.getDefaultDisplay().getSize(size);
+ if (size.x < size.y) { // if tall than wider
+ return true;
+ }
+ if (size.x < MINIMUM_WIDTH) {
+ return true;
+ }
+ if (size.y < MINIMUM_HEIGHT) {
+ return true;
+ }
+ return false;
+ }
+
+ protected Parameter getParameterToEdit(FilterRepresentation rep) {
+ if (this instanceof Parameter) {
+ return (Parameter) this;
+ } else if (rep instanceof Parameter) {
+ return ((Parameter) rep);
+ }
+ return null;
+ }
+
+ @Override
+ public void setUtilityPanelUI(View actionButton, View editControl) {
+ mActionButton = actionButton;
+ mEditControl = editControl;
+ FilterRepresentation rep = getLocalRepresentation();
+ Parameter param = getParameterToEdit(rep);
+ if (param != null) {
+ control(param, editControl);
+ } else {
+ mSeekBar = new SeekBar(editControl.getContext());
+ LayoutParams lp = new LinearLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ mSeekBar.setLayoutParams(lp);
+ ((LinearLayout) editControl).addView(mSeekBar);
+ mSeekBar.setOnSeekBarChangeListener(this);
+ }
+ }
+
+ protected void control(Parameter p, View editControl) {
+ String pType = p.getParameterType();
+ Context context = editControl.getContext();
+ Class c = ((useCompact(context)) ? portraitMap : landscapeMap).get(pType);
+
+ if (c != null) {
+ try {
+ mControl = (Control) c.newInstance();
+ p.setController(mControl);
+ mControl.setUp((ViewGroup) editControl, p, this);
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Error in loading Control ", e);
+ }
+ } else {
+ Log.e(LOGTAG, "Unable to find class for " + pType);
+ for (String string : portraitMap.keySet()) {
+ Log.e(LOGTAG, "for " + string + " use " + portraitMap.get(string));
+ }
+ }
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar sbar, int progress, boolean arg2) {
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar arg0) {
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar arg0) {
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/SwapButton.java b/src/com/android/gallery3d/filtershow/editors/SwapButton.java
new file mode 100644
index 000000000..bb4432e28
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/editors/SwapButton.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2013 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.filtershow.editors;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.widget.Button;
+
+public class SwapButton extends Button implements GestureDetector.OnGestureListener {
+
+ public static int ANIM_DURATION = 200;
+
+ public interface SwapButtonListener {
+ public void swapLeft(MenuItem item);
+ public void swapRight(MenuItem item);
+ }
+
+ private GestureDetector mDetector;
+ private SwapButtonListener mListener;
+ private Menu mMenu;
+ private int mCurrentMenuIndex;
+
+ public SwapButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mDetector = new GestureDetector(context, this);
+ }
+
+ public SwapButtonListener getListener() {
+ return mListener;
+ }
+
+ public void setListener(SwapButtonListener listener) {
+ mListener = listener;
+ }
+
+ public boolean onTouchEvent(MotionEvent me) {
+ if (!mDetector.onTouchEvent(me)) {
+ return super.onTouchEvent(me);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ callOnClick();
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ return false;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ if (mMenu == null) {
+ return false;
+ }
+ if (e1.getX() - e2.getX() > 0) {
+ // right to left
+ mCurrentMenuIndex++;
+ if (mCurrentMenuIndex == mMenu.size()) {
+ mCurrentMenuIndex = 0;
+ }
+ if (mListener != null) {
+ mListener.swapRight(mMenu.getItem(mCurrentMenuIndex));
+ }
+ } else {
+ // left to right
+ mCurrentMenuIndex--;
+ if (mCurrentMenuIndex < 0) {
+ mCurrentMenuIndex = mMenu.size() - 1;
+ }
+ if (mListener != null) {
+ mListener.swapLeft(mMenu.getItem(mCurrentMenuIndex));
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
new file mode 100644
index 000000000..3fa91916d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorCrop;
+import com.android.gallery3d.filtershow.editors.EditorMirror;
+import com.android.gallery3d.filtershow.editors.EditorRotate;
+import com.android.gallery3d.filtershow.editors.EditorStraighten;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Vector;
+
+public abstract class BaseFiltersManager implements FiltersManagerInterface {
+ protected HashMap<Class, ImageFilter> mFilters = null;
+ protected HashMap<String, FilterRepresentation> mRepresentationLookup = null;
+ private static final String LOGTAG = "BaseFiltersManager";
+
+ protected ArrayList<FilterRepresentation> mLooks = new ArrayList<FilterRepresentation>();
+ protected ArrayList<FilterRepresentation> mBorders = new ArrayList<FilterRepresentation>();
+ protected ArrayList<FilterRepresentation> mTools = new ArrayList<FilterRepresentation>();
+ protected ArrayList<FilterRepresentation> mEffects = new ArrayList<FilterRepresentation>();
+
+ protected void init() {
+ mFilters = new HashMap<Class, ImageFilter>();
+ mRepresentationLookup = new HashMap<String, FilterRepresentation>();
+ Vector<Class> filters = new Vector<Class>();
+ addFilterClasses(filters);
+ for (Class filterClass : filters) {
+ try {
+ Object filterInstance = filterClass.newInstance();
+ if (filterInstance instanceof ImageFilter) {
+ mFilters.put(filterClass, (ImageFilter) filterInstance);
+
+ FilterRepresentation rep =
+ ((ImageFilter) filterInstance).getDefaultRepresentation();
+ if (rep != null) {
+ addRepresentation(rep);
+ }
+ }
+ } catch (InstantiationException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void addRepresentation(FilterRepresentation rep) {
+ mRepresentationLookup.put(rep.getSerializationName(), rep);
+ }
+
+ public FilterRepresentation createFilterFromName(String name) {
+ try {
+ return mRepresentationLookup.get(name).copy();
+ } catch (Exception e) {
+ Log.v(LOGTAG, "unable to generate a filter representation for \"" + name + "\"");
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public ImageFilter getFilter(Class c) {
+ return mFilters.get(c);
+ }
+
+ @Override
+ public ImageFilter getFilterForRepresentation(FilterRepresentation representation) {
+ return mFilters.get(representation.getFilterClass());
+ }
+
+ public FilterRepresentation getRepresentation(Class c) {
+ ImageFilter filter = mFilters.get(c);
+ if (filter != null) {
+ return filter.getDefaultRepresentation();
+ }
+ return null;
+ }
+
+ public void freeFilterResources(ImagePreset preset) {
+ if (preset == null) {
+ return;
+ }
+ Vector<ImageFilter> usedFilters = preset.getUsedFilters(this);
+ for (Class c : mFilters.keySet()) {
+ ImageFilter filter = mFilters.get(c);
+ if (!usedFilters.contains(filter)) {
+ filter.freeResources();
+ }
+ }
+ }
+
+ public void freeRSFilterScripts() {
+ for (Class c : mFilters.keySet()) {
+ ImageFilter filter = mFilters.get(c);
+ if (filter != null && filter instanceof ImageFilterRS) {
+ ((ImageFilterRS) filter).resetScripts();
+ }
+ }
+ }
+
+ protected void addFilterClasses(Vector<Class> filters) {
+ filters.add(ImageFilterTinyPlanet.class);
+ filters.add(ImageFilterRedEye.class);
+ filters.add(ImageFilterWBalance.class);
+ filters.add(ImageFilterExposure.class);
+ filters.add(ImageFilterVignette.class);
+ filters.add(ImageFilterGrad.class);
+ filters.add(ImageFilterContrast.class);
+ filters.add(ImageFilterShadows.class);
+ filters.add(ImageFilterHighlights.class);
+ filters.add(ImageFilterVibrance.class);
+ filters.add(ImageFilterSharpen.class);
+ filters.add(ImageFilterCurves.class);
+ filters.add(ImageFilterDraw.class);
+ filters.add(ImageFilterHue.class);
+ filters.add(ImageFilterChanSat.class);
+ filters.add(ImageFilterSaturated.class);
+ filters.add(ImageFilterBwFilter.class);
+ filters.add(ImageFilterNegative.class);
+ filters.add(ImageFilterEdge.class);
+ filters.add(ImageFilterKMeans.class);
+ filters.add(ImageFilterFx.class);
+ filters.add(ImageFilterBorder.class);
+ filters.add(ImageFilterParametricBorder.class);
+ }
+
+ public ArrayList<FilterRepresentation> getLooks() {
+ return mLooks;
+ }
+
+ public ArrayList<FilterRepresentation> getBorders() {
+ return mBorders;
+ }
+
+ public ArrayList<FilterRepresentation> getTools() {
+ return mTools;
+ }
+
+ public ArrayList<FilterRepresentation> getEffects() {
+ return mEffects;
+ }
+
+ public void addBorders(Context context) {
+
+ }
+
+ public void addLooks(Context context) {
+ int[] drawid = {
+ R.drawable.filtershow_fx_0005_punch,
+ R.drawable.filtershow_fx_0000_vintage,
+ R.drawable.filtershow_fx_0004_bw_contrast,
+ R.drawable.filtershow_fx_0002_bleach,
+ R.drawable.filtershow_fx_0001_instant,
+ R.drawable.filtershow_fx_0007_washout,
+ R.drawable.filtershow_fx_0003_blue_crush,
+ R.drawable.filtershow_fx_0008_washout_color,
+ R.drawable.filtershow_fx_0006_x_process
+ };
+
+ int[] fxNameid = {
+ R.string.ffx_punch,
+ R.string.ffx_vintage,
+ R.string.ffx_bw_contrast,
+ R.string.ffx_bleach,
+ R.string.ffx_instant,
+ R.string.ffx_washout,
+ R.string.ffx_blue_crush,
+ R.string.ffx_washout_color,
+ R.string.ffx_x_process
+ };
+
+ // Do not localize.
+ String[] serializationNames = {
+ "LUT3D_PUNCH",
+ "LUT3D_VINTAGE",
+ "LUT3D_BW",
+ "LUT3D_BLEACH",
+ "LUT3D_INSTANT",
+ "LUT3D_WASHOUT",
+ "LUT3D_BLUECRUSH",
+ "LUT3D_WASHOUT",
+ "LUT3D_XPROCESS"
+ };
+
+ FilterFxRepresentation nullFx =
+ new FilterFxRepresentation(context.getString(R.string.none),
+ 0, R.string.none);
+ mLooks.add(nullFx);
+
+ for (int i = 0; i < drawid.length; i++) {
+ FilterFxRepresentation fx = new FilterFxRepresentation(
+ context.getString(fxNameid[i]), drawid[i], fxNameid[i]);
+ fx.setSerializationName(serializationNames[i]);
+ ImagePreset preset = new ImagePreset();
+ preset.addFilter(fx);
+ FilterUserPresetRepresentation rep = new FilterUserPresetRepresentation(
+ context.getString(fxNameid[i]), preset, -1);
+ mLooks.add(rep);
+ addRepresentation(fx);
+ }
+ }
+
+ public void addEffects() {
+ mEffects.add(getRepresentation(ImageFilterTinyPlanet.class));
+ mEffects.add(getRepresentation(ImageFilterWBalance.class));
+ mEffects.add(getRepresentation(ImageFilterExposure.class));
+ mEffects.add(getRepresentation(ImageFilterVignette.class));
+ mEffects.add(getRepresentation(ImageFilterGrad.class));
+ mEffects.add(getRepresentation(ImageFilterContrast.class));
+ mEffects.add(getRepresentation(ImageFilterShadows.class));
+ mEffects.add(getRepresentation(ImageFilterHighlights.class));
+ mEffects.add(getRepresentation(ImageFilterVibrance.class));
+ mEffects.add(getRepresentation(ImageFilterSharpen.class));
+ mEffects.add(getRepresentation(ImageFilterCurves.class));
+ mEffects.add(getRepresentation(ImageFilterHue.class));
+ mEffects.add(getRepresentation(ImageFilterChanSat.class));
+ mEffects.add(getRepresentation(ImageFilterBwFilter.class));
+ mEffects.add(getRepresentation(ImageFilterNegative.class));
+ mEffects.add(getRepresentation(ImageFilterEdge.class));
+ mEffects.add(getRepresentation(ImageFilterKMeans.class));
+ }
+
+ public void addTools(Context context) {
+
+ int[] editorsId = {
+ EditorCrop.ID,
+ EditorStraighten.ID,
+ EditorRotate.ID,
+ EditorMirror.ID
+ };
+
+ int[] textId = {
+ R.string.crop,
+ R.string.straighten,
+ R.string.rotate,
+ R.string.mirror
+ };
+
+ int[] overlayId = {
+ R.drawable.filtershow_button_geometry_crop,
+ R.drawable.filtershow_button_geometry_straighten,
+ R.drawable.filtershow_button_geometry_rotate,
+ R.drawable.filtershow_button_geometry_flip
+ };
+
+ FilterRepresentation[] geometryFilters = {
+ new FilterCropRepresentation(),
+ new FilterStraightenRepresentation(),
+ new FilterRotateRepresentation(),
+ new FilterMirrorRepresentation()
+ };
+
+ for (int i = 0; i < editorsId.length; i++) {
+ int editorId = editorsId[i];
+ FilterRepresentation geometry = geometryFilters[i];
+ geometry.setEditorId(editorId);
+ geometry.setTextId(textId[i]);
+ geometry.setOverlayId(overlayId[i]);
+ geometry.setOverlayOnly(true);
+ if (geometry.getTextId() != 0) {
+ geometry.setName(context.getString(geometry.getTextId()));
+ }
+ mTools.add(geometry);
+ }
+
+ mTools.add(getRepresentation(ImageFilterRedEye.class));
+ mTools.add(getRepresentation(ImageFilterDraw.class));
+ }
+
+ public void setFilterResources(Resources resources) {
+ ImageFilterBorder filterBorder = (ImageFilterBorder) getFilter(ImageFilterBorder.class);
+ filterBorder.setResources(resources);
+ ImageFilterFx filterFx = (ImageFilterFx) getFilter(ImageFilterFx.class);
+ filterFx.setResources(resources);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ColorSpaceMatrix.java b/src/com/android/gallery3d/filtershow/filters/ColorSpaceMatrix.java
new file mode 100644
index 000000000..7c307a9e7
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ColorSpaceMatrix.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import java.util.Arrays;
+
+public class ColorSpaceMatrix {
+ private final float[] mMatrix = new float[16];
+ private static final float RLUM = 0.3086f;
+ private static final float GLUM = 0.6094f;
+ private static final float BLUM = 0.0820f;
+
+ public ColorSpaceMatrix() {
+ identity();
+ }
+
+ /**
+ * Copy constructor
+ *
+ * @param matrix
+ */
+ public ColorSpaceMatrix(ColorSpaceMatrix matrix) {
+ System.arraycopy(matrix.mMatrix, 0, mMatrix, 0, matrix.mMatrix.length);
+ }
+
+ /**
+ * get the matrix
+ *
+ * @return the internal matrix
+ */
+ public float[] getMatrix() {
+ return mMatrix;
+ }
+
+ /**
+ * set matrix to identity
+ */
+ public void identity() {
+ Arrays.fill(mMatrix, 0);
+ mMatrix[0] = mMatrix[5] = mMatrix[10] = mMatrix[15] = 1;
+ }
+
+ public void convertToLuminance() {
+ mMatrix[0] = mMatrix[1] = mMatrix[2] = 0.3086f;
+ mMatrix[4] = mMatrix[5] = mMatrix[6] = 0.6094f;
+ mMatrix[8] = mMatrix[9] = mMatrix[10] = 0.0820f;
+ }
+
+ private void multiply(float[] a)
+ {
+ int x, y;
+ float[] temp = new float[16];
+
+ for (y = 0; y < 4; y++) {
+ int y4 = y * 4;
+ for (x = 0; x < 4; x++) {
+ temp[y4 + x] = mMatrix[y4 + 0] * a[x]
+ + mMatrix[y4 + 1] * a[4 + x]
+ + mMatrix[y4 + 2] * a[8 + x]
+ + mMatrix[y4 + 3] * a[12 + x];
+ }
+ }
+ for (int i = 0; i < 16; i++)
+ mMatrix[i] = temp[i];
+ }
+
+ private void xRotateMatrix(float rs, float rc)
+ {
+ ColorSpaceMatrix c = new ColorSpaceMatrix();
+ float[] tmp = c.mMatrix;
+
+ tmp[5] = rc;
+ tmp[6] = rs;
+ tmp[9] = -rs;
+ tmp[10] = rc;
+
+ multiply(tmp);
+ }
+
+ private void yRotateMatrix(float rs, float rc)
+ {
+ ColorSpaceMatrix c = new ColorSpaceMatrix();
+ float[] tmp = c.mMatrix;
+
+ tmp[0] = rc;
+ tmp[2] = -rs;
+ tmp[8] = rs;
+ tmp[10] = rc;
+
+ multiply(tmp);
+ }
+
+ private void zRotateMatrix(float rs, float rc)
+ {
+ ColorSpaceMatrix c = new ColorSpaceMatrix();
+ float[] tmp = c.mMatrix;
+
+ tmp[0] = rc;
+ tmp[1] = rs;
+ tmp[4] = -rs;
+ tmp[5] = rc;
+ multiply(tmp);
+ }
+
+ private void zShearMatrix(float dx, float dy)
+ {
+ ColorSpaceMatrix c = new ColorSpaceMatrix();
+ float[] tmp = c.mMatrix;
+
+ tmp[2] = dx;
+ tmp[6] = dy;
+ multiply(tmp);
+ }
+
+ /**
+ * sets the transform to a shift in Hue
+ *
+ * @param rot rotation in degrees
+ */
+ public void setHue(float rot)
+ {
+ float mag = (float) Math.sqrt(2.0);
+ float xrs = 1 / mag;
+ float xrc = 1 / mag;
+ xRotateMatrix(xrs, xrc);
+ mag = (float) Math.sqrt(3.0);
+ float yrs = -1 / mag;
+ float yrc = (float) Math.sqrt(2.0) / mag;
+ yRotateMatrix(yrs, yrc);
+
+ float lx = getRedf(RLUM, GLUM, BLUM);
+ float ly = getGreenf(RLUM, GLUM, BLUM);
+ float lz = getBluef(RLUM, GLUM, BLUM);
+ float zsx = lx / lz;
+ float zsy = ly / lz;
+ zShearMatrix(zsx, zsy);
+
+ float zrs = (float) Math.sin(rot * Math.PI / 180.0);
+ float zrc = (float) Math.cos(rot * Math.PI / 180.0);
+ zRotateMatrix(zrs, zrc);
+ zShearMatrix(-zsx, -zsy);
+ yRotateMatrix(-yrs, yrc);
+ xRotateMatrix(-xrs, xrc);
+ }
+
+ /**
+ * set it to a saturation matrix
+ *
+ * @param s
+ */
+ public void changeSaturation(float s) {
+ mMatrix[0] = (1 - s) * RLUM + s;
+ mMatrix[1] = (1 - s) * RLUM;
+ mMatrix[2] = (1 - s) * RLUM;
+ mMatrix[4] = (1 - s) * GLUM;
+ mMatrix[5] = (1 - s) * GLUM + s;
+ mMatrix[6] = (1 - s) * GLUM;
+ mMatrix[8] = (1 - s) * BLUM;
+ mMatrix[9] = (1 - s) * BLUM;
+ mMatrix[10] = (1 - s) * BLUM + s;
+ }
+
+ /**
+ * Transform RGB value
+ *
+ * @param r red pixel value
+ * @param g green pixel value
+ * @param b blue pixel value
+ * @return computed red pixel value
+ */
+ public float getRed(int r, int g, int b) {
+ return r * mMatrix[0] + g * mMatrix[4] + b * mMatrix[8] + mMatrix[12];
+ }
+
+ /**
+ * Transform RGB value
+ *
+ * @param r red pixel value
+ * @param g green pixel value
+ * @param b blue pixel value
+ * @return computed green pixel value
+ */
+ public float getGreen(int r, int g, int b) {
+ return r * mMatrix[1] + g * mMatrix[5] + b * mMatrix[9] + mMatrix[13];
+ }
+
+ /**
+ * Transform RGB value
+ *
+ * @param r red pixel value
+ * @param g green pixel value
+ * @param b blue pixel value
+ * @return computed blue pixel value
+ */
+ public float getBlue(int r, int g, int b) {
+ return r * mMatrix[2] + g * mMatrix[6] + b * mMatrix[10] + mMatrix[14];
+ }
+
+ private float getRedf(float r, float g, float b) {
+ return r * mMatrix[0] + g * mMatrix[4] + b * mMatrix[8] + mMatrix[12];
+ }
+
+ private float getGreenf(float r, float g, float b) {
+ return r * mMatrix[1] + g * mMatrix[5] + b * mMatrix[9] + mMatrix[13];
+ }
+
+ private float getBluef(float r, float g, float b) {
+ return r * mMatrix[2] + g * mMatrix[6] + b * mMatrix[10] + mMatrix[14];
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
new file mode 100644
index 000000000..1eebdb571
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterBasicRepresentation.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+
+import android.util.Log;
+
+import com.android.gallery3d.filtershow.controller.Control;
+import com.android.gallery3d.filtershow.controller.FilterView;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.controller.ParameterInteger;
+
+public class FilterBasicRepresentation extends FilterRepresentation implements ParameterInteger {
+ private static final String LOGTAG = "FilterBasicRep";
+ private int mMinimum;
+ private int mValue;
+ private int mMaximum;
+ private int mDefaultValue;
+ private int mPreviewValue;
+ public static final String SERIAL_NAME = "Name";
+ public static final String SERIAL_VALUE = "Value";
+ private boolean mLogVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
+
+ public FilterBasicRepresentation(String name, int minimum, int value, int maximum) {
+ super(name);
+ mMinimum = minimum;
+ mMaximum = maximum;
+ setValue(value);
+ }
+
+ @Override
+ public String toString() {
+ return getName() + " : " + mMinimum + " < " + mValue + " < " + mMaximum;
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ FilterBasicRepresentation representation = new FilterBasicRepresentation(getName(),0,0,0);
+ copyAllParameters(representation);
+ return representation;
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ @Override
+ public void useParametersFrom(FilterRepresentation a) {
+ if (a instanceof FilterBasicRepresentation) {
+ FilterBasicRepresentation representation = (FilterBasicRepresentation) a;
+ setMinimum(representation.getMinimum());
+ setMaximum(representation.getMaximum());
+ setValue(representation.getValue());
+ setDefaultValue(representation.getDefaultValue());
+ setPreviewValue(representation.getPreviewValue());
+ }
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation representation) {
+ if (!super.equals(representation)) {
+ return false;
+ }
+ if (representation instanceof FilterBasicRepresentation) {
+ FilterBasicRepresentation basic = (FilterBasicRepresentation) representation;
+ if (basic.mMinimum == mMinimum
+ && basic.mMaximum == mMaximum
+ && basic.mValue == mValue
+ && basic.mDefaultValue == mDefaultValue
+ && basic.mPreviewValue == mPreviewValue) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int getMinimum() {
+ return mMinimum;
+ }
+
+ public void setMinimum(int minimum) {
+ mMinimum = minimum;
+ }
+
+ @Override
+ public int getValue() {
+ return mValue;
+ }
+
+ @Override
+ public void setValue(int value) {
+ mValue = value;
+ if (mValue < mMinimum) {
+ mValue = mMinimum;
+ }
+ if (mValue > mMaximum) {
+ mValue = mMaximum;
+ }
+ }
+
+ @Override
+ public int getMaximum() {
+ return mMaximum;
+ }
+
+ public void setMaximum(int maximum) {
+ mMaximum = maximum;
+ }
+
+ public void setDefaultValue(int defaultValue) {
+ mDefaultValue = defaultValue;
+ }
+
+ @Override
+ public int getDefaultValue() {
+ return mDefaultValue;
+ }
+
+ public int getPreviewValue() {
+ return mPreviewValue;
+ }
+
+ public void setPreviewValue(int previewValue) {
+ mPreviewValue = previewValue;
+ }
+
+ @Override
+ public String getStateRepresentation() {
+ int val = getValue();
+ return ((val > 0) ? "+" : "") + val;
+ }
+
+ @Override
+ public String getParameterType(){
+ return sParameterType;
+ }
+
+ @Override
+ public void setController(Control control) {
+ }
+
+ @Override
+ public String getValueString() {
+ return getStateRepresentation();
+ }
+
+ @Override
+ public String getParameterName() {
+ return getName();
+ }
+
+ @Override
+ public void setFilterView(FilterView editor) {
+ }
+
+ @Override
+ public void copyFrom(Parameter src) {
+ useParametersFrom((FilterBasicRepresentation) src);
+ }
+
+ @Override
+ public String[][] serializeRepresentation() {
+ String[][] ret = {
+ {SERIAL_NAME , getName() },
+ {SERIAL_VALUE , Integer.toString(mValue)}};
+ return ret;
+ }
+
+ @Override
+ public void deSerializeRepresentation(String[][] rep) {
+ super.deSerializeRepresentation(rep);
+ for (int i = 0; i < rep.length; i++) {
+ if (SERIAL_VALUE.equals(rep[i][0])) {
+ mValue = Integer.parseInt(rep[i][1]);
+ break;
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterChanSatRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterChanSatRepresentation.java
new file mode 100644
index 000000000..7ce67dd96
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterChanSatRepresentation.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.controller.BasicParameterInt;
+import com.android.gallery3d.filtershow.controller.Parameter;
+import com.android.gallery3d.filtershow.controller.ParameterSet;
+import com.android.gallery3d.filtershow.editors.EditorChanSat;
+import com.android.gallery3d.filtershow.imageshow.ControlPoint;
+import com.android.gallery3d.filtershow.imageshow.Spline;
+
+import java.io.IOException;
+import java.util.Vector;
+
+/**
+ * Representation for a filter that has per channel & Master saturation
+ */
+public class FilterChanSatRepresentation extends FilterRepresentation implements ParameterSet {
+ private static final String LOGTAG = "FilterChanSatRepresentation";
+ private static final String ARGS = "ARGS";
+ private static final String SERIALIZATION_NAME = "channelsaturation";
+
+ public static final int MODE_MASTER = 0;
+ public static final int MODE_RED = 1;
+ public static final int MODE_YELLOW = 2;
+ public static final int MODE_GREEN = 3;
+ public static final int MODE_CYAN = 4;
+ public static final int MODE_BLUE = 5;
+ public static final int MODE_MAGENTA = 6;
+ private int mParameterMode = MODE_MASTER;
+
+ private static int MINSAT = -100;
+ private static int MAXSAT = 100;
+ private BasicParameterInt mParamMaster = new BasicParameterInt(MODE_MASTER, 0, MINSAT, MAXSAT);
+ private BasicParameterInt mParamRed = new BasicParameterInt(MODE_RED, 0, MINSAT, MAXSAT);
+ private BasicParameterInt mParamYellow = new BasicParameterInt(MODE_YELLOW, 0, MINSAT, MAXSAT);
+ private BasicParameterInt mParamGreen = new BasicParameterInt(MODE_GREEN, 0, MINSAT, MAXSAT);
+ private BasicParameterInt mParamCyan = new BasicParameterInt(MODE_CYAN, 0, MINSAT, MAXSAT);
+ private BasicParameterInt mParamBlue = new BasicParameterInt(MODE_BLUE, 0, MINSAT, MAXSAT);
+ private BasicParameterInt mParamMagenta = new BasicParameterInt(MODE_MAGENTA, 0, MINSAT, MAXSAT);
+
+ private BasicParameterInt[] mAllParam = {
+ mParamMaster,
+ mParamRed,
+ mParamYellow,
+ mParamGreen,
+ mParamCyan,
+ mParamBlue,
+ mParamMagenta};
+
+ public FilterChanSatRepresentation() {
+ super("ChannelSaturation");
+ setTextId(R.string.saturation);
+ setFilterType(FilterRepresentation.TYPE_NORMAL);
+ setSerializationName(SERIALIZATION_NAME);
+ setFilterClass(ImageFilterChanSat.class);
+ setEditorId(EditorChanSat.ID);
+ }
+
+ public String toString() {
+ return getName() + " : " + mParamRed + ", " + mParamCyan + ", " + mParamRed
+ + ", " + mParamGreen + ", " + mParamMaster + ", " + mParamYellow;
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ FilterChanSatRepresentation representation = new FilterChanSatRepresentation();
+ copyAllParameters(representation);
+ return representation;
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ public void useParametersFrom(FilterRepresentation a) {
+ if (a instanceof FilterChanSatRepresentation) {
+ FilterChanSatRepresentation representation = (FilterChanSatRepresentation) a;
+
+ for (int i = 0; i < mAllParam.length; i++) {
+ mAllParam[i].copyFrom(representation.mAllParam[i]);
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation representation) {
+ if (!super.equals(representation)) {
+ return false;
+ }
+ if (representation instanceof FilterChanSatRepresentation) {
+ FilterChanSatRepresentation rep = (FilterChanSatRepresentation) representation;
+ for (int i = 0; i < mAllParam.length; i++) {
+ if (rep.getValue(i) != getValue(i))
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public int getValue(int mode) {
+ return mAllParam[mode].getValue();
+ }
+
+ public void setValue(int mode, int value) {
+ mAllParam[mode].setValue(value);
+ }
+
+ public int getMinimum() {
+ return mParamMaster.getMinimum();
+ }
+
+ public int getMaximum() {
+ return mParamMaster.getMaximum();
+ }
+
+ public int getParameterMode() {
+ return mParameterMode;
+ }
+
+ public void setParameterMode(int parameterMode) {
+ mParameterMode = parameterMode;
+ }
+
+ public int getCurrentParameter() {
+ return getValue(mParameterMode);
+ }
+
+ public void setCurrentParameter(int value) {
+ setValue(mParameterMode, value);
+ }
+
+ @Override
+ public int getNumberOfParameters() {
+ return 6;
+ }
+
+ @Override
+ public Parameter getFilterParameter(int index) {
+ return mAllParam[index];
+ }
+
+ @Override
+ public void serializeRepresentation(JsonWriter writer) throws IOException {
+ writer.beginObject();
+
+ writer.name(ARGS);
+ writer.beginArray();
+ writer.value(getValue(MODE_MASTER));
+ writer.value(getValue(MODE_RED));
+ writer.value(getValue(MODE_YELLOW));
+ writer.value(getValue(MODE_GREEN));
+ writer.value(getValue(MODE_CYAN));
+ writer.value(getValue(MODE_BLUE));
+ writer.value(getValue(MODE_MAGENTA));
+ writer.endArray();
+ writer.endObject();
+ }
+
+ @Override
+ public void deSerializeRepresentation(JsonReader sreader) throws IOException {
+ sreader.beginObject();
+
+ while (sreader.hasNext()) {
+ String name = sreader.nextName();
+ if (name.startsWith(ARGS)) {
+ sreader.beginArray();
+ sreader.hasNext();
+ setValue(MODE_MASTER, sreader.nextInt());
+ sreader.hasNext();
+ setValue(MODE_RED, sreader.nextInt());
+ sreader.hasNext();
+ setValue(MODE_YELLOW, sreader.nextInt());
+ sreader.hasNext();
+ setValue(MODE_GREEN, sreader.nextInt());
+ sreader.hasNext();
+ setValue(MODE_CYAN, sreader.nextInt());
+ sreader.hasNext();
+ setValue(MODE_BLUE, sreader.nextInt());
+ sreader.hasNext();
+ setValue(MODE_MAGENTA, sreader.nextInt());
+ sreader.hasNext();
+ sreader.endArray();
+ } else {
+ sreader.skipValue();
+ }
+ }
+ sreader.endObject();
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterColorBorderRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterColorBorderRepresentation.java
new file mode 100644
index 000000000..94eb20631
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterColorBorderRepresentation.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+
+public class FilterColorBorderRepresentation extends FilterRepresentation {
+ private int mColor;
+ private int mBorderSize;
+ private int mBorderRadius;
+
+ public FilterColorBorderRepresentation(int color, int size, int radius) {
+ super("ColorBorder");
+ mColor = color;
+ mBorderSize = size;
+ mBorderRadius = radius;
+ setFilterType(FilterRepresentation.TYPE_BORDER);
+ setTextId(R.string.borders);
+ setEditorId(ImageOnlyEditor.ID);
+ setShowParameterValue(false);
+ }
+
+ public String toString() {
+ return "FilterBorder: " + getName();
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ FilterColorBorderRepresentation representation = new FilterColorBorderRepresentation(0,0,0);
+ copyAllParameters(representation);
+ return representation;
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ public void useParametersFrom(FilterRepresentation a) {
+ if (a instanceof FilterColorBorderRepresentation) {
+ FilterColorBorderRepresentation representation = (FilterColorBorderRepresentation) a;
+ setName(representation.getName());
+ setColor(representation.getColor());
+ setBorderSize(representation.getBorderSize());
+ setBorderRadius(representation.getBorderRadius());
+ }
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation representation) {
+ if (!super.equals(representation)) {
+ return false;
+ }
+ if (representation instanceof FilterColorBorderRepresentation) {
+ FilterColorBorderRepresentation border = (FilterColorBorderRepresentation) representation;
+ if (border.mColor == mColor
+ && border.mBorderSize == mBorderSize
+ && border.mBorderRadius == mBorderRadius) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean allowsSingleInstanceOnly() {
+ return true;
+ }
+
+ @Override
+ public int getTextId() {
+ return R.string.borders;
+ }
+
+ public int getColor() {
+ return mColor;
+ }
+
+ public void setColor(int color) {
+ mColor = color;
+ }
+
+ public int getBorderSize() {
+ return mBorderSize;
+ }
+
+ public void setBorderSize(int borderSize) {
+ mBorderSize = borderSize;
+ }
+
+ public int getBorderRadius() {
+ return mBorderRadius;
+ }
+
+ public void setBorderRadius(int borderRadius) {
+ mBorderRadius = borderRadius;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterCropRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterCropRepresentation.java
new file mode 100644
index 000000000..c1bd7b3bb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterCropRepresentation.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.graphics.RectF;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorCrop;
+
+import java.io.IOException;
+
+public class FilterCropRepresentation extends FilterRepresentation {
+ public static final String SERIALIZATION_NAME = "CROP";
+ public static final String[] BOUNDS = {
+ "C0", "C1", "C2", "C3"
+ };
+ private static final String TAG = FilterCropRepresentation.class.getSimpleName();
+
+ RectF mCrop = getNil();
+
+ public FilterCropRepresentation(RectF crop) {
+ super(FilterCropRepresentation.class.getSimpleName());
+ setSerializationName(SERIALIZATION_NAME);
+ setShowParameterValue(true);
+ setFilterClass(FilterCropRepresentation.class);
+ setFilterType(FilterRepresentation.TYPE_GEOMETRY);
+ setTextId(R.string.crop);
+ setEditorId(EditorCrop.ID);
+ setCrop(crop);
+ }
+
+ public FilterCropRepresentation(FilterCropRepresentation m) {
+ this(m.mCrop);
+ }
+
+ public FilterCropRepresentation() {
+ this(sNilRect);
+ }
+
+ public void set(FilterCropRepresentation r) {
+ mCrop.set(r.mCrop);
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation rep) {
+ if (!(rep instanceof FilterCropRepresentation)) {
+ return false;
+ }
+ FilterCropRepresentation crop = (FilterCropRepresentation) rep;
+ if (mCrop.bottom != crop.mCrop.bottom
+ || mCrop.left != crop.mCrop.left
+ || mCrop.right != crop.mCrop.right
+ || mCrop.top != crop.mCrop.top) {
+ return false;
+ }
+ return true;
+ }
+
+ public RectF getCrop() {
+ return new RectF(mCrop);
+ }
+
+ public void getCrop(RectF r) {
+ r.set(mCrop);
+ }
+
+ public void setCrop(RectF crop) {
+ if (crop == null) {
+ throw new IllegalArgumentException("Argument to setCrop is null");
+ }
+ mCrop.set(crop);
+ }
+
+ /**
+ * Takes a crop rect contained by [0, 0, 1, 1] and scales it by the height
+ * and width of the image rect.
+ */
+ public static void findScaledCrop(RectF crop, int bitmapWidth, int bitmapHeight) {
+ crop.left *= bitmapWidth;
+ crop.top *= bitmapHeight;
+ crop.right *= bitmapWidth;
+ crop.bottom *= bitmapHeight;
+ }
+
+ /**
+ * Takes crop rect and normalizes it by scaling down by the height and width
+ * of the image rect.
+ */
+ public static void findNormalizedCrop(RectF crop, int bitmapWidth, int bitmapHeight) {
+ crop.left /= bitmapWidth;
+ crop.top /= bitmapHeight;
+ crop.right /= bitmapWidth;
+ crop.bottom /= bitmapHeight;
+ }
+
+ @Override
+ public boolean allowsSingleInstanceOnly() {
+ return true;
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ return new FilterCropRepresentation(this);
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ if (!(representation instanceof FilterCropRepresentation)) {
+ throw new IllegalArgumentException("calling copyAllParameters with incompatible types!");
+ }
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ @Override
+ public void useParametersFrom(FilterRepresentation a) {
+ if (!(a instanceof FilterCropRepresentation)) {
+ throw new IllegalArgumentException("calling useParametersFrom with incompatible types!");
+ }
+ setCrop(((FilterCropRepresentation) a).mCrop);
+ }
+
+ private static final RectF sNilRect = new RectF(0, 0, 1, 1);
+
+ @Override
+ public boolean isNil() {
+ return mCrop.equals(sNilRect);
+ }
+
+ public static RectF getNil() {
+ return new RectF(sNilRect);
+ }
+
+ @Override
+ public void serializeRepresentation(JsonWriter writer) throws IOException {
+ writer.beginObject();
+ writer.name(BOUNDS[0]).value(mCrop.left);
+ writer.name(BOUNDS[1]).value(mCrop.top);
+ writer.name(BOUNDS[2]).value(mCrop.right);
+ writer.name(BOUNDS[3]).value(mCrop.bottom);
+ writer.endObject();
+ }
+
+ @Override
+ public void deSerializeRepresentation(JsonReader reader) throws IOException {
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if (BOUNDS[0].equals(name)) {
+ mCrop.left = (float) reader.nextDouble();
+ } else if (BOUNDS[1].equals(name)) {
+ mCrop.top = (float) reader.nextDouble();
+ } else if (BOUNDS[2].equals(name)) {
+ mCrop.right = (float) reader.nextDouble();
+ } else if (BOUNDS[3].equals(name)) {
+ mCrop.bottom = (float) reader.nextDouble();
+ } else {
+ reader.skipValue();
+ }
+ }
+ reader.endObject();
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java
new file mode 100644
index 000000000..edab2a08d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterCurvesRepresentation.java
@@ -0,0 +1,170 @@
+package com.android.gallery3d.filtershow.filters;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.imageshow.ControlPoint;
+import com.android.gallery3d.filtershow.imageshow.Spline;
+
+import java.io.IOException;
+
+/**
+ * TODO: Insert description here. (generated by hoford)
+ */
+public class FilterCurvesRepresentation extends FilterRepresentation {
+ private static final String LOGTAG = "FilterCurvesRepresentation";
+ public static final String SERIALIZATION_NAME = "Curve";
+ private static final int MAX_SPLINE_NUMBER = 4;
+
+ private Spline[] mSplines = new Spline[MAX_SPLINE_NUMBER];
+
+ public FilterCurvesRepresentation() {
+ super("Curves");
+ setSerializationName("CURVES");
+ setFilterClass(ImageFilterCurves.class);
+ setTextId(R.string.curvesRGB);
+ setOverlayId(R.drawable.filtershow_button_colors_curve);
+ setEditorId(R.id.imageCurves);
+ setShowParameterValue(false);
+ setSupportsPartialRendering(true);
+ reset();
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ FilterCurvesRepresentation representation = new FilterCurvesRepresentation();
+ copyAllParameters(representation);
+ return representation;
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ @Override
+ public void useParametersFrom(FilterRepresentation a) {
+ if (!(a instanceof FilterCurvesRepresentation)) {
+ Log.v(LOGTAG, "cannot use parameters from " + a);
+ return;
+ }
+ FilterCurvesRepresentation representation = (FilterCurvesRepresentation) a;
+ Spline[] spline = new Spline[MAX_SPLINE_NUMBER];
+ for (int i = 0; i < spline.length; i++) {
+ Spline sp = representation.mSplines[i];
+ if (sp != null) {
+ spline[i] = new Spline(sp);
+ } else {
+ spline[i] = new Spline();
+ }
+ }
+ mSplines = spline;
+ }
+
+ @Override
+ public boolean isNil() {
+ for (int i = 0; i < MAX_SPLINE_NUMBER; i++) {
+ if (getSpline(i) != null && !getSpline(i).isOriginal()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation representation) {
+ if (!super.equals(representation)) {
+ return false;
+ }
+
+ if (!(representation instanceof FilterCurvesRepresentation)) {
+ return false;
+ } else {
+ FilterCurvesRepresentation curve =
+ (FilterCurvesRepresentation) representation;
+ for (int i = 0; i < MAX_SPLINE_NUMBER; i++) {
+ if (!getSpline(i).sameValues(curve.getSpline(i))) {
+ return false;
+ }
+ }
+ }
+ // Every spline matches, therefore they are the same.
+ return true;
+ }
+
+ public void reset() {
+ Spline spline = new Spline();
+
+ spline.addPoint(0.0f, 1.0f);
+ spline.addPoint(1.0f, 0.0f);
+
+ for (int i = 0; i < MAX_SPLINE_NUMBER; i++) {
+ mSplines[i] = new Spline(spline);
+ }
+ }
+
+ public void setSpline(int splineIndex, Spline s) {
+ mSplines[splineIndex] = s;
+ }
+
+ public Spline getSpline(int splineIndex) {
+ return mSplines[splineIndex];
+ }
+
+ @Override
+ public void serializeRepresentation(JsonWriter writer) throws IOException {
+ writer.beginObject();
+ {
+ writer.name(NAME_TAG);
+ writer.value(getName());
+ for (int i = 0; i < mSplines.length; i++) {
+ writer.name(SERIALIZATION_NAME + i);
+ writer.beginArray();
+ int nop = mSplines[i].getNbPoints();
+ for (int j = 0; j < nop; j++) {
+ ControlPoint p = mSplines[i].getPoint(j);
+ writer.beginArray();
+ writer.value(p.x);
+ writer.value(p.y);
+ writer.endArray();
+ }
+ writer.endArray();
+ }
+
+ }
+ writer.endObject();
+ }
+
+ @Override
+ public void deSerializeRepresentation(JsonReader sreader) throws IOException {
+ sreader.beginObject();
+ Spline[] spline = new Spline[MAX_SPLINE_NUMBER];
+ while (sreader.hasNext()) {
+ String name = sreader.nextName();
+ if (NAME_TAG.equals(name)) {
+ setName(sreader.nextString());
+ } else if (name.startsWith(SERIALIZATION_NAME)) {
+ int curveNo = Integer.parseInt(name.substring(SERIALIZATION_NAME.length()));
+ spline[curveNo] = new Spline();
+ sreader.beginArray();
+ while (sreader.hasNext()) {
+ sreader.beginArray();
+ sreader.hasNext();
+ float x = (float) sreader.nextDouble();
+ sreader.hasNext();
+ float y = (float) sreader.nextDouble();
+ sreader.endArray();
+ spline[curveNo].addPoint(x, y);
+ }
+ sreader.endArray();
+
+ }
+ }
+ mSplines = spline;
+ sreader.endObject();
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterDirectRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterDirectRepresentation.java
new file mode 100644
index 000000000..ac0cb7492
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterDirectRepresentation.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+public class FilterDirectRepresentation extends FilterRepresentation {
+
+ @Override
+ public FilterRepresentation copy() {
+ FilterDirectRepresentation representation = new FilterDirectRepresentation(getName());
+ copyAllParameters(representation);
+ return representation;
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ public FilterDirectRepresentation(String name) {
+ super(name);
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java
new file mode 100644
index 000000000..977dbeac5
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterDrawRepresentation.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.graphics.Path;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorDraw;
+
+import java.util.Vector;
+
+public class FilterDrawRepresentation extends FilterRepresentation {
+ private static final String LOGTAG = "FilterDrawRepresentation";
+
+ public static class StrokeData implements Cloneable {
+ public byte mType;
+ public Path mPath;
+ public float mRadius;
+ public int mColor;
+ public int noPoints = 0;
+ @Override
+ public String toString() {
+ return "stroke(" + mType + ", path(" + (mPath) + "), " + mRadius + " , "
+ + Integer.toHexString(mColor) + ")";
+ }
+ @Override
+ public StrokeData clone() throws CloneNotSupportedException {
+ return (StrokeData) super.clone();
+ }
+ }
+
+ private Vector<StrokeData> mDrawing = new Vector<StrokeData>();
+ private StrokeData mCurrent; // used in the currently drawing style
+
+ public FilterDrawRepresentation() {
+ super("Draw");
+ setFilterClass(ImageFilterDraw.class);
+ setSerializationName("DRAW");
+ setFilterType(FilterRepresentation.TYPE_VIGNETTE);
+ setTextId(R.string.imageDraw);
+ setEditorId(EditorDraw.ID);
+ setOverlayId(R.drawable.filtershow_drawing);
+ setOverlayOnly(true);
+ }
+
+ @Override
+ public String toString() {
+ return getName() + " : strokes=" + mDrawing.size()
+ + ((mCurrent == null) ? " no current "
+ : ("draw=" + mCurrent.mType + " " + mCurrent.noPoints));
+ }
+
+ public Vector<StrokeData> getDrawing() {
+ return mDrawing;
+ }
+
+ public StrokeData getCurrentDrawing() {
+ return mCurrent;
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ FilterDrawRepresentation representation = new FilterDrawRepresentation();
+ copyAllParameters(representation);
+ return representation;
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ @Override
+ public boolean isNil() {
+ return getDrawing().isEmpty();
+ }
+
+ @Override
+ public void useParametersFrom(FilterRepresentation a) {
+ if (a instanceof FilterDrawRepresentation) {
+ FilterDrawRepresentation representation = (FilterDrawRepresentation) a;
+ try {
+ if (representation.mCurrent != null) {
+ mCurrent = (StrokeData) representation.mCurrent.clone();
+ } else {
+ mCurrent = null;
+ }
+ if (representation.mDrawing != null) {
+ mDrawing = (Vector<StrokeData>) representation.mDrawing.clone();
+ } else {
+ mDrawing = null;
+ }
+
+ } catch (CloneNotSupportedException e) {
+ e.printStackTrace();
+ }
+ } else {
+ Log.v(LOGTAG, "cannot use parameters from " + a);
+ }
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation representation) {
+ if (!super.equals(representation)) {
+ return false;
+ }
+ if (representation instanceof FilterDrawRepresentation) {
+ FilterDrawRepresentation fdRep = (FilterDrawRepresentation) representation;
+ if (fdRep.mDrawing.size() != mDrawing.size())
+ return false;
+ if (fdRep.mCurrent == null && mCurrent.mPath == null) {
+ return true;
+ }
+ if (fdRep.mCurrent != null && mCurrent.mPath != null) {
+ if (fdRep.mCurrent.noPoints == mCurrent.noPoints) {
+ return true;
+ }
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public void startNewSection(byte type, int color, float size, float x, float y) {
+ mCurrent = new StrokeData();
+ mCurrent.mColor = color;
+ mCurrent.mRadius = size;
+ mCurrent.mType = type;
+ mCurrent.mPath = new Path();
+ mCurrent.mPath.moveTo(x, y);
+ mCurrent.noPoints = 0;
+ }
+
+ public void addPoint(float x, float y) {
+ mCurrent.noPoints++;
+ mCurrent.mPath.lineTo(x, y);
+ }
+
+ public void endSection(float x, float y) {
+ mCurrent.mPath.lineTo(x, y);
+ mCurrent.noPoints++;
+ mDrawing.add(mCurrent);
+ mCurrent = null;
+ }
+
+ public void clearCurrentSection() {
+ mCurrent = null;
+ }
+
+ public void clear() {
+ mCurrent = null;
+ mDrawing.clear();
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java
new file mode 100644
index 000000000..e5a6fdd23
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterFxRepresentation.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+
+public class FilterFxRepresentation extends FilterRepresentation {
+ private static final String LOGTAG = "FilterFxRepresentation";
+ // TODO: When implementing serialization, we should find a unique way of
+ // specifying bitmaps / names (the resource IDs being random)
+ private int mBitmapResource = 0;
+ private int mNameResource = 0;
+
+ public FilterFxRepresentation(String name, int bitmapResource, int nameResource) {
+ super(name);
+ setFilterClass(ImageFilterFx.class);
+ mBitmapResource = bitmapResource;
+ mNameResource = nameResource;
+ setFilterType(FilterRepresentation.TYPE_FX);
+ setTextId(nameResource);
+ setEditorId(ImageOnlyEditor.ID);
+ setShowParameterValue(false);
+ setSupportsPartialRendering(true);
+ }
+
+ @Override
+ public String toString() {
+ return "FilterFx: " + hashCode() + " : " + getName() + " bitmap rsc: " + mBitmapResource;
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ FilterFxRepresentation representation = new FilterFxRepresentation(getName(),0,0);
+ copyAllParameters(representation);
+ return representation;
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ @Override
+ public synchronized void useParametersFrom(FilterRepresentation a) {
+ if (a instanceof FilterFxRepresentation) {
+ FilterFxRepresentation representation = (FilterFxRepresentation) a;
+ setName(representation.getName());
+ setSerializationName(representation.getSerializationName());
+ setBitmapResource(representation.getBitmapResource());
+ setNameResource(representation.getNameResource());
+ }
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation representation) {
+ if (!super.equals(representation)) {
+ return false;
+ }
+ if (representation instanceof FilterFxRepresentation) {
+ FilterFxRepresentation fx = (FilterFxRepresentation) representation;
+ if (fx.mNameResource == mNameResource
+ && fx.mBitmapResource == mBitmapResource) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean same(FilterRepresentation representation) {
+ if (!super.same(representation)) {
+ return false;
+ }
+ return equals(representation);
+ }
+
+ @Override
+ public boolean allowsSingleInstanceOnly() {
+ return true;
+ }
+
+ public int getNameResource() {
+ return mNameResource;
+ }
+
+ public void setNameResource(int nameResource) {
+ mNameResource = nameResource;
+ }
+
+ public int getBitmapResource() {
+ return mBitmapResource;
+ }
+
+ public void setBitmapResource(int bitmapResource) {
+ mBitmapResource = bitmapResource;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterGradRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterGradRepresentation.java
new file mode 100644
index 000000000..0c272d48a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterGradRepresentation.java
@@ -0,0 +1,497 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.graphics.Rect;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorGrad;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.imageshow.Line;
+
+import java.io.IOException;
+import java.util.Vector;
+
+public class FilterGradRepresentation extends FilterRepresentation
+ implements Line {
+ private static final String LOGTAG = "FilterGradRepresentation";
+ public static final int MAX_POINTS = 16;
+ public static final int PARAM_BRIGHTNESS = 0;
+ public static final int PARAM_SATURATION = 1;
+ public static final int PARAM_CONTRAST = 2;
+ private static final double ADD_MIN_DIST = .05;
+ private static String LINE_NAME = "Point";
+ private static final String SERIALIZATION_NAME = "grad";
+
+ public FilterGradRepresentation() {
+ super("Grad");
+ setSerializationName(SERIALIZATION_NAME);
+ creatExample();
+ setOverlayId(R.drawable.filtershow_button_grad);
+ setFilterClass(ImageFilterGrad.class);
+ setTextId(R.string.grad);
+ setEditorId(EditorGrad.ID);
+ }
+
+ public void trimVector(){
+ int n = mBands.size();
+ for (int i = n; i < MAX_POINTS; i++) {
+ mBands.add(new Band());
+ }
+ for (int i = MAX_POINTS; i < n; i++) {
+ mBands.remove(i);
+ }
+ }
+
+ Vector<Band> mBands = new Vector<Band>();
+ Band mCurrentBand;
+
+ static class Band {
+ private boolean mask = true;
+
+ private int xPos1 = -1;
+ private int yPos1 = 100;
+ private int xPos2 = -1;
+ private int yPos2 = 100;
+ private int brightness = 40;
+ private int contrast = 0;
+ private int saturation = 0;
+
+
+ public Band() {
+ }
+
+ public Band(int x, int y) {
+ xPos1 = x;
+ yPos1 = y+30;
+ xPos2 = x;
+ yPos2 = y-30;
+ }
+
+ public Band(Band copy) {
+ mask = copy.mask;
+ xPos1 = copy.xPos1;
+ yPos1 = copy.yPos1;
+ xPos2 = copy.xPos2;
+ yPos2 = copy.yPos2;
+ brightness = copy.brightness;
+ contrast = copy.contrast;
+ saturation = copy.saturation;
+ }
+
+ }
+
+ @Override
+ public String toString() {
+ int count = 0;
+ for (Band point : mBands) {
+ if (!point.mask) {
+ count++;
+ }
+ }
+ return "c=" + mBands.indexOf(mBands) + "[" + mBands.size() + "]" + count;
+ }
+
+ private void creatExample() {
+ Band p = new Band();
+ p.mask = false;
+ p.xPos1 = -1;
+ p.yPos1 = 100;
+ p.xPos2 = -1;
+ p.yPos2 = 100;
+ p.brightness = 40;
+ p.contrast = 0;
+ p.saturation = 0;
+ mBands.add(0, p);
+ mCurrentBand = p;
+ trimVector();
+ }
+
+ @Override
+ public void useParametersFrom(FilterRepresentation a) {
+ FilterGradRepresentation rep = (FilterGradRepresentation) a;
+ Vector<Band> tmpBands = new Vector<Band>();
+ int n = (rep.mCurrentBand == null) ? 0 : rep.mBands.indexOf(rep.mCurrentBand);
+ for (Band band : rep.mBands) {
+ tmpBands.add(new Band(band));
+ }
+ mCurrentBand = null;
+ mBands = tmpBands;
+ mCurrentBand = mBands.elementAt(n);
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ FilterGradRepresentation representation = new FilterGradRepresentation();
+ copyAllParameters(representation);
+ return representation;
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation representation) {
+ if (representation instanceof FilterGradRepresentation) {
+ FilterGradRepresentation rep = (FilterGradRepresentation) representation;
+ int n = getNumberOfBands();
+ if (rep.getNumberOfBands() != n) {
+ return false;
+ }
+ for (int i = 0; i < mBands.size(); i++) {
+ Band b1 = mBands.get(i);
+ Band b2 = rep.mBands.get(i);
+ if (b1.mask != b2.mask
+ || b1.brightness != b2.brightness
+ || b1.contrast != b2.contrast
+ || b1.saturation != b2.saturation
+ || b1.xPos1 != b2.xPos1
+ || b1.xPos2 != b2.xPos2
+ || b1.yPos1 != b2.yPos1
+ || b1.yPos2 != b2.yPos2) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public int getNumberOfBands() {
+ int count = 0;
+ for (Band point : mBands) {
+ if (!point.mask) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public int addBand(Rect rect) {
+ mBands.add(0, mCurrentBand = new Band(rect.centerX(), rect.centerY()));
+ mCurrentBand.mask = false;
+ int x = (mCurrentBand.xPos1 + mCurrentBand.xPos2)/2;
+ int y = (mCurrentBand.yPos1 + mCurrentBand.yPos2)/2;
+ double addDelta = ADD_MIN_DIST * Math.max(rect.width(), rect.height());
+ boolean moved = true;
+ int count = 0;
+ int toMove = mBands.indexOf(mCurrentBand);
+
+ while (moved) {
+ moved = false;
+ count++;
+ if (count > 14) {
+ break;
+ }
+
+ for (Band point : mBands) {
+ if (point.mask) {
+ break;
+ }
+ }
+
+ for (Band point : mBands) {
+ if (point.mask) {
+ break;
+ }
+ int index = mBands.indexOf(point);
+
+ if (toMove != index) {
+ double dist = Math.hypot(point.xPos1 - x, point.yPos1 - y);
+ if (dist < addDelta) {
+ moved = true;
+ mCurrentBand.xPos1 += addDelta;
+ mCurrentBand.yPos1 += addDelta;
+ mCurrentBand.xPos2 += addDelta;
+ mCurrentBand.yPos2 += addDelta;
+ x = (mCurrentBand.xPos1 + mCurrentBand.xPos2)/2;
+ y = (mCurrentBand.yPos1 + mCurrentBand.yPos2)/2;
+
+ if (mCurrentBand.yPos1 > rect.bottom) {
+ mCurrentBand.yPos1 = (int) (rect.top + addDelta);
+ }
+ if (mCurrentBand.xPos1 > rect.right) {
+ mCurrentBand.xPos1 = (int) (rect.left + addDelta);
+ }
+ }
+ }
+ }
+ }
+ trimVector();
+ return 0;
+ }
+
+ public void deleteCurrentBand() {
+ int index = mBands.indexOf(mCurrentBand);
+ mBands.remove(mCurrentBand);
+ trimVector();
+ if (getNumberOfBands() == 0) {
+ addBand(MasterImage.getImage().getOriginalBounds());
+ }
+ mCurrentBand = mBands.get(0);
+ }
+
+ public void nextPoint(){
+ int index = mBands.indexOf(mCurrentBand);
+ int tmp = index;
+ Band point;
+ int k = 0;
+ do {
+ index = (index+1)% mBands.size();
+ point = mBands.get(index);
+ if (k++ >= mBands.size()) {
+ break;
+ }
+ }
+ while (point.mask == true);
+ mCurrentBand = mBands.get(index);
+ }
+
+ public void setSelectedPoint(int pos) {
+ mCurrentBand = mBands.get(pos);
+ }
+
+ public int getSelectedPoint() {
+ return mBands.indexOf(mCurrentBand);
+ }
+
+ public boolean[] getMask() {
+ boolean[] ret = new boolean[mBands.size()];
+ int i = 0;
+ for (Band point : mBands) {
+ ret[i++] = !point.mask;
+ }
+ return ret;
+ }
+
+ public int[] getXPos1() {
+ int[] ret = new int[mBands.size()];
+ int i = 0;
+ for (Band point : mBands) {
+ ret[i++] = point.xPos1;
+ }
+ return ret;
+ }
+
+ public int[] getYPos1() {
+ int[] ret = new int[mBands.size()];
+ int i = 0;
+ for (Band point : mBands) {
+ ret[i++] = point.yPos1;
+ }
+ return ret;
+ }
+
+ public int[] getXPos2() {
+ int[] ret = new int[mBands.size()];
+ int i = 0;
+ for (Band point : mBands) {
+ ret[i++] = point.xPos2;
+ }
+ return ret;
+ }
+
+ public int[] getYPos2() {
+ int[] ret = new int[mBands.size()];
+ int i = 0;
+ for (Band point : mBands) {
+ ret[i++] = point.yPos2;
+ }
+ return ret;
+ }
+
+ public int[] getBrightness() {
+ int[] ret = new int[mBands.size()];
+ int i = 0;
+ for (Band point : mBands) {
+ ret[i++] = point.brightness;
+ }
+ return ret;
+ }
+
+ public int[] getContrast() {
+ int[] ret = new int[mBands.size()];
+ int i = 0;
+ for (Band point : mBands) {
+ ret[i++] = point.contrast;
+ }
+ return ret;
+ }
+
+ public int[] getSaturation() {
+ int[] ret = new int[mBands.size()];
+ int i = 0;
+ for (Band point : mBands) {
+ ret[i++] = point.saturation;
+ }
+ return ret;
+ }
+
+ public int getParameter(int type) {
+ switch (type){
+ case PARAM_BRIGHTNESS:
+ return mCurrentBand.brightness;
+ case PARAM_SATURATION:
+ return mCurrentBand.saturation;
+ case PARAM_CONTRAST:
+ return mCurrentBand.contrast;
+ }
+ throw new IllegalArgumentException("no such type " + type);
+ }
+
+ public int getParameterMax(int type) {
+ switch (type) {
+ case PARAM_BRIGHTNESS:
+ return 100;
+ case PARAM_SATURATION:
+ return 100;
+ case PARAM_CONTRAST:
+ return 100;
+ }
+ throw new IllegalArgumentException("no such type " + type);
+ }
+
+ public int getParameterMin(int type) {
+ switch (type) {
+ case PARAM_BRIGHTNESS:
+ return -100;
+ case PARAM_SATURATION:
+ return -100;
+ case PARAM_CONTRAST:
+ return -100;
+ }
+ throw new IllegalArgumentException("no such type " + type);
+ }
+
+ public void setParameter(int type, int value) {
+ mCurrentBand.mask = false;
+ switch (type) {
+ case PARAM_BRIGHTNESS:
+ mCurrentBand.brightness = value;
+ break;
+ case PARAM_SATURATION:
+ mCurrentBand.saturation = value;
+ break;
+ case PARAM_CONTRAST:
+ mCurrentBand.contrast = value;
+ break;
+ default:
+ throw new IllegalArgumentException("no such type " + type);
+ }
+ }
+
+ @Override
+ public void setPoint1(float x, float y) {
+ mCurrentBand.xPos1 = (int)x;
+ mCurrentBand.yPos1 = (int)y;
+ }
+
+ @Override
+ public void setPoint2(float x, float y) {
+ mCurrentBand.xPos2 = (int)x;
+ mCurrentBand.yPos2 = (int)y;
+ }
+
+ @Override
+ public float getPoint1X() {
+ return mCurrentBand.xPos1;
+ }
+
+ @Override
+ public float getPoint1Y() {
+ return mCurrentBand.yPos1;
+ }
+ @Override
+ public float getPoint2X() {
+ return mCurrentBand.xPos2;
+ }
+
+ @Override
+ public float getPoint2Y() {
+ return mCurrentBand.yPos2;
+ }
+
+ @Override
+ public void serializeRepresentation(JsonWriter writer) throws IOException {
+ writer.beginObject();
+ int len = mBands.size();
+ int count = 0;
+
+ for (int i = 0; i < len; i++) {
+ Band point = mBands.get(i);
+ if (point.mask) {
+ continue;
+ }
+ writer.name(LINE_NAME + count);
+ count++;
+ writer.beginArray();
+ writer.value(point.xPos1);
+ writer.value(point.yPos1);
+ writer.value(point.xPos2);
+ writer.value(point.yPos2);
+ writer.value(point.brightness);
+ writer.value(point.contrast);
+ writer.value(point.saturation);
+ writer.endArray();
+ }
+ writer.endObject();
+ }
+
+ @Override
+ public void deSerializeRepresentation(JsonReader sreader) throws IOException {
+ sreader.beginObject();
+ Vector<Band> points = new Vector<Band>();
+
+ while (sreader.hasNext()) {
+ String name = sreader.nextName();
+ if (name.startsWith(LINE_NAME)) {
+ int pointNo = Integer.parseInt(name.substring(LINE_NAME.length()));
+ sreader.beginArray();
+ Band p = new Band();
+ p.mask = false;
+ sreader.hasNext();
+ p.xPos1 = sreader.nextInt();
+ sreader.hasNext();
+ p.yPos1 = sreader.nextInt();
+ sreader.hasNext();
+ p.xPos2 = sreader.nextInt();
+ sreader.hasNext();
+ p.yPos2 = sreader.nextInt();
+ sreader.hasNext();
+ p.brightness = sreader.nextInt();
+ sreader.hasNext();
+ p.contrast = sreader.nextInt();
+ sreader.hasNext();
+ p.saturation = sreader.nextInt();
+ sreader.hasNext();
+ sreader.endArray();
+ points.add(p);
+
+ } else {
+ sreader.skipValue();
+ }
+ }
+ mBands = points;
+ trimVector();
+ mCurrentBand = mBands.get(0);
+ sreader.endObject();
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterImageBorderRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterImageBorderRepresentation.java
new file mode 100644
index 000000000..f310a2be1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterImageBorderRepresentation.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+
+public class FilterImageBorderRepresentation extends FilterRepresentation {
+ private int mDrawableResource = 0;
+
+ public FilterImageBorderRepresentation(int drawableResource) {
+ super("ImageBorder");
+ setFilterClass(ImageFilterBorder.class);
+ mDrawableResource = drawableResource;
+ setFilterType(FilterRepresentation.TYPE_BORDER);
+ setTextId(R.string.borders);
+ setEditorId(ImageOnlyEditor.ID);
+ setShowParameterValue(false);
+ }
+
+ public String toString() {
+ return "FilterBorder: " + getName();
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ FilterImageBorderRepresentation representation =
+ new FilterImageBorderRepresentation(mDrawableResource);
+ copyAllParameters(representation);
+ return representation;
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ public void useParametersFrom(FilterRepresentation a) {
+ if (a instanceof FilterImageBorderRepresentation) {
+ FilterImageBorderRepresentation representation = (FilterImageBorderRepresentation) a;
+ setName(representation.getName());
+ setDrawableResource(representation.getDrawableResource());
+ }
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation representation) {
+ if (!super.equals(representation)) {
+ return false;
+ }
+ if (representation instanceof FilterImageBorderRepresentation) {
+ FilterImageBorderRepresentation border = (FilterImageBorderRepresentation) representation;
+ if (border.mDrawableResource == mDrawableResource) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int getTextId() {
+ return R.string.none;
+ }
+
+ public boolean allowsSingleInstanceOnly() {
+ return true;
+ }
+
+ public int getDrawableResource() {
+ return mDrawableResource;
+ }
+
+ public void setDrawableResource(int drawableResource) {
+ mDrawableResource = drawableResource;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterMirrorRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterMirrorRepresentation.java
new file mode 100644
index 000000000..8dcff0d16
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterMirrorRepresentation.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorMirror;
+
+import java.io.IOException;
+
+public class FilterMirrorRepresentation extends FilterRepresentation {
+ public static final String SERIALIZATION_NAME = "MIRROR";
+ private static final String SERIALIZATION_MIRROR_VALUE = "value";
+ private static final String TAG = FilterMirrorRepresentation.class.getSimpleName();
+
+ Mirror mMirror;
+
+ public enum Mirror {
+ NONE('N'), VERTICAL('V'), HORIZONTAL('H'), BOTH('B');
+ char mValue;
+
+ private Mirror(char value) {
+ mValue = value;
+ }
+
+ public char value() {
+ return mValue;
+ }
+
+ public static Mirror fromValue(char value) {
+ switch (value) {
+ case 'N':
+ return NONE;
+ case 'V':
+ return VERTICAL;
+ case 'H':
+ return HORIZONTAL;
+ case 'B':
+ return BOTH;
+ default:
+ return null;
+ }
+ }
+ }
+
+ public FilterMirrorRepresentation(Mirror mirror) {
+ super(FilterMirrorRepresentation.class.getSimpleName());
+ setSerializationName(SERIALIZATION_NAME);
+ setShowParameterValue(true);
+ setFilterClass(FilterMirrorRepresentation.class);
+ setFilterType(FilterRepresentation.TYPE_GEOMETRY);
+ setTextId(R.string.mirror);
+ setEditorId(EditorMirror.ID);
+ setMirror(mirror);
+ }
+
+ public FilterMirrorRepresentation(FilterMirrorRepresentation m) {
+ this(m.getMirror());
+ }
+
+ public FilterMirrorRepresentation() {
+ this(getNil());
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation rep) {
+ if (!(rep instanceof FilterMirrorRepresentation)) {
+ return false;
+ }
+ FilterMirrorRepresentation mirror = (FilterMirrorRepresentation) rep;
+ if (mMirror != mirror.mMirror) {
+ return false;
+ }
+ return true;
+ }
+
+ public Mirror getMirror() {
+ return mMirror;
+ }
+
+ public void set(FilterMirrorRepresentation r) {
+ mMirror = r.mMirror;
+ }
+
+ public void setMirror(Mirror mirror) {
+ if (mirror == null) {
+ throw new IllegalArgumentException("Argument to setMirror is null");
+ }
+ mMirror = mirror;
+ }
+
+ public void cycle() {
+ switch (mMirror) {
+ case NONE:
+ mMirror = Mirror.HORIZONTAL;
+ break;
+ case HORIZONTAL:
+ mMirror = Mirror.VERTICAL;
+ break;
+ case VERTICAL:
+ mMirror = Mirror.BOTH;
+ break;
+ case BOTH:
+ mMirror = Mirror.NONE;
+ break;
+ }
+ }
+
+ @Override
+ public boolean allowsSingleInstanceOnly() {
+ return true;
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ return new FilterMirrorRepresentation(this);
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ if (!(representation instanceof FilterMirrorRepresentation)) {
+ throw new IllegalArgumentException("calling copyAllParameters with incompatible types!");
+ }
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ @Override
+ public void useParametersFrom(FilterRepresentation a) {
+ if (!(a instanceof FilterMirrorRepresentation)) {
+ throw new IllegalArgumentException("calling useParametersFrom with incompatible types!");
+ }
+ setMirror(((FilterMirrorRepresentation) a).getMirror());
+ }
+
+ @Override
+ public boolean isNil() {
+ return mMirror == getNil();
+ }
+
+ public static Mirror getNil() {
+ return Mirror.NONE;
+ }
+
+ @Override
+ public void serializeRepresentation(JsonWriter writer) throws IOException {
+ writer.beginObject();
+ writer.name(SERIALIZATION_MIRROR_VALUE).value(mMirror.value());
+ writer.endObject();
+ }
+
+ @Override
+ public void deSerializeRepresentation(JsonReader reader) throws IOException {
+ boolean unset = true;
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if (SERIALIZATION_MIRROR_VALUE.equals(name)) {
+ Mirror r = Mirror.fromValue((char) reader.nextInt());
+ if (r != null) {
+ setMirror(r);
+ unset = false;
+ }
+ } else {
+ reader.skipValue();
+ }
+ }
+ if (unset) {
+ Log.w(TAG, "WARNING: bad value when deserializing " + SERIALIZATION_NAME);
+ }
+ reader.endObject();
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterPoint.java b/src/com/android/gallery3d/filtershow/filters/FilterPoint.java
new file mode 100644
index 000000000..4520717a1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterPoint.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+public interface FilterPoint {
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterPointRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterPointRepresentation.java
new file mode 100644
index 000000000..9bd1699d9
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterPointRepresentation.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import java.util.Vector;
+
+public abstract class FilterPointRepresentation extends FilterRepresentation {
+ private static final String LOGTAG = "FilterPointRepresentation";
+ private Vector<FilterPoint> mCandidates = new Vector<FilterPoint>();
+
+ public FilterPointRepresentation(String type, int textid, int editorID) {
+ super(type);
+ setFilterClass(ImageFilterRedEye.class);
+ setFilterType(FilterRepresentation.TYPE_NORMAL);
+ setTextId(textid);
+ setEditorId(editorID);
+ }
+
+ @Override
+ public abstract FilterRepresentation copy();
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ public boolean hasCandidates() {
+ return mCandidates != null;
+ }
+
+ public Vector<FilterPoint> getCandidates() {
+ return mCandidates;
+ }
+
+ @Override
+ public boolean isNil() {
+ if (getCandidates() != null && getCandidates().size() > 0) {
+ return false;
+ }
+ return true;
+ }
+
+ public Object getCandidate(int index) {
+ return this.mCandidates.get(index);
+ }
+
+ public void addCandidate(FilterPoint c) {
+ this.mCandidates.add(c);
+ }
+
+ @Override
+ public void useParametersFrom(FilterRepresentation a) {
+ if (a instanceof FilterPointRepresentation) {
+ FilterPointRepresentation representation = (FilterPointRepresentation) a;
+ mCandidates.clear();
+ for (FilterPoint redEyeCandidate : representation.mCandidates) {
+ mCandidates.add(redEyeCandidate);
+ }
+ }
+ }
+
+ public void removeCandidate(RedEyeCandidate c) {
+ this.mCandidates.remove(c);
+ }
+
+ public void clearCandidates() {
+ this.mCandidates.clear();
+ }
+
+ public int getNumberOfCandidates() {
+ return mCandidates.size();
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
new file mode 100644
index 000000000..dd06a9760
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterRedEyeRepresentation.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.graphics.RectF;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorRedEye;
+
+import java.util.Vector;
+
+public class FilterRedEyeRepresentation extends FilterPointRepresentation {
+ private static final String LOGTAG = "FilterRedEyeRepresentation";
+
+ public FilterRedEyeRepresentation() {
+ super("RedEye",R.string.redeye,EditorRedEye.ID);
+ setSerializationName("REDEYE");
+ setFilterClass(ImageFilterRedEye.class);
+ setOverlayId(R.drawable.photoeditor_effect_redeye);
+ setOverlayOnly(true);
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ FilterRedEyeRepresentation representation = new FilterRedEyeRepresentation();
+ copyAllParameters(representation);
+ return representation;
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ public void addRect(RectF rect, RectF bounds) {
+ Vector<RedEyeCandidate> intersects = new Vector<RedEyeCandidate>();
+ for (int i = 0; i < getCandidates().size(); i++) {
+ RedEyeCandidate r = (RedEyeCandidate) getCandidate(i);
+ if (r.intersect(rect)) {
+ intersects.add(r);
+ }
+ }
+ for (int i = 0; i < intersects.size(); i++) {
+ RedEyeCandidate r = intersects.elementAt(i);
+ rect.union(r.mRect);
+ bounds.union(r.mBounds);
+ removeCandidate(r);
+ }
+ addCandidate(new RedEyeCandidate(rect, bounds));
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java
new file mode 100644
index 000000000..5b33ffba5
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.gallery3d.filtershow.editors.BasicEditor;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+public class FilterRepresentation {
+ private static final String LOGTAG = "FilterRepresentation";
+ private static final boolean DEBUG = false;
+ private String mName;
+ private int mPriority = TYPE_NORMAL;
+ private Class<?> mFilterClass;
+ private boolean mSupportsPartialRendering = false;
+ private int mTextId = 0;
+ private int mEditorId = BasicEditor.ID;
+ private int mButtonId = 0;
+ private int mOverlayId = 0;
+ private boolean mOverlayOnly = false;
+ private boolean mShowParameterValue = true;
+ private String mSerializationName;
+ public static final byte TYPE_BORDER = 1;
+ public static final byte TYPE_FX = 2;
+ public static final byte TYPE_WBALANCE = 3;
+ public static final byte TYPE_VIGNETTE = 4;
+ public static final byte TYPE_NORMAL = 5;
+ public static final byte TYPE_TINYPLANET = 6;
+ public static final byte TYPE_GEOMETRY = 7;
+ protected static final String NAME_TAG = "Name";
+
+ public FilterRepresentation(String name) {
+ mName = name;
+ }
+
+ public FilterRepresentation copy(){
+ FilterRepresentation representation = new FilterRepresentation(mName);
+ representation.useParametersFrom(this);
+ return representation;
+ }
+
+ protected void copyAllParameters(FilterRepresentation representation) {
+ representation.setName(getName());
+ representation.setFilterClass(getFilterClass());
+ representation.setFilterType(getFilterType());
+ representation.setSupportsPartialRendering(supportsPartialRendering());
+ representation.setTextId(getTextId());
+ representation.setEditorId(getEditorId());
+ representation.setOverlayId(getOverlayId());
+ representation.setOverlayOnly(getOverlayOnly());
+ representation.setShowParameterValue(showParameterValue());
+ representation.mSerializationName = mSerializationName;
+
+ }
+
+ public boolean equals(FilterRepresentation representation) {
+ if (representation == null) {
+ return false;
+ }
+ if (representation.mFilterClass == mFilterClass
+ && representation.mName.equalsIgnoreCase(mName)
+ && representation.mPriority == mPriority
+ // TODO: After we enable partial rendering, we can switch back
+ // to use member variable here.
+ && representation.supportsPartialRendering() == supportsPartialRendering()
+ && representation.mTextId == mTextId
+ && representation.mEditorId == mEditorId
+ && representation.mButtonId == mButtonId
+ && representation.mOverlayId == mOverlayId
+ && representation.mOverlayOnly == mOverlayOnly
+ && representation.mShowParameterValue == mShowParameterValue) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return mName;
+ }
+
+ public void setName(String name) {
+ mName = name;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void setSerializationName(String sname) {
+ mSerializationName = sname;
+ }
+
+ public String getSerializationName() {
+ return mSerializationName;
+ }
+
+ public void setFilterType(int priority) {
+ mPriority = priority;
+ }
+
+ public int getFilterType() {
+ return mPriority;
+ }
+
+ public boolean isNil() {
+ return false;
+ }
+
+ public boolean supportsPartialRendering() {
+ return false && mSupportsPartialRendering; // disable for now
+ }
+
+ public void setSupportsPartialRendering(boolean value) {
+ mSupportsPartialRendering = value;
+ }
+
+ public void useParametersFrom(FilterRepresentation a) {
+ }
+
+ public boolean allowsSingleInstanceOnly() {
+ return false;
+ }
+
+ public Class<?> getFilterClass() {
+ return mFilterClass;
+ }
+
+ public void setFilterClass(Class<?> filterClass) {
+ mFilterClass = filterClass;
+ }
+
+ // This same() function is different from equals(), basically it checks
+ // whether 2 FilterRepresentations are the same type. It doesn't care about
+ // the values.
+ public boolean same(FilterRepresentation b) {
+ if (b == null) {
+ return false;
+ }
+ return getFilterClass() == b.getFilterClass();
+ }
+
+ public int getTextId() {
+ return mTextId;
+ }
+
+ public void setTextId(int textId) {
+ mTextId = textId;
+ }
+
+ public int getOverlayId() {
+ return mOverlayId;
+ }
+
+ public void setOverlayId(int overlayId) {
+ mOverlayId = overlayId;
+ }
+
+ public boolean getOverlayOnly() {
+ return mOverlayOnly;
+ }
+
+ public void setOverlayOnly(boolean value) {
+ mOverlayOnly = value;
+ }
+
+ final public int getEditorId() {
+ return mEditorId;
+ }
+
+ public int[] getEditorIds() {
+ return new int[] {
+ mEditorId };
+ }
+
+ public void setEditorId(int editorId) {
+ mEditorId = editorId;
+ }
+
+ public boolean showParameterValue() {
+ return mShowParameterValue;
+ }
+
+ public void setShowParameterValue(boolean showParameterValue) {
+ mShowParameterValue = showParameterValue;
+ }
+
+ public String getStateRepresentation() {
+ return "";
+ }
+
+ /**
+ * Method must "beginObject()" add its info and "endObject()"
+ * @param writer
+ * @throws IOException
+ */
+ public void serializeRepresentation(JsonWriter writer) throws IOException {
+ writer.beginObject();
+ {
+ String[][] rep = serializeRepresentation();
+ for (int k = 0; k < rep.length; k++) {
+ writer.name(rep[k][0]);
+ writer.value(rep[k][1]);
+ }
+ }
+ writer.endObject();
+ }
+
+ // this is the old way of doing this and will be removed soon
+ public String[][] serializeRepresentation() {
+ String[][] ret = {{NAME_TAG, getName()}};
+ return ret;
+ }
+
+ public void deSerializeRepresentation(JsonReader reader) throws IOException {
+ ArrayList<String[]> al = new ArrayList<String[]>();
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String[] kv = {reader.nextName(), reader.nextString()};
+ al.add(kv);
+
+ }
+ reader.endObject();
+ String[][] oldFormat = al.toArray(new String[al.size()][]);
+
+ deSerializeRepresentation(oldFormat);
+ }
+
+ // this is the old way of doing this and will be removed soon
+ public void deSerializeRepresentation(String[][] rep) {
+ for (int i = 0; i < rep.length; i++) {
+ if (NAME_TAG.equals(rep[i][0])) {
+ mName = rep[i][1];
+ break;
+ }
+ }
+ }
+
+ // Override this in subclasses
+ public int getStyle() {
+ return -1;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRotateRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRotateRepresentation.java
new file mode 100644
index 000000000..eb89de036
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterRotateRepresentation.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorRotate;
+
+import java.io.IOException;
+
+public class FilterRotateRepresentation extends FilterRepresentation {
+ public static final String SERIALIZATION_NAME = "ROTATION";
+ public static final String SERIALIZATION_ROTATE_VALUE = "value";
+ private static final String TAG = FilterRotateRepresentation.class.getSimpleName();
+
+ Rotation mRotation;
+
+ public enum Rotation {
+ ZERO(0), NINETY(90), ONE_EIGHTY(180), TWO_SEVENTY(270);
+ private final int mValue;
+
+ private Rotation(int value) {
+ mValue = value;
+ }
+
+ public int value() {
+ return mValue;
+ }
+
+ public static Rotation fromValue(int value) {
+ switch (value) {
+ case 0:
+ return ZERO;
+ case 90:
+ return NINETY;
+ case 180:
+ return ONE_EIGHTY;
+ case 270:
+ return TWO_SEVENTY;
+ default:
+ return null;
+ }
+ }
+ }
+
+ public FilterRotateRepresentation(Rotation rotation) {
+ super(FilterRotateRepresentation.class.getSimpleName());
+ setSerializationName(SERIALIZATION_NAME);
+ setShowParameterValue(true);
+ setFilterClass(FilterRotateRepresentation.class);
+ setFilterType(FilterRepresentation.TYPE_GEOMETRY);
+ setTextId(R.string.rotate);
+ setEditorId(EditorRotate.ID);
+ setRotation(rotation);
+ }
+
+ public FilterRotateRepresentation(FilterRotateRepresentation r) {
+ this(r.getRotation());
+ }
+
+ public FilterRotateRepresentation() {
+ this(getNil());
+ }
+
+ public Rotation getRotation() {
+ return mRotation;
+ }
+
+ public void rotateCW() {
+ switch(mRotation) {
+ case ZERO:
+ mRotation = Rotation.NINETY;
+ break;
+ case NINETY:
+ mRotation = Rotation.ONE_EIGHTY;
+ break;
+ case ONE_EIGHTY:
+ mRotation = Rotation.TWO_SEVENTY;
+ break;
+ case TWO_SEVENTY:
+ mRotation = Rotation.ZERO;
+ break;
+ }
+ }
+
+ public void set(FilterRotateRepresentation r) {
+ mRotation = r.mRotation;
+ }
+
+ public void setRotation(Rotation rotation) {
+ if (rotation == null) {
+ throw new IllegalArgumentException("Argument to setRotation is null");
+ }
+ mRotation = rotation;
+ }
+
+ @Override
+ public boolean allowsSingleInstanceOnly() {
+ return true;
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ return new FilterRotateRepresentation(this);
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ if (!(representation instanceof FilterRotateRepresentation)) {
+ throw new IllegalArgumentException("calling copyAllParameters with incompatible types!");
+ }
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ @Override
+ public void useParametersFrom(FilterRepresentation a) {
+ if (!(a instanceof FilterRotateRepresentation)) {
+ throw new IllegalArgumentException("calling useParametersFrom with incompatible types!");
+ }
+ setRotation(((FilterRotateRepresentation) a).getRotation());
+ }
+
+ @Override
+ public boolean isNil() {
+ return mRotation == getNil();
+ }
+
+ public static Rotation getNil() {
+ return Rotation.ZERO;
+ }
+
+ @Override
+ public void serializeRepresentation(JsonWriter writer) throws IOException {
+ writer.beginObject();
+ writer.name(SERIALIZATION_ROTATE_VALUE).value(mRotation.value());
+ writer.endObject();
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation rep) {
+ if (!(rep instanceof FilterRotateRepresentation)) {
+ return false;
+ }
+ FilterRotateRepresentation rotate = (FilterRotateRepresentation) rep;
+ if (rotate.mRotation.value() != mRotation.value()) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void deSerializeRepresentation(JsonReader reader) throws IOException {
+ boolean unset = true;
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if (SERIALIZATION_ROTATE_VALUE.equals(name)) {
+ Rotation r = Rotation.fromValue(reader.nextInt());
+ if (r != null) {
+ setRotation(r);
+ unset = false;
+ }
+ } else {
+ reader.skipValue();
+ }
+ }
+ if (unset) {
+ Log.w(TAG, "WARNING: bad value when deserializing " + SERIALIZATION_NAME);
+ }
+ reader.endObject();
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterStraightenRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterStraightenRepresentation.java
new file mode 100644
index 000000000..94c9497fc
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterStraightenRepresentation.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorStraighten;
+
+import java.io.IOException;
+
+public class FilterStraightenRepresentation extends FilterRepresentation {
+ public static final String SERIALIZATION_NAME = "STRAIGHTEN";
+ public static final String SERIALIZATION_STRAIGHTEN_VALUE = "value";
+ private static final String TAG = FilterStraightenRepresentation.class.getSimpleName();
+ public static final int MAX_STRAIGHTEN_ANGLE = 45;
+ public static final int MIN_STRAIGHTEN_ANGLE = -45;
+
+ float mStraighten;
+
+ public FilterStraightenRepresentation(float straighten) {
+ super(FilterStraightenRepresentation.class.getSimpleName());
+ setSerializationName(SERIALIZATION_NAME);
+ setShowParameterValue(true);
+ setFilterClass(FilterStraightenRepresentation.class);
+ setFilterType(FilterRepresentation.TYPE_GEOMETRY);
+ setTextId(R.string.straighten);
+ setEditorId(EditorStraighten.ID);
+ setStraighten(straighten);
+ }
+
+ public FilterStraightenRepresentation(FilterStraightenRepresentation s) {
+ this(s.getStraighten());
+ }
+
+ public FilterStraightenRepresentation() {
+ this(getNil());
+ }
+
+ public void set(FilterStraightenRepresentation r) {
+ mStraighten = r.mStraighten;
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation rep) {
+ if (!(rep instanceof FilterStraightenRepresentation)) {
+ return false;
+ }
+ FilterStraightenRepresentation straighten = (FilterStraightenRepresentation) rep;
+ if (straighten.mStraighten != mStraighten) {
+ return false;
+ }
+ return true;
+ }
+
+ public float getStraighten() {
+ return mStraighten;
+ }
+
+ public void setStraighten(float straighten) {
+ if (!rangeCheck(straighten)) {
+ straighten = Math.min(Math.max(straighten, MIN_STRAIGHTEN_ANGLE), MAX_STRAIGHTEN_ANGLE);
+ }
+ mStraighten = straighten;
+ }
+
+ @Override
+ public boolean allowsSingleInstanceOnly() {
+ return true;
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ return new FilterStraightenRepresentation(this);
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ if (!(representation instanceof FilterStraightenRepresentation)) {
+ throw new IllegalArgumentException("calling copyAllParameters with incompatible types!");
+ }
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ @Override
+ public void useParametersFrom(FilterRepresentation a) {
+ if (!(a instanceof FilterStraightenRepresentation)) {
+ throw new IllegalArgumentException("calling useParametersFrom with incompatible types!");
+ }
+ setStraighten(((FilterStraightenRepresentation) a).getStraighten());
+ }
+
+ @Override
+ public boolean isNil() {
+ return mStraighten == getNil();
+ }
+
+ public static float getNil() {
+ return 0;
+ }
+
+ @Override
+ public void serializeRepresentation(JsonWriter writer) throws IOException {
+ writer.beginObject();
+ writer.name(SERIALIZATION_STRAIGHTEN_VALUE).value(mStraighten);
+ writer.endObject();
+ }
+
+ @Override
+ public void deSerializeRepresentation(JsonReader reader) throws IOException {
+ boolean unset = true;
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+ if (SERIALIZATION_STRAIGHTEN_VALUE.equals(name)) {
+ float s = (float) reader.nextDouble();
+ if (rangeCheck(s)) {
+ setStraighten(s);
+ unset = false;
+ }
+ } else {
+ reader.skipValue();
+ }
+ }
+ if (unset) {
+ Log.w(TAG, "WARNING: bad value when deserializing " + SERIALIZATION_NAME);
+ }
+ reader.endObject();
+ }
+
+ private boolean rangeCheck(double s) {
+ if (s < -45 || s > 45) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java
new file mode 100644
index 000000000..be1812957
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterTinyPlanetRepresentation.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorTinyPlanet;
+
+public class FilterTinyPlanetRepresentation extends FilterBasicRepresentation {
+ private static final String SERIALIZATION_NAME = "TINYPLANET";
+ private static final String LOGTAG = "FilterTinyPlanetRepresentation";
+ private static final String SERIAL_ANGLE = "Angle";
+ private float mAngle = 0;
+
+ public FilterTinyPlanetRepresentation() {
+ super("TinyPlanet", 0, 50, 100);
+ setSerializationName(SERIALIZATION_NAME);
+ setShowParameterValue(true);
+ setFilterClass(ImageFilterTinyPlanet.class);
+ setFilterType(FilterRepresentation.TYPE_TINYPLANET);
+ setTextId(R.string.tinyplanet);
+ setEditorId(EditorTinyPlanet.ID);
+ setMinimum(1);
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ FilterTinyPlanetRepresentation representation = new FilterTinyPlanetRepresentation();
+ copyAllParameters(representation);
+ return representation;
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ @Override
+ public void useParametersFrom(FilterRepresentation a) {
+ FilterTinyPlanetRepresentation representation = (FilterTinyPlanetRepresentation) a;
+ super.useParametersFrom(a);
+ mAngle = representation.mAngle;
+ setZoom(representation.getZoom());
+ }
+
+ public void setAngle(float angle) {
+ mAngle = angle;
+ }
+
+ public float getAngle() {
+ return mAngle;
+ }
+
+ public int getZoom() {
+ return getValue();
+ }
+
+ public void setZoom(int zoom) {
+ setValue(zoom);
+ }
+
+ public boolean isNil() {
+ // TinyPlanet always has an effect
+ return false;
+ }
+
+ @Override
+ public String[][] serializeRepresentation() {
+ String[][] ret = {
+ {SERIAL_NAME , getName() },
+ {SERIAL_VALUE , Integer.toString(getValue())},
+ {SERIAL_ANGLE , Float.toString(mAngle)}};
+ return ret;
+ }
+
+ @Override
+ public void deSerializeRepresentation(String[][] rep) {
+ super.deSerializeRepresentation(rep);
+ for (int i = 0; i < rep.length; i++) {
+ if (SERIAL_VALUE.equals(rep[i][0])) {
+ setValue(Integer.parseInt(rep[i][1]));
+ } else if (SERIAL_ANGLE.equals(rep[i][0])) {
+ setAngle(Float.parseFloat(rep[i][1]));
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterUserPresetRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterUserPresetRepresentation.java
new file mode 100644
index 000000000..dfdb6fcf0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterUserPresetRepresentation.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+public class FilterUserPresetRepresentation extends FilterRepresentation {
+
+ private ImagePreset mPreset;
+ private int mId;
+
+ public FilterUserPresetRepresentation(String name, ImagePreset preset, int id) {
+ super(name);
+ setEditorId(ImageOnlyEditor.ID);
+ setFilterType(FilterRepresentation.TYPE_FX);
+ mPreset = preset;
+ mId = id;
+ }
+
+ public ImagePreset getImagePreset() {
+ return mPreset;
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public FilterRepresentation copy(){
+ FilterRepresentation representation = new FilterUserPresetRepresentation(getName(),
+ new ImagePreset(mPreset), mId);
+ return representation;
+ }
+
+ @Override
+ public boolean allowsSingleInstanceOnly() {
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java
new file mode 100644
index 000000000..42a7406bc
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FilterVignetteRepresentation.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorVignette;
+import com.android.gallery3d.filtershow.imageshow.Oval;
+
+public class FilterVignetteRepresentation extends FilterBasicRepresentation implements Oval {
+ private static final String LOGTAG = "FilterVignetteRepresentation";
+ private float mCenterX = Float.NaN;
+ private float mCenterY;
+ private float mRadiusX = Float.NaN;
+ private float mRadiusY;
+
+ public FilterVignetteRepresentation() {
+ super("Vignette", -100, 50, 100);
+ setSerializationName("VIGNETTE");
+ setShowParameterValue(true);
+ setFilterType(FilterRepresentation.TYPE_VIGNETTE);
+ setTextId(R.string.vignette);
+ setEditorId(EditorVignette.ID);
+ setName("Vignette");
+ setFilterClass(ImageFilterVignette.class);
+ setMinimum(-100);
+ setMaximum(100);
+ setDefaultValue(0);
+ }
+
+ @Override
+ public void useParametersFrom(FilterRepresentation a) {
+ super.useParametersFrom(a);
+ mCenterX = ((FilterVignetteRepresentation) a).mCenterX;
+ mCenterY = ((FilterVignetteRepresentation) a).mCenterY;
+ mRadiusX = ((FilterVignetteRepresentation) a).mRadiusX;
+ mRadiusY = ((FilterVignetteRepresentation) a).mRadiusY;
+ }
+
+ @Override
+ public FilterRepresentation copy() {
+ FilterVignetteRepresentation representation = new FilterVignetteRepresentation();
+ copyAllParameters(representation);
+ return representation;
+ }
+
+ @Override
+ protected void copyAllParameters(FilterRepresentation representation) {
+ super.copyAllParameters(representation);
+ representation.useParametersFrom(this);
+ }
+
+ @Override
+ public void setCenter(float centerX, float centerY) {
+ mCenterX = centerX;
+ mCenterY = centerY;
+ }
+
+ @Override
+ public float getCenterX() {
+ return mCenterX;
+ }
+
+ @Override
+ public float getCenterY() {
+ return mCenterY;
+ }
+
+ @Override
+ public void setRadius(float radiusX, float radiusY) {
+ mRadiusX = radiusX;
+ mRadiusY = radiusY;
+ }
+
+ @Override
+ public void setRadiusX(float radiusX) {
+ mRadiusX = radiusX;
+ }
+
+ @Override
+ public void setRadiusY(float radiusY) {
+ mRadiusY = radiusY;
+ }
+
+ @Override
+ public float getRadiusX() {
+ return mRadiusX;
+ }
+
+ @Override
+ public float getRadiusY() {
+ return mRadiusY;
+ }
+
+ public boolean isCenterSet() {
+ return mCenterX != Float.NaN;
+ }
+
+ @Override
+ public boolean isNil() {
+ return getValue() == 0;
+ }
+
+ @Override
+ public boolean equals(FilterRepresentation representation) {
+ if (!super.equals(representation)) {
+ return false;
+ }
+ if (representation instanceof FilterVignetteRepresentation) {
+ FilterVignetteRepresentation rep = (FilterVignetteRepresentation) representation;
+ if (rep.getCenterX() == getCenterX()
+ && rep.getCenterY() == getCenterY()
+ && rep.getRadiusX() == getRadiusX()
+ && rep.getRadiusY() == getRadiusY()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static final String[] sParams = {
+ "Name", "value", "mCenterX", "mCenterY", "mRadiusX",
+ "mRadiusY"
+ };
+
+ @Override
+ public String[][] serializeRepresentation() {
+ String[][] ret = {
+ { sParams[0], getName() },
+ { sParams[1], Integer.toString(getValue()) },
+ { sParams[2], Float.toString(mCenterX) },
+ { sParams[3], Float.toString(mCenterY) },
+ { sParams[4], Float.toString(mRadiusX) },
+ { sParams[5], Float.toString(mRadiusY) }
+ };
+ return ret;
+ }
+
+ @Override
+ public void deSerializeRepresentation(String[][] rep) {
+ super.deSerializeRepresentation(rep);
+ for (int i = 0; i < rep.length; i++) {
+ String key = rep[i][0];
+ String value = rep[i][1];
+ if (sParams[0].equals(key)) {
+ setName(value);
+ } else if (sParams[1].equals(key)) {
+ setValue(Integer.parseInt(value));
+ } else if (sParams[2].equals(key)) {
+ mCenterX = Float.parseFloat(value);
+ } else if (sParams[3].equals(key)) {
+ mCenterY = Float.parseFloat(value);
+ } else if (sParams[4].equals(key)) {
+ mRadiusX = Float.parseFloat(value);
+ } else if (sParams[5].equals(key)) {
+ mRadiusY = Float.parseFloat(value);
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java b/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java
new file mode 100644
index 000000000..710128f99
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/FiltersManagerInterface.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+public interface FiltersManagerInterface {
+ ImageFilter getFilterForRepresentation(FilterRepresentation representation);
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/IconUtilities.java b/src/com/android/gallery3d/filtershow/filters/IconUtilities.java
new file mode 100644
index 000000000..e2a01472d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/IconUtilities.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import com.android.gallery3d.R;
+
+public class IconUtilities {
+ public static final int PUNCH = R.drawable.filtershow_fx_0005_punch;
+ public static final int VINTAGE = R.drawable.filtershow_fx_0000_vintage;
+ public static final int BW_CONTRAST = R.drawable.filtershow_fx_0004_bw_contrast;
+ public static final int BLEACH = R.drawable.filtershow_fx_0002_bleach;
+ public static final int INSTANT = R.drawable.filtershow_fx_0001_instant;
+ public static final int WASHOUT = R.drawable.filtershow_fx_0007_washout;
+ public static final int BLUECRUSH = R.drawable.filtershow_fx_0003_blue_crush;
+ public static final int WASHOUT_COLOR = R.drawable.filtershow_fx_0008_washout_color;
+ public static final int X_PROCESS = R.drawable.filtershow_fx_0006_x_process;
+
+ public static Bitmap getFXBitmap(Resources res, int id) {
+ Bitmap ret;
+ BitmapFactory.Options o = new BitmapFactory.Options();
+ o.inScaled = false;
+
+ if (id != 0) {
+ return BitmapFactory.decodeResource(res, id, o);
+ }
+ return null;
+ }
+
+ public static Bitmap loadBitmap(Resources res, int resource) {
+
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ Bitmap bitmap = BitmapFactory.decodeResource(
+ res,
+ resource, options);
+
+ return bitmap;
+ }
+
+ public static Bitmap applyFX(Bitmap bitmap, final Bitmap fxBitmap) {
+ ImageFilterFx fx = new ImageFilterFx() {
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ int fxw = fxBitmap.getWidth();
+ int fxh = fxBitmap.getHeight();
+ int start = 0;
+ int end = w * h * 4;
+ nativeApplyFilter(bitmap, w, h, fxBitmap, fxw, fxh, start, end);
+ return bitmap;
+ }
+ };
+ return fx.apply(bitmap, 0, 0);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
new file mode 100644
index 000000000..437137416
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilter.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.support.v8.renderscript.Allocation;
+import android.widget.Toast;
+
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
+
+public abstract class ImageFilter implements Cloneable {
+ private FilterEnvironment mEnvironment = null;
+
+ protected String mName = "Original";
+ private final String LOGTAG = "ImageFilter";
+ protected static final boolean SIMPLE_ICONS = true;
+ // TODO: Temporary, for dogfood note memory issues with toasts for better
+ // feedback. Remove this when filters actually work in low memory
+ // situations.
+ private static Activity sActivity = null;
+
+ public static void setActivityForMemoryToasts(Activity activity) {
+ sActivity = activity;
+ }
+
+ public static void resetStatics() {
+ sActivity = null;
+ }
+
+ public void freeResources() {}
+
+ public void displayLowMemoryToast() {
+ if (sActivity != null) {
+ sActivity.runOnUiThread(new Runnable() {
+ public void run() {
+ Toast.makeText(sActivity, "Memory too low for filter " + getName() +
+ ", please file a bug report", Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+
+ public void setName(String name) {
+ mName = name;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public boolean supportsAllocationInput() { return false; }
+
+ public void apply(Allocation in, Allocation out) {
+ setGeneralParameters();
+ }
+
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ // do nothing here, subclasses will implement filtering here
+ setGeneralParameters();
+ return bitmap;
+ }
+
+ public abstract void useRepresentation(FilterRepresentation representation);
+
+ native protected void nativeApplyGradientFilter(Bitmap bitmap, int w, int h,
+ int[] redGradient, int[] greenGradient, int[] blueGradient);
+
+ public FilterRepresentation getDefaultRepresentation() {
+ return null;
+ }
+
+ protected Matrix getOriginalToScreenMatrix(int w, int h) {
+ return GeometryMathUtils.getImageToScreenMatrix(getEnvironment().getImagePreset()
+ .getGeometryFilters(), true, MasterImage.getImage().getOriginalBounds(), w, h);
+ }
+
+ public void setEnvironment(FilterEnvironment environment) {
+ mEnvironment = environment;
+ }
+
+ public FilterEnvironment getEnvironment() {
+ return mEnvironment;
+ }
+
+ public void setGeneralParameters() {
+ // should implement in subclass which like to transport
+ // some information to other filters. (like the style setting from RetroLux
+ // and Film to FixedFrame)
+ mEnvironment.clearGeneralParameters();
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java
new file mode 100644
index 000000000..a7286f0fa
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterBorder.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+
+import java.util.HashMap;
+
+public class ImageFilterBorder extends ImageFilter {
+ private static final float NINEPATCH_ICON_SCALING = 10;
+ private static final float BITMAP_ICON_SCALING = 1 / 3.0f;
+ private FilterImageBorderRepresentation mParameters = null;
+ private Resources mResources = null;
+
+ private HashMap<Integer, Drawable> mDrawables = new HashMap<Integer, Drawable>();
+
+ public ImageFilterBorder() {
+ mName = "Border";
+ }
+
+ public void useRepresentation(FilterRepresentation representation) {
+ FilterImageBorderRepresentation parameters = (FilterImageBorderRepresentation) representation;
+ mParameters = parameters;
+ }
+
+ public FilterImageBorderRepresentation getParameters() {
+ return mParameters;
+ }
+
+ public void freeResources() {
+ mDrawables.clear();
+ }
+
+ public Bitmap applyHelper(Bitmap bitmap, float scale1, float scale2 ) {
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ Rect bounds = new Rect(0, 0, (int) (w * scale1), (int) (h * scale1));
+ Canvas canvas = new Canvas(bitmap);
+ canvas.scale(scale2, scale2);
+ Drawable drawable = getDrawable(getParameters().getDrawableResource());
+ drawable.setBounds(bounds);
+ drawable.draw(canvas);
+ return bitmap;
+ }
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null || getParameters().getDrawableResource() == 0) {
+ return bitmap;
+ }
+ float scale2 = scaleFactor * 2.0f;
+ float scale1 = 1 / scale2;
+ return applyHelper(bitmap, scale1, scale2);
+ }
+
+ public void setResources(Resources resources) {
+ if (mResources != resources) {
+ mResources = resources;
+ mDrawables.clear();
+ }
+ }
+
+ public Drawable getDrawable(int rsc) {
+ Drawable drawable = mDrawables.get(rsc);
+ if (drawable == null && mResources != null && rsc != 0) {
+ drawable = new BitmapDrawable(mResources, BitmapFactory.decodeResource(mResources, rsc));
+ mDrawables.put(rsc, drawable);
+ }
+ return drawable;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java
new file mode 100644
index 000000000..50837ca2f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterBwFilter.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+
+
+public class ImageFilterBwFilter extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "BWFILTER";
+
+ public ImageFilterBwFilter() {
+ mName = "BW Filter";
+ }
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation();
+ representation.setName("BW Filter");
+ representation.setSerializationName(SERIALIZATION_NAME);
+
+ representation.setFilterClass(ImageFilterBwFilter.class);
+ representation.setMaximum(180);
+ representation.setMinimum(-180);
+ representation.setTextId(R.string.bwfilter);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, int r, int g, int b);
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null) {
+ return bitmap;
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ float[] hsv = new float[] {
+ 180 + getParameters().getValue(), 1, 1
+ };
+ int rgb = Color.HSVToColor(hsv);
+ int r = 0xFF & (rgb >> 16);
+ int g = 0xFF & (rgb >> 8);
+ int b = 0xFF & (rgb >> 0);
+ nativeApplyFilter(bitmap, w, h, r, g, b);
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterChanSat.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterChanSat.java
new file mode 100644
index 000000000..1ea8edfb8
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterChanSat.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.support.v8.renderscript.Allocation;
+import android.support.v8.renderscript.Element;
+import android.support.v8.renderscript.RenderScript;
+import android.support.v8.renderscript.Script.LaunchOptions;
+import android.support.v8.renderscript.Type;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
+
+public class ImageFilterChanSat extends ImageFilterRS {
+ private static final String LOGTAG = "ImageFilterChanSat";
+ private ScriptC_saturation mScript;
+ private Bitmap mSourceBitmap;
+
+ private static final int STRIP_SIZE = 64;
+
+ FilterChanSatRepresentation mParameters = new FilterChanSatRepresentation();
+ private Bitmap mOverlayBitmap;
+
+ public ImageFilterChanSat() {
+ mName = "ChannelSat";
+ }
+
+ @Override
+ public FilterRepresentation getDefaultRepresentation() {
+ return new FilterChanSatRepresentation();
+ }
+
+ @Override
+ public void useRepresentation(FilterRepresentation representation) {
+ mParameters = (FilterChanSatRepresentation) representation;
+ }
+
+ @Override
+ protected void resetAllocations() {
+
+ }
+
+ @Override
+ public void resetScripts() {
+ if (mScript != null) {
+ mScript.destroy();
+ mScript = null;
+ }
+ }
+ @Override
+ protected void createFilter(android.content.res.Resources res, float scaleFactor,
+ int quality) {
+ createFilter(res, scaleFactor, quality, getInPixelsAllocation());
+ }
+
+ @Override
+ protected void createFilter(android.content.res.Resources res, float scaleFactor,
+ int quality, Allocation in) {
+ RenderScript rsCtx = getRenderScriptContext();
+
+ Type.Builder tb_float = new Type.Builder(rsCtx, Element.F32_4(rsCtx));
+ tb_float.setX(in.getType().getX());
+ tb_float.setY(in.getType().getY());
+ mScript = new ScriptC_saturation(rsCtx, res, R.raw.saturation);
+ }
+
+
+ private Bitmap getSourceBitmap() {
+ assert (mSourceBitmap != null);
+ return mSourceBitmap;
+ }
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (SIMPLE_ICONS && FilterEnvironment.QUALITY_ICON == quality) {
+ return bitmap;
+ }
+
+ mSourceBitmap = bitmap;
+ Bitmap ret = super.apply(bitmap, scaleFactor, quality);
+ mSourceBitmap = null;
+
+ return ret;
+ }
+
+ @Override
+ protected void bindScriptValues() {
+ int width = getInPixelsAllocation().getType().getX();
+ int height = getInPixelsAllocation().getType().getY();
+ }
+
+
+
+ @Override
+ protected void runFilter() {
+ int []sat = new int[7];
+ for(int i = 0;i<sat.length ;i ++){
+ sat[i] = mParameters.getValue(i);
+ }
+
+
+ int width = getInPixelsAllocation().getType().getX();
+ int height = getInPixelsAllocation().getType().getY();
+ Matrix m = getOriginalToScreenMatrix(width, height);
+
+
+ mScript.set_saturation(sat);
+
+ mScript.invoke_setupGradParams();
+ runSelectiveAdjust(
+ getInPixelsAllocation(), getOutPixelsAllocation());
+
+ }
+
+ private void runSelectiveAdjust(Allocation in, Allocation out) {
+ int width = in.getType().getX();
+ int height = in.getType().getY();
+
+ LaunchOptions options = new LaunchOptions();
+ int ty;
+ options.setX(0, width);
+
+ for (ty = 0; ty < height; ty += STRIP_SIZE) {
+ int endy = ty + STRIP_SIZE;
+ if (endy > height) {
+ endy = height;
+ }
+ options.setY(ty, endy);
+ mScript.forEach_selectiveAdjust(in, out, options);
+ if (checkStop()) {
+ return;
+ }
+ }
+ }
+
+ private boolean checkStop() {
+ RenderScript rsCtx = getRenderScriptContext();
+ rsCtx.finish();
+ if (getEnvironment().needsStop()) {
+ return true;
+ }
+ return false;
+ }
+}
+
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java
new file mode 100644
index 000000000..27c0e0877
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterContrast.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterContrast extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "CONTRAST";
+
+ public ImageFilterContrast() {
+ mName = "Contrast";
+ }
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterBasicRepresentation representation =
+ (FilterBasicRepresentation) super.getDefaultRepresentation();
+ representation.setName("Contrast");
+ representation.setSerializationName(SERIALIZATION_NAME);
+
+ representation.setFilterClass(ImageFilterContrast.class);
+ representation.setTextId(R.string.contrast);
+ representation.setMinimum(-100);
+ representation.setMaximum(100);
+ representation.setDefaultValue(0);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float strength);
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null) {
+ return bitmap;
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ float value = getParameters().getValue();
+ nativeApplyFilter(bitmap, w, h, value);
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java
new file mode 100644
index 000000000..61b60d2e3
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterCurves.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import android.graphics.Bitmap;
+
+import com.android.gallery3d.filtershow.imageshow.Spline;
+
+public class ImageFilterCurves extends ImageFilter {
+
+ private static final String LOGTAG = "ImageFilterCurves";
+ FilterCurvesRepresentation mParameters = new FilterCurvesRepresentation();
+
+ @Override
+ public FilterRepresentation getDefaultRepresentation() {
+ return new FilterCurvesRepresentation();
+ }
+
+ @Override
+ public void useRepresentation(FilterRepresentation representation) {
+ FilterCurvesRepresentation parameters = (FilterCurvesRepresentation) representation;
+ mParameters = parameters;
+ }
+
+ public ImageFilterCurves() {
+ mName = "Curves";
+ reset();
+ }
+
+ public void populateArray(int[] array, int curveIndex) {
+ Spline spline = mParameters.getSpline(curveIndex);
+ if (spline == null) {
+ return;
+ }
+ float[] curve = spline.getAppliedCurve();
+ for (int i = 0; i < 256; i++) {
+ array[i] = (int) (curve[i] * 255);
+ }
+ }
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (!mParameters.getSpline(Spline.RGB).isOriginal()) {
+ int[] rgbGradient = new int[256];
+ populateArray(rgbGradient, Spline.RGB);
+ nativeApplyGradientFilter(bitmap, bitmap.getWidth(), bitmap.getHeight(),
+ rgbGradient, rgbGradient, rgbGradient);
+ }
+
+ int[] redGradient = null;
+ if (!mParameters.getSpline(Spline.RED).isOriginal()) {
+ redGradient = new int[256];
+ populateArray(redGradient, Spline.RED);
+ }
+ int[] greenGradient = null;
+ if (!mParameters.getSpline(Spline.GREEN).isOriginal()) {
+ greenGradient = new int[256];
+ populateArray(greenGradient, Spline.GREEN);
+ }
+ int[] blueGradient = null;
+ if (!mParameters.getSpline(Spline.BLUE).isOriginal()) {
+ blueGradient = new int[256];
+ populateArray(blueGradient, Spline.BLUE);
+ }
+
+ nativeApplyGradientFilter(bitmap, bitmap.getWidth(), bitmap.getHeight(),
+ redGradient, greenGradient, blueGradient);
+ return bitmap;
+ }
+
+ public void setSpline(Spline spline, int splineIndex) {
+ mParameters.setSpline(splineIndex, new Spline(spline));
+ }
+
+ public Spline getSpline(int splineIndex) {
+ return mParameters.getSpline(splineIndex);
+ }
+
+ public void reset() {
+ Spline spline = new Spline();
+
+ spline.addPoint(0.0f, 1.0f);
+ spline.addPoint(1.0f, 0.0f);
+
+ for (int i = 0; i < 4; i++) {
+ mParameters.setSpline(i, new Spline(spline));
+ }
+ }
+
+ public void useFilter(ImageFilter a) {
+ ImageFilterCurves c = (ImageFilterCurves) a;
+ for (int i = 0; i < 4; i++) {
+ if (c.mParameters.getSpline(i) != null) {
+ setSpline(c.mParameters.getSpline(i), i);
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java
new file mode 100644
index 000000000..efb9cde71
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDownsample.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class ImageFilterDownsample extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "DOWNSAMPLE";
+ private static final int ICON_DOWNSAMPLE_FRACTION = 8;
+ private ImageLoader mImageLoader;
+
+ public ImageFilterDownsample(ImageLoader loader) {
+ mName = "Downsample";
+ mImageLoader = loader;
+ }
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation();
+ representation.setName("Downsample");
+ representation.setSerializationName(SERIALIZATION_NAME);
+
+ representation.setFilterClass(ImageFilterDownsample.class);
+ representation.setMaximum(100);
+ representation.setMinimum(1);
+ representation.setValue(50);
+ representation.setDefaultValue(50);
+ representation.setPreviewValue(3);
+ representation.setTextId(R.string.downsample);
+ return representation;
+ }
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null) {
+ return bitmap;
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ int p = getParameters().getValue();
+
+ // size of original precached image
+ Rect size = MasterImage.getImage().getOriginalBounds();
+ int orig_w = size.width();
+ int orig_h = size.height();
+
+ if (p > 0 && p < 100) {
+ // scale preview to same size as the resulting bitmap from a "save"
+ int newWidth = orig_w * p / 100;
+ int newHeight = orig_h * p / 100;
+
+ // only scale preview if preview isn't already scaled enough
+ if (newWidth <= 0 || newHeight <= 0 || newWidth >= w || newHeight >= h) {
+ return bitmap;
+ }
+ Bitmap ret = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
+ if (ret != bitmap) {
+ bitmap.recycle();
+ }
+ return ret;
+ }
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java
new file mode 100644
index 000000000..7df5ffb64
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation.StrokeData;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
+
+import java.util.Vector;
+
+public class ImageFilterDraw extends ImageFilter {
+ private static final String LOGTAG = "ImageFilterDraw";
+ public final static byte SIMPLE_STYLE = 0;
+ public final static byte BRUSH_STYLE_SPATTER = 1;
+ public final static byte BRUSH_STYLE_MARKER = 2;
+ public final static int NUMBER_OF_STYLES = 3;
+ Bitmap mOverlayBitmap; // this accelerates interaction
+ int mCachedStrokes = -1;
+ int mCurrentStyle = 0;
+
+ FilterDrawRepresentation mParameters = new FilterDrawRepresentation();
+
+ public ImageFilterDraw() {
+ mName = "Image Draw";
+ }
+
+ DrawStyle[] mDrawingsTypes = new DrawStyle[] {
+ new SimpleDraw(),
+ new Brush(R.drawable.brush_marker),
+ new Brush(R.drawable.brush_spatter)
+ };
+ {
+ for (int i = 0; i < mDrawingsTypes.length; i++) {
+ mDrawingsTypes[i].setType((byte) i);
+ }
+
+ }
+
+ @Override
+ public FilterRepresentation getDefaultRepresentation() {
+ return new FilterDrawRepresentation();
+ }
+
+ @Override
+ public void useRepresentation(FilterRepresentation representation) {
+ FilterDrawRepresentation parameters = (FilterDrawRepresentation) representation;
+ mParameters = parameters;
+ }
+
+ public void setStyle(byte style) {
+ mCurrentStyle = style % mDrawingsTypes.length;
+ }
+
+ public int getStyle() {
+ return mCurrentStyle;
+ }
+
+ public static interface DrawStyle {
+ public void setType(byte type);
+ public void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas, Matrix toScrMatrix,
+ int quality);
+ }
+
+ class SimpleDraw implements DrawStyle {
+ byte mType;
+
+ @Override
+ public void setType(byte type) {
+ mType = type;
+ }
+
+ @Override
+ public void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas, Matrix toScrMatrix,
+ int quality) {
+ if (sd == null) {
+ return;
+ }
+ if (sd.mPath == null) {
+ return;
+ }
+ Paint paint = new Paint();
+
+ paint.setStyle(Style.STROKE);
+ paint.setColor(sd.mColor);
+ paint.setStrokeWidth(toScrMatrix.mapRadius(sd.mRadius));
+
+ // done this way because of a bug in path.transform(matrix)
+ Path mCacheTransPath = new Path();
+ mCacheTransPath.addPath(sd.mPath, toScrMatrix);
+
+ canvas.drawPath(mCacheTransPath, paint);
+ }
+ }
+
+ class Brush implements DrawStyle {
+ int mBrushID;
+ Bitmap mBrush;
+ byte mType;
+
+ public Brush(int brushID) {
+ mBrushID = brushID;
+ }
+
+ public Bitmap getBrush() {
+ if (mBrush == null) {
+ BitmapFactory.Options opt = new BitmapFactory.Options();
+ opt.inPreferredConfig = Bitmap.Config.ALPHA_8;
+ mBrush = BitmapFactory.decodeResource(MasterImage.getImage().getActivity()
+ .getResources(), mBrushID, opt);
+ mBrush = mBrush.extractAlpha();
+ }
+ return mBrush;
+ }
+
+ @Override
+ public void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas,
+ Matrix toScrMatrix,
+ int quality) {
+ if (sd == null || sd.mPath == null) {
+ return;
+ }
+ Paint paint = new Paint();
+ paint.setStyle(Style.STROKE);
+ paint.setAntiAlias(true);
+ Path mCacheTransPath = new Path();
+ mCacheTransPath.addPath(sd.mPath, toScrMatrix);
+ draw(canvas, paint, sd.mColor, toScrMatrix.mapRadius(sd.mRadius) * 2,
+ mCacheTransPath);
+ }
+
+ public Bitmap createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)
+ {
+ Matrix m = new Matrix();
+ m.setScale(dstWidth / (float) src.getWidth(), dstHeight / (float) src.getHeight());
+ Bitmap result = Bitmap.createBitmap(dstWidth, dstHeight, src.getConfig());
+ Canvas canvas = new Canvas(result);
+
+ Paint paint = new Paint();
+ paint.setFilterBitmap(filter);
+ canvas.drawBitmap(src, m, paint);
+
+ return result;
+
+ }
+ void draw(Canvas canvas, Paint paint, int color, float size, Path path) {
+ PathMeasure mPathMeasure = new PathMeasure();
+ float[] mPosition = new float[2];
+ float[] mTan = new float[2];
+
+ mPathMeasure.setPath(path, false);
+
+ paint.setAntiAlias(true);
+ paint.setColor(color);
+
+ paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY));
+ Bitmap brush;
+ // done this way because of a bug in
+ // Bitmap.createScaledBitmap(getBrush(),(int) size,(int) size,true);
+ brush = createScaledBitmap(getBrush(), (int) size, (int) size, true);
+ float len = mPathMeasure.getLength();
+ float s2 = size / 2;
+ float step = s2 / 8;
+ for (float i = 0; i < len; i += step) {
+ mPathMeasure.getPosTan(i, mPosition, mTan);
+ // canvas.drawCircle(pos[0], pos[1], size, paint);
+ canvas.drawBitmap(brush, mPosition[0] - s2, mPosition[1] - s2, paint);
+ }
+ }
+
+ @Override
+ public void setType(byte type) {
+ mType = type;
+ }
+ }
+
+ void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas, Matrix toScrMatrix,
+ int quality) {
+ mDrawingsTypes[sd.mType].paint(sd, canvas, toScrMatrix, quality);
+ }
+
+ public void drawData(Canvas canvas, Matrix originalRotateToScreen, int quality) {
+ Paint paint = new Paint();
+ if (quality == FilterEnvironment.QUALITY_FINAL) {
+ paint.setAntiAlias(true);
+ }
+ paint.setStyle(Style.STROKE);
+ paint.setColor(Color.RED);
+ paint.setStrokeWidth(40);
+
+ if (mParameters.getDrawing().isEmpty() && mParameters.getCurrentDrawing() == null) {
+ return;
+ }
+ if (quality == FilterEnvironment.QUALITY_FINAL) {
+ for (FilterDrawRepresentation.StrokeData strokeData : mParameters.getDrawing()) {
+ paint(strokeData, canvas, originalRotateToScreen, quality);
+ }
+ return;
+ }
+
+ if (mOverlayBitmap == null ||
+ mOverlayBitmap.getWidth() != canvas.getWidth() ||
+ mOverlayBitmap.getHeight() != canvas.getHeight() ||
+ mParameters.getDrawing().size() < mCachedStrokes) {
+
+ mOverlayBitmap = Bitmap.createBitmap(
+ canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);
+ mCachedStrokes = 0;
+ }
+
+ if (mCachedStrokes < mParameters.getDrawing().size()) {
+ fillBuffer(originalRotateToScreen);
+ }
+ canvas.drawBitmap(mOverlayBitmap, 0, 0, paint);
+
+ StrokeData stroke = mParameters.getCurrentDrawing();
+ if (stroke != null) {
+ paint(stroke, canvas, originalRotateToScreen, quality);
+ }
+ }
+
+ public void fillBuffer(Matrix originalRotateToScreen) {
+ Canvas drawCache = new Canvas(mOverlayBitmap);
+ Vector<FilterDrawRepresentation.StrokeData> v = mParameters.getDrawing();
+ int n = v.size();
+
+ for (int i = mCachedStrokes; i < n; i++) {
+ paint(v.get(i), drawCache, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW);
+ }
+ mCachedStrokes = n;
+ }
+
+ public void draw(Canvas canvas, Matrix originalRotateToScreen) {
+ for (FilterDrawRepresentation.StrokeData strokeData : mParameters.getDrawing()) {
+ paint(strokeData, canvas, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW);
+ }
+ mDrawingsTypes[mCurrentStyle].paint(
+ null, canvas, originalRotateToScreen, FilterEnvironment.QUALITY_PREVIEW);
+ }
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+
+ Matrix m = getOriginalToScreenMatrix(w, h);
+ drawData(new Canvas(bitmap), m, quality);
+ return bitmap;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java
new file mode 100644
index 000000000..2d0d7653d
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterEdge.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import android.graphics.Bitmap;
+
+import com.android.gallery3d.R;
+
+public class ImageFilterEdge extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "EDGE";
+ public ImageFilterEdge() {
+ mName = "Edge";
+ }
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterRepresentation representation = super.getDefaultRepresentation();
+ representation.setName("Edge");
+ representation.setSerializationName(SERIALIZATION_NAME);
+ representation.setFilterClass(ImageFilterEdge.class);
+ representation.setTextId(R.string.edge);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float p);
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null) {
+ return bitmap;
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ float p = getParameters().getValue() + 101;
+ p = (float) p / 100;
+ nativeApplyFilter(bitmap, w, h, p);
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java
new file mode 100644
index 000000000..69eab7330
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterExposure.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterExposure extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "EXPOSURE";
+ public ImageFilterExposure() {
+ mName = "Exposure";
+ }
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterBasicRepresentation representation =
+ (FilterBasicRepresentation) super.getDefaultRepresentation();
+ representation.setName("Exposure");
+ representation.setSerializationName(SERIALIZATION_NAME);
+ representation.setFilterClass(ImageFilterExposure.class);
+ representation.setTextId(R.string.exposure);
+ representation.setMinimum(-100);
+ representation.setMaximum(100);
+ representation.setDefaultValue(0);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float bright);
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null) {
+ return bitmap;
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ float value = getParameters().getValue();
+ nativeApplyFilter(bitmap, w, h, value);
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
new file mode 100644
index 000000000..19bea593b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterFx.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import com.android.gallery3d.app.Log;
+
+public class ImageFilterFx extends ImageFilter {
+ private static final String LOGTAG = "ImageFilterFx";
+ private FilterFxRepresentation mParameters = null;
+ private Bitmap mFxBitmap = null;
+ private Resources mResources = null;
+ private int mFxBitmapId = 0;
+
+ public ImageFilterFx() {
+ }
+
+ @Override
+ public void freeResources() {
+ if (mFxBitmap != null) mFxBitmap.recycle();
+ mFxBitmap = null;
+ }
+
+ @Override
+ public FilterRepresentation getDefaultRepresentation() {
+ return null;
+ }
+
+ public void useRepresentation(FilterRepresentation representation) {
+ FilterFxRepresentation parameters = (FilterFxRepresentation) representation;
+ mParameters = parameters;
+ }
+
+ public FilterFxRepresentation getParameters() {
+ return mParameters;
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h,
+ Bitmap fxBitmap, int fxw, int fxh,
+ int start, int end);
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null || mResources == null) {
+ return bitmap;
+ }
+
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+
+ int bitmapResourceId = getParameters().getBitmapResource();
+ if (bitmapResourceId == 0) { // null filter fx
+ return bitmap;
+ }
+
+ if (mFxBitmap == null || mFxBitmapId != bitmapResourceId) {
+ BitmapFactory.Options o = new BitmapFactory.Options();
+ o.inScaled = false;
+ mFxBitmapId = bitmapResourceId;
+ if (mFxBitmapId != 0) {
+ mFxBitmap = BitmapFactory.decodeResource(mResources, mFxBitmapId, o);
+ } else {
+ Log.w(LOGTAG, "bad resource for filter: " + mName);
+ }
+ }
+
+ if (mFxBitmap == null) {
+ return bitmap;
+ }
+
+ int fxw = mFxBitmap.getWidth();
+ int fxh = mFxBitmap.getHeight();
+
+ int stride = w * 4;
+ int max = stride * h;
+ int increment = stride * 256; // 256 lines
+ for (int i = 0; i < max; i += increment) {
+ int start = i;
+ int end = i + increment;
+ if (end > max) {
+ end = max;
+ }
+ if (!getEnvironment().needsStop()) {
+ nativeApplyFilter(bitmap, w, h, mFxBitmap, fxw, fxh, start, end);
+ }
+ }
+
+ return bitmap;
+ }
+
+ public void setResources(Resources resources) {
+ mResources = resources;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterGrad.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterGrad.java
new file mode 100644
index 000000000..cbdfaa623
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterGrad.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Matrix;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.support.v8.renderscript.Allocation;
+import android.support.v8.renderscript.Element;
+import android.support.v8.renderscript.RenderScript;
+import android.support.v8.renderscript.Script.LaunchOptions;
+import android.support.v8.renderscript.Type;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
+
+public class ImageFilterGrad extends ImageFilterRS {
+ private static final String LOGTAG = "ImageFilterGrad";
+ private ScriptC_grad mScript;
+ private Bitmap mSourceBitmap;
+ private static final int RADIUS_SCALE_FACTOR = 160;
+
+ private static final int STRIP_SIZE = 64;
+
+ FilterGradRepresentation mParameters = new FilterGradRepresentation();
+ private Bitmap mOverlayBitmap;
+
+ public ImageFilterGrad() {
+ mName = "grad";
+ }
+
+ @Override
+ public FilterRepresentation getDefaultRepresentation() {
+ return new FilterGradRepresentation();
+ }
+
+ @Override
+ public void useRepresentation(FilterRepresentation representation) {
+ mParameters = (FilterGradRepresentation) representation;
+ }
+
+ @Override
+ protected void resetAllocations() {
+
+ }
+
+ @Override
+ public void resetScripts() {
+ if (mScript != null) {
+ mScript.destroy();
+ mScript = null;
+ }
+ }
+ @Override
+ protected void createFilter(android.content.res.Resources res, float scaleFactor,
+ int quality) {
+ createFilter(res, scaleFactor, quality, getInPixelsAllocation());
+ }
+
+ @Override
+ protected void createFilter(android.content.res.Resources res, float scaleFactor,
+ int quality, Allocation in) {
+ RenderScript rsCtx = getRenderScriptContext();
+
+ Type.Builder tb_float = new Type.Builder(rsCtx, Element.F32_4(rsCtx));
+ tb_float.setX(in.getType().getX());
+ tb_float.setY(in.getType().getY());
+ mScript = new ScriptC_grad(rsCtx, res, R.raw.grad);
+ }
+
+
+ private Bitmap getSourceBitmap() {
+ assert (mSourceBitmap != null);
+ return mSourceBitmap;
+ }
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (SIMPLE_ICONS && FilterEnvironment.QUALITY_ICON == quality) {
+ return bitmap;
+ }
+
+ mSourceBitmap = bitmap;
+ Bitmap ret = super.apply(bitmap, scaleFactor, quality);
+ mSourceBitmap = null;
+
+ return ret;
+ }
+
+ @Override
+ protected void bindScriptValues() {
+ int width = getInPixelsAllocation().getType().getX();
+ int height = getInPixelsAllocation().getType().getY();
+ mScript.set_inputWidth(width);
+ mScript.set_inputHeight(height);
+ }
+
+ @Override
+ protected void runFilter() {
+ int[] x1 = mParameters.getXPos1();
+ int[] y1 = mParameters.getYPos1();
+ int[] x2 = mParameters.getXPos2();
+ int[] y2 = mParameters.getYPos2();
+
+ int width = getInPixelsAllocation().getType().getX();
+ int height = getInPixelsAllocation().getType().getY();
+ Matrix m = getOriginalToScreenMatrix(width, height);
+ float[] coord = new float[2];
+ for (int i = 0; i < x1.length; i++) {
+ coord[0] = x1[i];
+ coord[1] = y1[i];
+ m.mapPoints(coord);
+ x1[i] = (int) coord[0];
+ y1[i] = (int) coord[1];
+ coord[0] = x2[i];
+ coord[1] = y2[i];
+ m.mapPoints(coord);
+ x2[i] = (int) coord[0];
+ y2[i] = (int) coord[1];
+ }
+
+ mScript.set_mask(mParameters.getMask());
+ mScript.set_xPos1(x1);
+ mScript.set_yPos1(y1);
+ mScript.set_xPos2(x2);
+ mScript.set_yPos2(y2);
+
+ mScript.set_brightness(mParameters.getBrightness());
+ mScript.set_contrast(mParameters.getContrast());
+ mScript.set_saturation(mParameters.getSaturation());
+
+ mScript.invoke_setupGradParams();
+ runSelectiveAdjust(
+ getInPixelsAllocation(), getOutPixelsAllocation());
+
+ }
+
+ private void runSelectiveAdjust(Allocation in, Allocation out) {
+ int width = in.getType().getX();
+ int height = in.getType().getY();
+
+ LaunchOptions options = new LaunchOptions();
+ int ty;
+ options.setX(0, width);
+
+ for (ty = 0; ty < height; ty += STRIP_SIZE) {
+ int endy = ty + STRIP_SIZE;
+ if (endy > height) {
+ endy = height;
+ }
+ options.setY(ty, endy);
+ mScript.forEach_selectiveAdjust(in, out, options);
+ if (checkStop()) {
+ return;
+ }
+ }
+ }
+
+ private boolean checkStop() {
+ RenderScript rsCtx = getRenderScriptContext();
+ rsCtx.finish();
+ if (getEnvironment().needsStop()) {
+ return true;
+ }
+ return false;
+ }
+}
+
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java
new file mode 100644
index 000000000..4c837e0bf
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHighlights.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.graphics.Bitmap;
+
+import com.android.gallery3d.R;
+
+public class ImageFilterHighlights extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "HIGHLIGHTS";
+ private static final String LOGTAG = "ImageFilterVignette";
+
+ public ImageFilterHighlights() {
+ mName = "Highlights";
+ }
+
+ SplineMath mSpline = new SplineMath(5);
+ double[] mHighlightCurve = { 0.0, 0.32, 0.418, 0.476, 0.642 };
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterBasicRepresentation representation =
+ (FilterBasicRepresentation) super.getDefaultRepresentation();
+ representation.setName("Highlights");
+ representation.setSerializationName(SERIALIZATION_NAME);
+ representation.setFilterClass(ImageFilterHighlights.class);
+ representation.setTextId(R.string.highlight_recovery);
+ representation.setMinimum(-100);
+ representation.setMaximum(100);
+ representation.setDefaultValue(0);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float[] luminanceMap);
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null) {
+ return bitmap;
+ }
+ float p = getParameters().getValue();
+ double t = p/100.;
+ for (int i = 0; i < 5; i++) {
+ double x = i / 4.;
+ double y = mHighlightCurve[i] *t+x*(1-t);
+ mSpline.setPoint(i, x, y);
+ }
+
+ float[][] curve = mSpline.calculatetCurve(256);
+ float[] luminanceMap = new float[curve.length];
+ for (int i = 0; i < luminanceMap.length; i++) {
+ luminanceMap[i] = curve[i][1];
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+
+ nativeApplyFilter(bitmap, w, h, luminanceMap);
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java
new file mode 100644
index 000000000..b87c25490
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterHue.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.BasicEditor;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterHue extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "HUE";
+ private ColorSpaceMatrix cmatrix = null;
+
+ public ImageFilterHue() {
+ mName = "Hue";
+ cmatrix = new ColorSpaceMatrix();
+ }
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterBasicRepresentation representation =
+ (FilterBasicRepresentation) super.getDefaultRepresentation();
+ representation.setName("Hue");
+ representation.setSerializationName(SERIALIZATION_NAME);
+ representation.setFilterClass(ImageFilterHue.class);
+ representation.setMinimum(-180);
+ representation.setMaximum(180);
+ representation.setTextId(R.string.hue);
+ representation.setEditorId(BasicEditor.ID);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float []matrix);
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null) {
+ return bitmap;
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ float value = getParameters().getValue();
+ cmatrix.identity();
+ cmatrix.setHue(value);
+
+ nativeApplyFilter(bitmap, w, h, cmatrix.getMatrix());
+
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java
new file mode 100644
index 000000000..77cdf47b3
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterKMeans.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.text.format.Time;
+
+import com.android.gallery3d.R;
+
+public class ImageFilterKMeans extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "KMEANS";
+ private int mSeed = 0;
+
+ public ImageFilterKMeans() {
+ mName = "KMeans";
+
+ // set random seed for session
+ Time t = new Time();
+ t.setToNow();
+ mSeed = (int) t.toMillis(false);
+ }
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterBasicRepresentation representation = (FilterBasicRepresentation) super.getDefaultRepresentation();
+ representation.setName("KMeans");
+ representation.setSerializationName(SERIALIZATION_NAME);
+ representation.setFilterClass(ImageFilterKMeans.class);
+ representation.setMaximum(20);
+ representation.setMinimum(2);
+ representation.setValue(4);
+ representation.setDefaultValue(4);
+ representation.setPreviewValue(4);
+ representation.setTextId(R.string.kmeans);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int width, int height,
+ Bitmap large_ds_bm, int lwidth, int lheight, Bitmap small_ds_bm,
+ int swidth, int sheight, int p, int seed);
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null) {
+ return bitmap;
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+
+ Bitmap large_bm_ds = bitmap;
+ Bitmap small_bm_ds = bitmap;
+
+ // find width/height for larger downsampled bitmap
+ int lw = w;
+ int lh = h;
+ while (lw > 256 && lh > 256) {
+ lw /= 2;
+ lh /= 2;
+ }
+ if (lw != w) {
+ large_bm_ds = Bitmap.createScaledBitmap(bitmap, lw, lh, true);
+ }
+
+ // find width/height for smaller downsampled bitmap
+ int sw = lw;
+ int sh = lh;
+ while (sw > 64 && sh > 64) {
+ sw /= 2;
+ sh /= 2;
+ }
+ if (sw != lw) {
+ small_bm_ds = Bitmap.createScaledBitmap(large_bm_ds, sw, sh, true);
+ }
+
+ if (getParameters() != null) {
+ int p = Math.max(getParameters().getValue(), getParameters().getMinimum()) % (getParameters().getMaximum() + 1);
+ nativeApplyFilter(bitmap, w, h, large_bm_ds, lw, lh, small_bm_ds, sw, sh, p, mSeed);
+ }
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java
new file mode 100644
index 000000000..98497596b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterNegative.java
@@ -0,0 +1,39 @@
+package com.android.gallery3d.filtershow.filters;
+
+import android.graphics.Bitmap;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+
+public class ImageFilterNegative extends ImageFilter {
+ private static final String SERIALIZATION_NAME = "NEGATIVE";
+ public ImageFilterNegative() {
+ mName = "Negative";
+ }
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterRepresentation representation = new FilterDirectRepresentation("Negative");
+ representation.setSerializationName(SERIALIZATION_NAME);
+ representation.setFilterClass(ImageFilterNegative.class);
+ representation.setTextId(R.string.negative);
+ representation.setShowParameterValue(false);
+ representation.setEditorId(ImageOnlyEditor.ID);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h);
+
+ @Override
+ public void useRepresentation(FilterRepresentation representation) {
+
+ }
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ nativeApplyFilter(bitmap, w, h);
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java
new file mode 100644
index 000000000..25e5d1476
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterParametricBorder.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+
+public class ImageFilterParametricBorder extends ImageFilter {
+ private FilterColorBorderRepresentation mParameters = null;
+
+ public ImageFilterParametricBorder() {
+ mName = "Border";
+ }
+
+ public void useRepresentation(FilterRepresentation representation) {
+ FilterColorBorderRepresentation parameters = (FilterColorBorderRepresentation) representation;
+ mParameters = parameters;
+ }
+
+ public FilterColorBorderRepresentation getParameters() {
+ return mParameters;
+ }
+
+ private void applyHelper(Canvas canvas, int w, int h) {
+ if (getParameters() == null) {
+ return;
+ }
+ Path border = new Path();
+ border.moveTo(0, 0);
+ float bs = getParameters().getBorderSize() / 100.0f * w;
+ float r = getParameters().getBorderRadius() / 100.0f * w;
+ border.lineTo(0, h);
+ border.lineTo(w, h);
+ border.lineTo(w, 0);
+ border.lineTo(0, 0);
+ border.addRoundRect(new RectF(bs, bs, w - bs, h - bs),
+ r, r, Path.Direction.CW);
+
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setColor(getParameters().getColor());
+ canvas.drawPath(border, paint);
+ }
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ Canvas canvas = new Canvas(bitmap);
+ applyHelper(canvas, bitmap.getWidth(), bitmap.getHeight());
+ return bitmap;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
new file mode 100644
index 000000000..5695ef53e
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterRS.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.support.v8.renderscript.*;
+import android.util.Log;
+import android.content.res.Resources;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.pipeline.PipelineInterface;
+
+public abstract class ImageFilterRS extends ImageFilter {
+ private static final String LOGTAG = "ImageFilterRS";
+ private boolean DEBUG = false;
+ private int mLastInputWidth = 0;
+ private int mLastInputHeight = 0;
+ private long mLastTimeCalled;
+
+ public static boolean PERF_LOGGING = false;
+
+ private static ScriptC_grey mGreyConvert = null;
+ private static RenderScript mRScache = null;
+
+ private volatile boolean mResourcesLoaded = false;
+
+ protected abstract void createFilter(android.content.res.Resources res,
+ float scaleFactor, int quality);
+
+ protected void createFilter(android.content.res.Resources res,
+ float scaleFactor, int quality, Allocation in) {}
+ protected void bindScriptValues(Allocation in) {}
+
+ protected abstract void runFilter();
+
+ protected void update(Bitmap bitmap) {
+ getOutPixelsAllocation().copyTo(bitmap);
+ }
+
+ protected RenderScript getRenderScriptContext() {
+ PipelineInterface pipeline = getEnvironment().getPipeline();
+ return pipeline.getRSContext();
+ }
+
+ protected Allocation getInPixelsAllocation() {
+ PipelineInterface pipeline = getEnvironment().getPipeline();
+ return pipeline.getInPixelsAllocation();
+ }
+
+ protected Allocation getOutPixelsAllocation() {
+ PipelineInterface pipeline = getEnvironment().getPipeline();
+ return pipeline.getOutPixelsAllocation();
+ }
+
+ @Override
+ public void apply(Allocation in, Allocation out) {
+ long startOverAll = System.nanoTime();
+ if (PERF_LOGGING) {
+ long delay = (startOverAll - mLastTimeCalled) / 1000;
+ String msg = String.format("%s; image size %dx%d; ", getName(),
+ in.getType().getX(), in.getType().getY());
+ msg += String.format("called after %.2f ms (%.2f FPS); ",
+ delay / 1000.f, 1000000.f / delay);
+ Log.i(LOGTAG, msg);
+ }
+ mLastTimeCalled = startOverAll;
+ long startFilter = 0;
+ long endFilter = 0;
+ if (!mResourcesLoaded) {
+ PipelineInterface pipeline = getEnvironment().getPipeline();
+ createFilter(pipeline.getResources(), getEnvironment().getScaleFactor(),
+ getEnvironment().getQuality(), in);
+ mResourcesLoaded = true;
+ }
+ startFilter = System.nanoTime();
+ bindScriptValues(in);
+ run(in, out);
+ if (PERF_LOGGING) {
+ getRenderScriptContext().finish();
+ endFilter = System.nanoTime();
+ long endOverAll = System.nanoTime();
+ String msg = String.format("%s; image size %dx%d; ", getName(),
+ in.getType().getX(), in.getType().getY());
+ long timeOverAll = (endOverAll - startOverAll) / 1000;
+ long timeFilter = (endFilter - startFilter) / 1000;
+ msg += String.format("over all %.2f ms (%.2f FPS); ",
+ timeOverAll / 1000.f, 1000000.f / timeOverAll);
+ msg += String.format("run filter %.2f ms (%.2f FPS)",
+ timeFilter / 1000.f, 1000000.f / timeFilter);
+ Log.i(LOGTAG, msg);
+ }
+ }
+
+ protected void run(Allocation in, Allocation out) {}
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (bitmap == null || bitmap.getWidth() == 0 || bitmap.getHeight() == 0) {
+ return bitmap;
+ }
+ try {
+ PipelineInterface pipeline = getEnvironment().getPipeline();
+ if (DEBUG) {
+ Log.v(LOGTAG, "apply filter " + getName() + " in pipeline " + pipeline.getName());
+ }
+ Resources rsc = pipeline.getResources();
+ boolean sizeChanged = false;
+ if (getInPixelsAllocation() != null
+ && ((getInPixelsAllocation().getType().getX() != mLastInputWidth)
+ || (getInPixelsAllocation().getType().getY() != mLastInputHeight))) {
+ sizeChanged = true;
+ }
+ if (pipeline.prepareRenderscriptAllocations(bitmap)
+ || !isResourcesLoaded() || sizeChanged) {
+ freeResources();
+ createFilter(rsc, scaleFactor, quality);
+ setResourcesLoaded(true);
+ mLastInputWidth = getInPixelsAllocation().getType().getX();
+ mLastInputHeight = getInPixelsAllocation().getType().getY();
+ }
+ bindScriptValues();
+ runFilter();
+ update(bitmap);
+ if (DEBUG) {
+ Log.v(LOGTAG, "DONE apply filter " + getName() + " in pipeline " + pipeline.getName());
+ }
+ } catch (android.renderscript.RSIllegalArgumentException e) {
+ Log.e(LOGTAG, "Illegal argument? " + e);
+ } catch (android.renderscript.RSRuntimeException e) {
+ Log.e(LOGTAG, "RS runtime exception ? " + e);
+ } catch (java.lang.OutOfMemoryError e) {
+ // Many of the renderscript filters allocated large (>16Mb resources) in order to apply.
+ System.gc();
+ displayLowMemoryToast();
+ Log.e(LOGTAG, "not enough memory for filter " + getName(), e);
+ }
+ return bitmap;
+ }
+
+ protected static Allocation convertBitmap(RenderScript RS, Bitmap bitmap) {
+ return Allocation.createFromBitmap(RS, bitmap,
+ Allocation.MipmapControl.MIPMAP_NONE,
+ Allocation.USAGE_SCRIPT | Allocation.USAGE_GRAPHICS_TEXTURE);
+ }
+
+ private static Allocation convertRGBAtoA(RenderScript RS, Bitmap bitmap) {
+ if (RS != mRScache || mGreyConvert == null) {
+ mGreyConvert = new ScriptC_grey(RS, RS.getApplicationContext().getResources(),
+ R.raw.grey);
+ mRScache = RS;
+ }
+
+ Type.Builder tb_a8 = new Type.Builder(RS, Element.A_8(RS));
+
+ Allocation bitmapTemp = convertBitmap(RS, bitmap);
+ if (bitmapTemp.getType().getElement().isCompatible(Element.A_8(RS))) {
+ return bitmapTemp;
+ }
+
+ tb_a8.setX(bitmapTemp.getType().getX());
+ tb_a8.setY(bitmapTemp.getType().getY());
+ Allocation bitmapAlloc = Allocation.createTyped(RS, tb_a8.create(),
+ Allocation.MipmapControl.MIPMAP_NONE,
+ Allocation.USAGE_SCRIPT | Allocation.USAGE_GRAPHICS_TEXTURE);
+ mGreyConvert.forEach_RGBAtoA(bitmapTemp, bitmapAlloc);
+ bitmapTemp.destroy();
+ return bitmapAlloc;
+ }
+
+ public Allocation loadScaledResourceAlpha(int resource, int inSampleSize) {
+ Resources res = getEnvironment().getPipeline().getResources();
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ALPHA_8;
+ options.inSampleSize = inSampleSize;
+ Bitmap bitmap = BitmapFactory.decodeResource(
+ res,
+ resource, options);
+ Allocation ret = convertRGBAtoA(getRenderScriptContext(), bitmap);
+ bitmap.recycle();
+ return ret;
+ }
+
+ public Allocation loadScaledResourceAlpha(int resource, int w, int h, int inSampleSize) {
+ Resources res = getEnvironment().getPipeline().getResources();
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ALPHA_8;
+ options.inSampleSize = inSampleSize;
+ Bitmap bitmap = BitmapFactory.decodeResource(
+ res,
+ resource, options);
+ Bitmap resizeBitmap = Bitmap.createScaledBitmap(bitmap, w, h, true);
+ Allocation ret = convertRGBAtoA(getRenderScriptContext(), resizeBitmap);
+ resizeBitmap.recycle();
+ bitmap.recycle();
+ return ret;
+ }
+
+ public Allocation loadResourceAlpha(int resource) {
+ return loadScaledResourceAlpha(resource, 1);
+ }
+
+ public Allocation loadResource(int resource) {
+ Resources res = getEnvironment().getPipeline().getResources();
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ Bitmap bitmap = BitmapFactory.decodeResource(
+ res,
+ resource, options);
+ Allocation ret = convertBitmap(getRenderScriptContext(), bitmap);
+ bitmap.recycle();
+ return ret;
+ }
+
+ private boolean isResourcesLoaded() {
+ return mResourcesLoaded;
+ }
+
+ private void setResourcesLoaded(boolean resourcesLoaded) {
+ mResourcesLoaded = resourcesLoaded;
+ }
+
+ /**
+ * Bitmaps and RS Allocations should be cleared here
+ */
+ abstract protected void resetAllocations();
+
+ /**
+ * RS Script objects (and all other RS objects) should be cleared here
+ */
+ public abstract void resetScripts();
+
+ /**
+ * Scripts values should be bound here
+ */
+ abstract protected void bindScriptValues();
+
+ public void freeResources() {
+ if (!isResourcesLoaded()) {
+ return;
+ }
+ resetAllocations();
+ mLastInputWidth = 0;
+ mLastInputHeight = 0;
+ setResourcesLoaded(false);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterRedEye.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterRedEye.java
new file mode 100644
index 000000000..511f9e90f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterRedEye.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+
+import java.util.Vector;
+
+public class ImageFilterRedEye extends ImageFilter {
+ private static final String LOGTAG = "ImageFilterRedEye";
+ FilterRedEyeRepresentation mParameters = new FilterRedEyeRepresentation();
+
+ public ImageFilterRedEye() {
+ mName = "Red Eye";
+ }
+
+ @Override
+ public FilterRepresentation getDefaultRepresentation() {
+ return new FilterRedEyeRepresentation();
+ }
+
+ public boolean isNil() {
+ return mParameters.isNil();
+ }
+
+ public Vector<FilterPoint> getCandidates() {
+ return mParameters.getCandidates();
+ }
+
+ public void clear() {
+ mParameters.clearCandidates();
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, short[] matrix);
+
+ @Override
+ public void useRepresentation(FilterRepresentation representation) {
+ FilterRedEyeRepresentation parameters = (FilterRedEyeRepresentation) representation;
+ mParameters = parameters;
+ }
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ short[] rect = new short[4];
+
+ int size = mParameters.getNumberOfCandidates();
+ Matrix originalToScreen = getOriginalToScreenMatrix(w, h);
+ for (int i = 0; i < size; i++) {
+ RectF r = new RectF(((RedEyeCandidate) (mParameters.getCandidate(i))).mRect);
+ originalToScreen.mapRect(r);
+ if (r.intersect(0, 0, w, h)) {
+ rect[0] = (short) r.left;
+ rect[1] = (short) r.top;
+ rect[2] = (short) r.width();
+ rect[3] = (short) r.height();
+ nativeApplyFilter(bitmap, w, h, rect);
+ }
+ }
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java
new file mode 100644
index 000000000..c3124ff77
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSaturated.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterSaturated extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "SATURATED";
+ public ImageFilterSaturated() {
+ mName = "Saturated";
+ }
+
+ @Override
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterBasicRepresentation representation =
+ (FilterBasicRepresentation) super.getDefaultRepresentation();
+ representation.setName("Saturated");
+ representation.setSerializationName(SERIALIZATION_NAME);
+ representation.setFilterClass(ImageFilterSaturated.class);
+ representation.setTextId(R.string.saturation);
+ representation.setMinimum(-100);
+ representation.setMaximum(100);
+ representation.setDefaultValue(0);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float saturation);
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null) {
+ return bitmap;
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ int p = getParameters().getValue();
+ float value = 1 + p / 100.0f;
+ nativeApplyFilter(bitmap, w, h, value);
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java
new file mode 100644
index 000000000..bd119bbc9
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterShadows.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterShadows extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "SHADOWS";
+ public ImageFilterShadows() {
+ mName = "Shadows";
+
+ }
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterBasicRepresentation representation =
+ (FilterBasicRepresentation) super.getDefaultRepresentation();
+ representation.setName("Shadows");
+ representation.setSerializationName(SERIALIZATION_NAME);
+ representation.setFilterClass(ImageFilterShadows.class);
+ representation.setTextId(R.string.shadow_recovery);
+ representation.setMinimum(-100);
+ representation.setMaximum(100);
+ representation.setDefaultValue(0);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float factor);
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null) {
+ return bitmap;
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ float p = getParameters().getValue();
+
+ nativeApplyFilter(bitmap, w, h, p);
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java
new file mode 100644
index 000000000..3bd794464
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterSharpen.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+public class ImageFilterSharpen extends ImageFilterRS {
+ private static final String SERIALIZATION_NAME = "SHARPEN";
+ private static final String LOGTAG = "ImageFilterSharpen";
+ private ScriptC_convolve3x3 mScript;
+
+ private FilterBasicRepresentation mParameters;
+
+ public ImageFilterSharpen() {
+ mName = "Sharpen";
+ }
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterRepresentation representation = new FilterBasicRepresentation("Sharpen", 0, 0, 100);
+ representation.setSerializationName(SERIALIZATION_NAME);
+ representation.setShowParameterValue(true);
+ representation.setFilterClass(ImageFilterSharpen.class);
+ representation.setTextId(R.string.sharpness);
+ representation.setOverlayId(R.drawable.filtershow_button_colors_sharpen);
+ representation.setEditorId(R.id.imageShow);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ public void useRepresentation(FilterRepresentation representation) {
+ FilterBasicRepresentation parameters = (FilterBasicRepresentation) representation;
+ mParameters = parameters;
+ }
+
+ @Override
+ protected void resetAllocations() {
+ // nothing to do
+ }
+
+ @Override
+ public void resetScripts() {
+ if (mScript != null) {
+ mScript.destroy();
+ mScript = null;
+ }
+ }
+
+ @Override
+ protected void createFilter(android.content.res.Resources res, float scaleFactor,
+ int quality) {
+ if (mScript == null) {
+ mScript = new ScriptC_convolve3x3(getRenderScriptContext(), res, R.raw.convolve3x3);
+ }
+ }
+
+ private void computeKernel() {
+ float scaleFactor = getEnvironment().getScaleFactor();
+ float p1 = mParameters.getValue() * scaleFactor;
+ float value = p1 / 100.0f;
+ float f[] = new float[9];
+ float p = value;
+ f[0] = -p;
+ f[1] = -p;
+ f[2] = -p;
+ f[3] = -p;
+ f[4] = 8 * p + 1;
+ f[5] = -p;
+ f[6] = -p;
+ f[7] = -p;
+ f[8] = -p;
+ mScript.set_gCoeffs(f);
+ }
+
+ @Override
+ protected void bindScriptValues() {
+ int w = getInPixelsAllocation().getType().getX();
+ int h = getInPixelsAllocation().getType().getY();
+ mScript.set_gWidth(w);
+ mScript.set_gHeight(h);
+ }
+
+ @Override
+ protected void runFilter() {
+ if (mParameters == null) {
+ return;
+ }
+ computeKernel();
+ mScript.set_gIn(getInPixelsAllocation());
+ mScript.bind_gPixels(getInPixelsAllocation());
+ mScript.forEach_root(getInPixelsAllocation(), getOutPixelsAllocation());
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java
new file mode 100644
index 000000000..77250bd7a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterTinyPlanet.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPMeta;
+import com.android.gallery3d.app.Log;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+/**
+ * An image filter which creates a tiny planet projection.
+ */
+public class ImageFilterTinyPlanet extends SimpleImageFilter {
+
+
+ private static final String LOGTAG = ImageFilterTinyPlanet.class.getSimpleName();
+ public static final String GOOGLE_PANO_NAMESPACE = "http://ns.google.com/photos/1.0/panorama/";
+ FilterTinyPlanetRepresentation mParameters = new FilterTinyPlanetRepresentation();
+
+ public static final String CROPPED_AREA_IMAGE_WIDTH_PIXELS =
+ "CroppedAreaImageWidthPixels";
+ public static final String CROPPED_AREA_IMAGE_HEIGHT_PIXELS =
+ "CroppedAreaImageHeightPixels";
+ public static final String CROPPED_AREA_FULL_PANO_WIDTH_PIXELS =
+ "FullPanoWidthPixels";
+ public static final String CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS =
+ "FullPanoHeightPixels";
+ public static final String CROPPED_AREA_LEFT =
+ "CroppedAreaLeftPixels";
+ public static final String CROPPED_AREA_TOP =
+ "CroppedAreaTopPixels";
+
+ public ImageFilterTinyPlanet() {
+ mName = "TinyPlanet";
+ }
+
+ @Override
+ public void useRepresentation(FilterRepresentation representation) {
+ FilterTinyPlanetRepresentation parameters = (FilterTinyPlanetRepresentation) representation;
+ mParameters = parameters;
+ }
+
+ @Override
+ public FilterRepresentation getDefaultRepresentation() {
+ return new FilterTinyPlanetRepresentation();
+ }
+
+
+ native protected void nativeApplyFilter(
+ Bitmap bitmapIn, int width, int height, Bitmap bitmapOut, int outSize, float scale,
+ float angle);
+
+
+ @Override
+ public Bitmap apply(Bitmap bitmapIn, float scaleFactor, int quality) {
+ int w = bitmapIn.getWidth();
+ int h = bitmapIn.getHeight();
+ int outputSize = (int) (w / 2f);
+ ImagePreset preset = getEnvironment().getImagePreset();
+ Bitmap mBitmapOut = null;
+ if (preset != null) {
+ XMPMeta xmp = ImageLoader.getXmpObject(MasterImage.getImage().getActivity());
+ // Do nothing, just use bitmapIn as is if we don't have XMP.
+ if(xmp != null) {
+ bitmapIn = applyXmp(bitmapIn, xmp, w);
+ }
+ }
+ if (mBitmapOut != null) {
+ if (outputSize != mBitmapOut.getHeight()) {
+ mBitmapOut = null;
+ }
+ }
+ while (mBitmapOut == null) {
+ try {
+ mBitmapOut = getEnvironment().getBitmap(outputSize, outputSize);
+ } catch (java.lang.OutOfMemoryError e) {
+ System.gc();
+ outputSize /= 2;
+ Log.v(LOGTAG, "No memory to create Full Tiny Planet create half");
+ }
+ }
+ nativeApplyFilter(bitmapIn, bitmapIn.getWidth(), bitmapIn.getHeight(), mBitmapOut,
+ outputSize, mParameters.getZoom() / 100f, mParameters.getAngle());
+
+ return mBitmapOut;
+ }
+
+ private Bitmap applyXmp(Bitmap bitmapIn, XMPMeta xmp, int intermediateWidth) {
+ try {
+ int croppedAreaWidth =
+ getInt(xmp, CROPPED_AREA_IMAGE_WIDTH_PIXELS);
+ int croppedAreaHeight =
+ getInt(xmp, CROPPED_AREA_IMAGE_HEIGHT_PIXELS);
+ int fullPanoWidth =
+ getInt(xmp, CROPPED_AREA_FULL_PANO_WIDTH_PIXELS);
+ int fullPanoHeight =
+ getInt(xmp, CROPPED_AREA_FULL_PANO_HEIGHT_PIXELS);
+ int left = getInt(xmp, CROPPED_AREA_LEFT);
+ int top = getInt(xmp, CROPPED_AREA_TOP);
+
+ if (fullPanoWidth == 0 || fullPanoHeight == 0) {
+ return bitmapIn;
+ }
+ // Make sure the intermediate image has the similar size to the
+ // input.
+ Bitmap paddedBitmap = null;
+ float scale = intermediateWidth / (float) fullPanoWidth;
+ while (paddedBitmap == null) {
+ try {
+ paddedBitmap = Bitmap.createBitmap(
+ (int) (fullPanoWidth * scale), (int) (fullPanoHeight * scale),
+ Bitmap.Config.ARGB_8888);
+ } catch (java.lang.OutOfMemoryError e) {
+ System.gc();
+ scale /= 2;
+ }
+ }
+ Canvas paddedCanvas = new Canvas(paddedBitmap);
+
+ int right = left + croppedAreaWidth;
+ int bottom = top + croppedAreaHeight;
+ RectF destRect = new RectF(left * scale, top * scale, right * scale, bottom * scale);
+ paddedCanvas.drawBitmap(bitmapIn, null, destRect, null);
+ bitmapIn = paddedBitmap;
+ } catch (XMPException ex) {
+ // Do nothing, just use bitmapIn as is.
+ }
+ return bitmapIn;
+ }
+
+ private static int getInt(XMPMeta xmp, String key) throws XMPException {
+ if (xmp.doesPropertyExist(GOOGLE_PANO_NAMESPACE, key)) {
+ return xmp.getPropertyInteger(GOOGLE_PANO_NAMESPACE, key);
+ } else {
+ return 0;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java
new file mode 100644
index 000000000..86be9a155
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterVibrance.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterVibrance extends SimpleImageFilter {
+ private static final String SERIALIZATION_NAME = "VIBRANCE";
+ public ImageFilterVibrance() {
+ mName = "Vibrance";
+ }
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterBasicRepresentation representation =
+ (FilterBasicRepresentation) super.getDefaultRepresentation();
+ representation.setName("Vibrance");
+ representation.setSerializationName(SERIALIZATION_NAME);
+ representation.setFilterClass(ImageFilterVibrance.class);
+ representation.setTextId(R.string.vibrance);
+ representation.setMinimum(-100);
+ representation.setMaximum(100);
+ representation.setDefaultValue(0);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, float bright);
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (getParameters() == null) {
+ return bitmap;
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ float value = getParameters().getValue();
+ nativeApplyFilter(bitmap, w, h, value);
+
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java
new file mode 100644
index 000000000..7e0a452bf
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterVignette.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.pipeline.FilterEnvironment;
+
+public class ImageFilterVignette extends SimpleImageFilter {
+ private static final String LOGTAG = "ImageFilterVignette";
+ private Bitmap mOverlayBitmap;
+
+ public ImageFilterVignette() {
+ mName = "Vignette";
+ }
+
+ @Override
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterVignetteRepresentation representation = new FilterVignetteRepresentation();
+ return representation;
+ }
+
+ native protected void nativeApplyFilter(
+ Bitmap bitmap, int w, int h, int cx, int cy, float radx, float rady, float strength);
+
+ private float calcRadius(float cx, float cy, int w, int h) {
+ float d = cx;
+ if (d < (w - cx)) {
+ d = w - cx;
+ }
+ if (d < cy) {
+ d = cy;
+ }
+ if (d < (h - cy)) {
+ d = h - cy;
+ }
+ return d * d * 2.0f;
+ }
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ if (SIMPLE_ICONS && FilterEnvironment.QUALITY_ICON == quality) {
+ if (mOverlayBitmap == null) {
+ Resources res = getEnvironment().getPipeline().getResources();
+ mOverlayBitmap = IconUtilities.getFXBitmap(res,
+ R.drawable.filtershow_icon_vignette);
+ }
+ Canvas c = new Canvas(bitmap);
+ int dim = Math.max(bitmap.getWidth(), bitmap.getHeight());
+ Rect r = new Rect(0, 0, dim, dim);
+ c.drawBitmap(mOverlayBitmap, null, r, null);
+ return bitmap;
+ }
+ FilterVignetteRepresentation rep = (FilterVignetteRepresentation) getParameters();
+ if (rep == null) {
+ return bitmap;
+ }
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ float value = rep.getValue() / 100.0f;
+ float cx = w / 2;
+ float cy = h / 2;
+ float r = calcRadius(cx, cy, w, h);
+ float rx = r;
+ float ry = r;
+ if (rep.isCenterSet()) {
+ Matrix m = getOriginalToScreenMatrix(w, h);
+ cx = rep.getCenterX();
+ cy = rep.getCenterY();
+ float[] center = new float[] { cx, cy };
+ m.mapPoints(center);
+ cx = center[0];
+ cy = center[1];
+ rx = m.mapRadius(rep.getRadiusX());
+ ry = m.mapRadius(rep.getRadiusY());
+ }
+ nativeApplyFilter(bitmap, w, h, (int) cx, (int) cy, rx, ry, value);
+ return bitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java
new file mode 100644
index 000000000..6bb88ec21
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterWBalance.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2012 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.filtershow.filters;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+
+import android.graphics.Bitmap;
+
+public class ImageFilterWBalance extends ImageFilter {
+ private static final String SERIALIZATION_NAME = "WBALANCE";
+ private static final String TAG = "ImageFilterWBalance";
+
+ public ImageFilterWBalance() {
+ mName = "WBalance";
+ }
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterRepresentation representation = new FilterDirectRepresentation("WBalance");
+ representation.setSerializationName(SERIALIZATION_NAME);
+ representation.setFilterClass(ImageFilterWBalance.class);
+ representation.setFilterType(FilterRepresentation.TYPE_WBALANCE);
+ representation.setTextId(R.string.wbalance);
+ representation.setShowParameterValue(false);
+ representation.setEditorId(ImageOnlyEditor.ID);
+ representation.setSupportsPartialRendering(true);
+ return representation;
+ }
+
+ @Override
+ public void useRepresentation(FilterRepresentation representation) {
+
+ }
+
+ native protected void nativeApplyFilter(Bitmap bitmap, int w, int h, int locX, int locY);
+
+ @Override
+ public Bitmap apply(Bitmap bitmap, float scaleFactor, int quality) {
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ nativeApplyFilter(bitmap, w, h, -1, -1);
+ return bitmap;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/RedEyeCandidate.java b/src/com/android/gallery3d/filtershow/filters/RedEyeCandidate.java
new file mode 100644
index 000000000..a40d4fa3b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/RedEyeCandidate.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+import android.graphics.RectF;
+
+public class RedEyeCandidate implements FilterPoint {
+ RectF mRect = new RectF();
+ RectF mBounds = new RectF();
+
+ public RedEyeCandidate(RedEyeCandidate candidate) {
+ mRect.set(candidate.mRect);
+ mBounds.set(candidate.mBounds);
+ }
+
+ public RedEyeCandidate(RectF rect, RectF bounds) {
+ mRect.set(rect);
+ mBounds.set(bounds);
+ }
+
+ public boolean equals(RedEyeCandidate candidate) {
+ if (candidate.mRect.equals(mRect)
+ && candidate.mBounds.equals(mBounds)) {
+ return true;
+ }
+ return false;
+ }
+
+ public boolean intersect(RectF rect) {
+ return mRect.intersect(rect);
+ }
+
+ public RectF getRect() {
+ return mRect;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/SimpleImageFilter.java b/src/com/android/gallery3d/filtershow/filters/SimpleImageFilter.java
new file mode 100644
index 000000000..c891d20f3
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/SimpleImageFilter.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2013 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.filtershow.filters;
+
+public class SimpleImageFilter extends ImageFilter {
+
+ private FilterBasicRepresentation mParameters;
+
+ public FilterRepresentation getDefaultRepresentation() {
+ FilterRepresentation representation = new FilterBasicRepresentation("Default", 0, 50, 100);
+ representation.setShowParameterValue(true);
+ return representation;
+ }
+
+ public void useRepresentation(FilterRepresentation representation) {
+ FilterBasicRepresentation parameters = (FilterBasicRepresentation) representation;
+ mParameters = parameters;
+ }
+
+ public FilterBasicRepresentation getParameters() {
+ return mParameters;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/SplineMath.java b/src/com/android/gallery3d/filtershow/filters/SplineMath.java
new file mode 100644
index 000000000..5b12d0a61
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/SplineMath.java
@@ -0,0 +1,166 @@
+package com.android.gallery3d.filtershow.filters;
+
+
+public class SplineMath {
+ double[][] mPoints = new double[6][2];
+ double[] mDerivatives;
+ SplineMath(int n) {
+ mPoints = new double[n][2];
+ }
+
+ public void setPoint(int index, double x, double y) {
+ mPoints[index][0] = x;
+ mPoints[index][1] = y;
+ mDerivatives = null;
+ }
+
+ public float[][] calculatetCurve(int n) {
+ float[][] curve = new float[n][2];
+ double[][] points = new double[mPoints.length][2];
+ for (int i = 0; i < mPoints.length; i++) {
+
+ points[i][0] = mPoints[i][0];
+ points[i][1] = mPoints[i][1];
+
+ }
+ double[] derivatives = solveSystem(points);
+ float start = (float) points[0][0];
+ float end = (float) (points[points.length - 1][0]);
+
+ curve[0][0] = (float) (points[0][0]);
+ curve[0][1] = (float) (points[0][1]);
+ int last = curve.length - 1;
+ curve[last][0] = (float) (points[points.length - 1][0]);
+ curve[last][1] = (float) (points[points.length - 1][1]);
+
+ for (int i = 0; i < curve.length; i++) {
+
+ double[] cur = null;
+ double[] next = null;
+ double x = start + i * (end - start) / (curve.length - 1);
+ int pivot = 0;
+ for (int j = 0; j < points.length - 1; j++) {
+ if (x >= points[j][0] && x <= points[j + 1][0]) {
+ pivot = j;
+ }
+ }
+ cur = points[pivot];
+ next = points[pivot + 1];
+ if (x <= next[0]) {
+ double x1 = cur[0];
+ double x2 = next[0];
+ double y1 = cur[1];
+ double y2 = next[1];
+
+ // Use the second derivatives to apply the cubic spline
+ // equation:
+ double delta = (x2 - x1);
+ double delta2 = delta * delta;
+ double b = (x - x1) / delta;
+ double a = 1 - b;
+ double ta = a * y1;
+ double tb = b * y2;
+ double tc = (a * a * a - a) * derivatives[pivot];
+ double td = (b * b * b - b) * derivatives[pivot + 1];
+ double y = ta + tb + (delta2 / 6) * (tc + td);
+
+ curve[i][0] = (float) (x);
+ curve[i][1] = (float) (y);
+ } else {
+ curve[i][0] = (float) (next[0]);
+ curve[i][1] = (float) (next[1]);
+ }
+ }
+ return curve;
+ }
+
+ public double getValue(double x) {
+ double[] cur = null;
+ double[] next = null;
+ if (mDerivatives == null)
+ mDerivatives = solveSystem(mPoints);
+ int pivot = 0;
+ for (int j = 0; j < mPoints.length - 1; j++) {
+ pivot = j;
+ if (x <= mPoints[j][0]) {
+ break;
+ }
+ }
+ cur = mPoints[pivot];
+ next = mPoints[pivot + 1];
+ double x1 = cur[0];
+ double x2 = next[0];
+ double y1 = cur[1];
+ double y2 = next[1];
+
+ // Use the second derivatives to apply the cubic spline
+ // equation:
+ double delta = (x2 - x1);
+ double delta2 = delta * delta;
+ double b = (x - x1) / delta;
+ double a = 1 - b;
+ double ta = a * y1;
+ double tb = b * y2;
+ double tc = (a * a * a - a) * mDerivatives[pivot];
+ double td = (b * b * b - b) * mDerivatives[pivot + 1];
+ double y = ta + tb + (delta2 / 6) * (tc + td);
+
+ return y;
+
+ }
+
+ double[] solveSystem(double[][] points) {
+ int n = points.length;
+ double[][] system = new double[n][3];
+ double[] result = new double[n]; // d
+ double[] solution = new double[n]; // returned coefficients
+ system[0][1] = 1;
+ system[n - 1][1] = 1;
+ double d6 = 1.0 / 6.0;
+ double d3 = 1.0 / 3.0;
+
+ // let's create a tridiagonal matrix representing the
+ // system, and apply the TDMA algorithm to solve it
+ // (see http://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
+ for (int i = 1; i < n - 1; i++) {
+ double deltaPrevX = points[i][0] - points[i - 1][0];
+ double deltaX = points[i + 1][0] - points[i - 1][0];
+ double deltaNextX = points[i + 1][0] - points[i][0];
+ double deltaNextY = points[i + 1][1] - points[i][1];
+ double deltaPrevY = points[i][1] - points[i - 1][1];
+ system[i][0] = d6 * deltaPrevX; // a_i
+ system[i][1] = d3 * deltaX; // b_i
+ system[i][2] = d6 * deltaNextX; // c_i
+ result[i] = (deltaNextY / deltaNextX) - (deltaPrevY / deltaPrevX); // d_i
+ }
+
+ // Forward sweep
+ for (int i = 1; i < n; i++) {
+ // m = a_i/b_i-1
+ double m = system[i][0] / system[i - 1][1];
+ // b_i = b_i - m(c_i-1)
+ system[i][1] = system[i][1] - m * system[i - 1][2];
+ // d_i = d_i - m(d_i-1)
+ result[i] = result[i] - m * result[i - 1];
+ }
+
+ // Back substitution
+ solution[n - 1] = result[n - 1] / system[n - 1][1];
+ for (int i = n - 2; i >= 0; --i) {
+ solution[i] = (result[i] - system[i][2] * solution[i + 1]) / system[i][1];
+ }
+ return solution;
+ }
+
+ public static void main(String[] args) {
+ SplineMath s = new SplineMath(10);
+ for (int i = 0; i < 10; i++) {
+ s.setPoint(i, i, i);
+ }
+ float[][] curve = s.calculatetCurve(40);
+
+ for (int j = 0; j < curve.length; j++) {
+ System.out.println(curve[j][0] + "," + curve[j][1]);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/convolve3x3.rs b/src/com/android/gallery3d/filtershow/filters/convolve3x3.rs
new file mode 100644
index 000000000..2acffab06
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/convolve3x3.rs
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+#pragma version(1)
+#pragma rs java_package_name(com.android.gallery3d.filtershow.filters)
+#pragma rs_fp_relaxed
+
+int32_t gWidth;
+int32_t gHeight;
+const uchar4 *gPixels;
+rs_allocation gIn;
+
+float gCoeffs[9];
+
+void root(const uchar4 *in, uchar4 *out, const void *usrData, uint32_t x, uint32_t y) {
+ uint32_t x1 = min((int32_t)x+1, gWidth-1);
+ uint32_t x2 = max((int32_t)x-1, 0);
+ uint32_t y1 = min((int32_t)y+1, gHeight-1);
+ uint32_t y2 = max((int32_t)y-1, 0);
+
+ float4 p00 = rsUnpackColor8888(gPixels[x1 + gWidth * y1]);
+ float4 p01 = rsUnpackColor8888(gPixels[x + gWidth * y1]);
+ float4 p02 = rsUnpackColor8888(gPixels[x2 + gWidth * y1]);
+ float4 p10 = rsUnpackColor8888(gPixels[x1 + gWidth * y]);
+ float4 p11 = rsUnpackColor8888(gPixels[x + gWidth * y]);
+ float4 p12 = rsUnpackColor8888(gPixels[x2 + gWidth * y]);
+ float4 p20 = rsUnpackColor8888(gPixels[x1 + gWidth * y2]);
+ float4 p21 = rsUnpackColor8888(gPixels[x + gWidth * y2]);
+ float4 p22 = rsUnpackColor8888(gPixels[x2 + gWidth * y2]);
+
+ p00 *= gCoeffs[0];
+ p01 *= gCoeffs[1];
+ p02 *= gCoeffs[2];
+ p10 *= gCoeffs[3];
+ p11 *= gCoeffs[4];
+ p12 *= gCoeffs[5];
+ p20 *= gCoeffs[6];
+ p21 *= gCoeffs[7];
+ p22 *= gCoeffs[8];
+
+ p00 += p01;
+ p02 += p10;
+ p11 += p12;
+ p20 += p21;
+
+ p22 += p00;
+ p02 += p11;
+
+ p20 += p22;
+ p20 += p02;
+
+ p20 = clamp(p20, 0.f, 1.f);
+ *out = rsPackColorTo8888(p20.r, p20.g, p20.b);
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/grad.rs b/src/com/android/gallery3d/filtershow/filters/grad.rs
new file mode 100644
index 000000000..ddbafd349
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/grad.rs
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 Unknown
+ *
+ * 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.
+ */
+
+#pragma version(1)
+#pragma rs java_package_name(com.android.gallery3d.filtershow.filters)
+
+#define MAX_POINTS 16
+
+uint32_t inputWidth;
+uint32_t inputHeight;
+static const float Rf = 0.2999f;
+static const float Gf = 0.587f;
+static const float Bf = 0.114f;
+//static const float size_scale = 0.01f;
+
+typedef struct {
+ rs_matrix3x3 colorMatrix;
+ float rgbOff;
+ float dx;
+ float dy;
+ float off;
+} UPointData;
+int mNumberOfLines;
+// input data
+bool mask[MAX_POINTS];
+int xPos1[MAX_POINTS];
+int yPos1[MAX_POINTS];
+int xPos2[MAX_POINTS];
+int yPos2[MAX_POINTS];
+int size[MAX_POINTS];
+int brightness[MAX_POINTS];
+int contrast[MAX_POINTS];
+int saturation[MAX_POINTS];
+
+// generated data
+static UPointData grads[MAX_POINTS];
+
+void setupGradParams() {
+ int k = 0;
+ for (int i = 0; i < MAX_POINTS; i++) {
+ if (!mask[i]) {
+ continue;
+ }
+ float x1 = xPos1[i];
+ float y1 = yPos1[i];
+ float x2 = xPos2[i];
+ float y2 = yPos2[i];
+
+ float denom = (y2 * y2 - 2 * y1 * y2 + x2 * x2 - 2 * x1 * x2 + y1 * y1 + x1 * x1);
+ if (denom == 0) {
+ continue;
+ }
+ grads[k].dy = (y1 - y2) / denom;
+ grads[k].dx = (x1 - x2) / denom;
+ grads[k].off = (y2 * y2 + x2 * x2 - x1 * x2 - y1 * y2) / denom;
+
+ float S = 1+saturation[i]/100.f;
+ float MS = 1-S;
+ float Rt = Rf * MS;
+ float Gt = Gf * MS;
+ float Bt = Bf * MS;
+
+ float b = 1+brightness[i]/100.f;
+ float c = 1+contrast[i]/100.f;
+ b *= c;
+ grads[k].rgbOff = .5f - c/2.f;
+ rsMatrixSet(&grads[i].colorMatrix, 0, 0, b * (Rt + S));
+ rsMatrixSet(&grads[i].colorMatrix, 1, 0, b * Gt);
+ rsMatrixSet(&grads[i].colorMatrix, 2, 0, b * Bt);
+ rsMatrixSet(&grads[i].colorMatrix, 0, 1, b * Rt);
+ rsMatrixSet(&grads[i].colorMatrix, 1, 1, b * (Gt + S));
+ rsMatrixSet(&grads[i].colorMatrix, 2, 1, b * Bt);
+ rsMatrixSet(&grads[i].colorMatrix, 0, 2, b * Rt);
+ rsMatrixSet(&grads[i].colorMatrix, 1, 2, b * Gt);
+ rsMatrixSet(&grads[i].colorMatrix, 2, 2, b * (Bt + S));
+
+ k++;
+ }
+ mNumberOfLines = k;
+}
+
+void init() {
+
+}
+
+uchar4 __attribute__((kernel)) selectiveAdjust(const uchar4 in, uint32_t x,
+ uint32_t y) {
+ float4 pixel = rsUnpackColor8888(in);
+
+ float4 wsum = pixel;
+ wsum.a = 0.f;
+ for (int i = 0; i < mNumberOfLines; i++) {
+ UPointData* grad = &grads[i];
+ float t = clamp(x*grad->dx+y*grad->dy+grad->off,0.f,1.0f);
+ wsum.xyz = wsum.xyz*(1-t)+
+ t*(rsMatrixMultiply(&grad->colorMatrix ,wsum.xyz)+grad->rgbOff);
+
+ }
+
+ pixel.rgb = wsum.rgb;
+ pixel.a = 1.0f;
+
+ uchar4 out = rsPackColorTo8888(clamp(pixel, 0.f, 1.0f));
+ return out;
+}
+
+
+
diff --git a/src/com/android/gallery3d/filtershow/filters/grey.rs b/src/com/android/gallery3d/filtershow/filters/grey.rs
new file mode 100644
index 000000000..e01880360
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/grey.rs
@@ -0,0 +1,22 @@
+ /*
+ * Copyright (C) 2013 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.
+ */
+
+#pragma version(1)
+#pragma rs java_package_name(com.android.gallery3d.filtershow.filters)
+
+uchar __attribute__((kernel)) RGBAtoA(uchar4 in) {
+ return in.r;
+}
diff --git a/src/com/android/gallery3d/filtershow/filters/saturation.rs b/src/com/android/gallery3d/filtershow/filters/saturation.rs
new file mode 100644
index 000000000..5210e34a3
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/filters/saturation.rs
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2012 Unknown
+ *
+ * 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.
+ */
+
+#pragma version(1)
+#pragma rs java_package_name(com.android.gallery3d.filtershow.filters)
+
+#define MAX_CHANELS 7
+#define MAX_HUE 4096
+static const int ABITS = 4;
+static const int HSCALE = 256;
+static const int k1=255 << ABITS;
+static const int k2=HSCALE << ABITS;
+
+static const float Rf = 0.2999f;
+static const float Gf = 0.587f;
+static const float Bf = 0.114f;
+
+rs_matrix3x3 colorMatrix_min;
+rs_matrix3x3 colorMatrix_max;
+
+int mNumberOfLines;
+// input data
+int saturation[MAX_CHANELS];
+float sat[MAX_CHANELS];
+
+float satLut[MAX_HUE];
+// generated data
+
+
+void setupGradParams() {
+
+ int master = saturation[0];
+ int max = master+saturation[1];
+ int min = max;
+
+ // calculate the minimum and maximum saturation
+ for (int i = 1; i < MAX_CHANELS; i++) {
+ int v = master+saturation[i];
+ if (max < v) {
+ max = v;
+ }
+ else if (min > v) {
+ min = v;
+ }
+ }
+ // generate a lookup table for all hue 0 to 4K which goes from 0 to 1 0=min sat 1 = max sat
+ min = min - 1;
+ for(int i = 0; i < MAX_HUE ; i++) {
+ float p = i * 6 / (float)MAX_HUE;
+ int ip = ((int)(p + .5f)) % 6;
+ int v = master + saturation[ip + 1];
+ satLut[i] = (v - min)/(float)(max - min);
+ }
+
+ float S = 1 + max / 100.f;
+ float MS = 1 - S;
+ float Rt = Rf * MS;
+ float Gt = Gf * MS;
+ float Bt = Bf * MS;
+ float b = 1.f;
+
+ // Generate 2 color matrix one at min sat and one at max
+ rsMatrixSet(&colorMatrix_max, 0, 0, b * (Rt + S));
+ rsMatrixSet(&colorMatrix_max, 1, 0, b * Gt);
+ rsMatrixSet(&colorMatrix_max, 2, 0, b * Bt);
+ rsMatrixSet(&colorMatrix_max, 0, 1, b * Rt);
+ rsMatrixSet(&colorMatrix_max, 1, 1, b * (Gt + S));
+ rsMatrixSet(&colorMatrix_max, 2, 1, b * Bt);
+ rsMatrixSet(&colorMatrix_max, 0, 2, b * Rt);
+ rsMatrixSet(&colorMatrix_max, 1, 2, b * Gt);
+ rsMatrixSet(&colorMatrix_max, 2, 2, b * (Bt + S));
+
+ S = 1 + min / 100.f;
+ MS = 1-S;
+ Rt = Rf * MS;
+ Gt = Gf * MS;
+ Bt = Bf * MS;
+ b = 1;
+
+ rsMatrixSet(&colorMatrix_min, 0, 0, b * (Rt + S));
+ rsMatrixSet(&colorMatrix_min, 1, 0, b * Gt);
+ rsMatrixSet(&colorMatrix_min, 2, 0, b * Bt);
+ rsMatrixSet(&colorMatrix_min, 0, 1, b * Rt);
+ rsMatrixSet(&colorMatrix_min, 1, 1, b * (Gt + S));
+ rsMatrixSet(&colorMatrix_min, 2, 1, b * Bt);
+ rsMatrixSet(&colorMatrix_min, 0, 2, b * Rt);
+ rsMatrixSet(&colorMatrix_min, 1, 2, b * Gt);
+ rsMatrixSet(&colorMatrix_min, 2, 2, b * (Bt + S));
+}
+
+static ushort rgb2hue( uchar4 rgb)
+{
+ int iMin,iMax,chroma;
+
+ int ri = rgb.r;
+ int gi = rgb.g;
+ int bi = rgb.b;
+ short rv,rs,rh;
+
+ if (ri > gi) {
+ iMax = max (ri, bi);
+ iMin = min (gi, bi);
+ } else {
+ iMax = max (gi, bi);
+ iMin = min (ri, bi);
+ }
+
+ rv = (short) (iMax << ABITS);
+
+ if (rv == 0) {
+ return 0;
+ }
+
+ chroma = iMax - iMin;
+ rs = (short) ((k1 * chroma) / iMax);
+ if (rs == 0) {
+ return 0;
+ }
+
+ if ( ri == iMax ) {
+ rh = (short) ((k2 * (6 * chroma + gi - bi))/(6 * chroma));
+ if (rh >= k2) {
+ rh -= k2;
+ }
+ return rh;
+ }
+
+ if (gi == iMax) {
+ return(short) ((k2 * (2 * chroma + bi - ri)) / (6 * chroma));
+ }
+
+ return (short) ((k2 * (4 * chroma + ri - gi)) / (6 * chroma));
+}
+
+uchar4 __attribute__((kernel)) selectiveAdjust(const uchar4 in, uint32_t x,
+ uint32_t y) {
+ float4 pixel = rsUnpackColor8888(in);
+
+ float4 wsum = pixel;
+ int hue = rgb2hue(in);
+
+ float t = satLut[hue];
+ pixel.xyz = rsMatrixMultiply(&colorMatrix_min ,pixel.xyz) * (1 - t) +
+ t * (rsMatrixMultiply(&colorMatrix_max ,pixel.xyz));
+
+ pixel.a = 1.0f;
+ return rsPackColorTo8888(clamp(pixel, 0.f, 1.0f));
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/filtershow/history/HistoryItem.java b/src/com/android/gallery3d/filtershow/history/HistoryItem.java
new file mode 100644
index 000000000..2baaac327
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/history/HistoryItem.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 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.filtershow.history;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+public class HistoryItem {
+ private static final String LOGTAG = "HistoryItem";
+ private ImagePreset mImagePreset;
+ private FilterRepresentation mFilterRepresentation;
+ private Bitmap mPreviewImage;
+
+ public HistoryItem(ImagePreset preset, FilterRepresentation representation) {
+ mImagePreset = new ImagePreset(preset);
+ if (representation != null) {
+ mFilterRepresentation = representation.copy();
+ }
+ }
+
+ public ImagePreset getImagePreset() {
+ return mImagePreset;
+ }
+
+ public FilterRepresentation getFilterRepresentation() {
+ return mFilterRepresentation;
+ }
+
+ public Bitmap getPreviewImage() {
+ return mPreviewImage;
+ }
+
+ public void setPreviewImage(Bitmap previewImage) {
+ mPreviewImage = previewImage;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/history/HistoryManager.java b/src/com/android/gallery3d/filtershow/history/HistoryManager.java
new file mode 100644
index 000000000..755e2ea58
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/history/HistoryManager.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2013 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.filtershow.history;
+
+import android.graphics.drawable.Drawable;
+import android.view.MenuItem;
+
+import java.util.Vector;
+
+public class HistoryManager {
+ private static final String LOGTAG = "HistoryManager";
+
+ private Vector<HistoryItem> mHistoryItems = new Vector<HistoryItem>();
+ private int mCurrentPresetPosition = 0;
+ private MenuItem mUndoMenuItem = null;
+ private MenuItem mRedoMenuItem = null;
+ private MenuItem mResetMenuItem = null;
+
+ public void setMenuItems(MenuItem undoItem, MenuItem redoItem, MenuItem resetItem) {
+ mUndoMenuItem = undoItem;
+ mRedoMenuItem = redoItem;
+ mResetMenuItem = resetItem;
+ updateMenuItems();
+ }
+
+ private int getCount() {
+ return mHistoryItems.size();
+ }
+
+ public HistoryItem getItem(int position) {
+ return mHistoryItems.elementAt(position);
+ }
+
+ private void clear() {
+ mHistoryItems.clear();
+ }
+
+ private void add(HistoryItem item) {
+ mHistoryItems.add(item);
+ }
+
+ private void notifyDataSetChanged() {
+ // TODO
+ }
+
+ public boolean canReset() {
+ if (getCount() <= 1) {
+ return false;
+ }
+ return true;
+ }
+
+ public boolean canUndo() {
+ if (mCurrentPresetPosition == getCount() - 1) {
+ return false;
+ }
+ return true;
+ }
+
+ public boolean canRedo() {
+ if (mCurrentPresetPosition == 0) {
+ return false;
+ }
+ return true;
+ }
+
+ public void updateMenuItems() {
+ if (mUndoMenuItem != null) {
+ setEnabled(mUndoMenuItem, canUndo());
+ }
+ if (mRedoMenuItem != null) {
+ setEnabled(mRedoMenuItem, canRedo());
+ }
+ if (mResetMenuItem != null) {
+ setEnabled(mResetMenuItem, canReset());
+ }
+ }
+
+ private void setEnabled(MenuItem item, boolean enabled) {
+ item.setEnabled(enabled);
+ Drawable drawable = item.getIcon();
+ if (drawable != null) {
+ drawable.setAlpha(enabled ? 255 : 80);
+ }
+ }
+
+ public void setCurrentPreset(int n) {
+ mCurrentPresetPosition = n;
+ updateMenuItems();
+ notifyDataSetChanged();
+ }
+
+ public void reset() {
+ if (getCount() == 0) {
+ return;
+ }
+ HistoryItem first = getItem(getCount() - 1);
+ clear();
+ addHistoryItem(first);
+ updateMenuItems();
+ }
+
+ public HistoryItem getLast() {
+ if (getCount() == 0) {
+ return null;
+ }
+ return getItem(0);
+ }
+
+ public HistoryItem getCurrent() {
+ return getItem(mCurrentPresetPosition);
+ }
+
+ public void addHistoryItem(HistoryItem preset) {
+ insert(preset, 0);
+ updateMenuItems();
+ }
+
+ private void insert(HistoryItem preset, int position) {
+ if (mCurrentPresetPosition != 0) {
+ // in this case, let's discount the presets before the current one
+ Vector<HistoryItem> oldItems = new Vector<HistoryItem>();
+ for (int i = mCurrentPresetPosition; i < getCount(); i++) {
+ oldItems.add(getItem(i));
+ }
+ clear();
+ for (int i = 0; i < oldItems.size(); i++) {
+ add(oldItems.elementAt(i));
+ }
+ mCurrentPresetPosition = position;
+ notifyDataSetChanged();
+ }
+ mHistoryItems.insertElementAt(preset, position);
+ mCurrentPresetPosition = position;
+ notifyDataSetChanged();
+ }
+
+ public int redo() {
+ mCurrentPresetPosition--;
+ if (mCurrentPresetPosition < 0) {
+ mCurrentPresetPosition = 0;
+ }
+ notifyDataSetChanged();
+ updateMenuItems();
+ return mCurrentPresetPosition;
+ }
+
+ public int undo() {
+ mCurrentPresetPosition++;
+ if (mCurrentPresetPosition >= getCount()) {
+ mCurrentPresetPosition = getCount() - 1;
+ }
+ notifyDataSetChanged();
+ updateMenuItems();
+ return mCurrentPresetPosition;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java b/src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java
new file mode 100644
index 000000000..aaec728a6
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ControlPoint.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 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.filtershow.imageshow;
+
+public class ControlPoint implements Comparable {
+ public float x;
+ public float y;
+
+ public ControlPoint(float px, float py) {
+ x = px;
+ y = py;
+ }
+
+ public ControlPoint(ControlPoint point) {
+ x = point.x;
+ y = point.y;
+ }
+
+ public boolean sameValues(ControlPoint other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null) {
+ return false;
+ }
+
+ if (Float.floatToIntBits(x) != Float.floatToIntBits(other.x)) {
+ return false;
+ }
+ if (Float.floatToIntBits(y) != Float.floatToIntBits(other.y)) {
+ return false;
+ }
+ return true;
+ }
+
+ public ControlPoint copy() {
+ return new ControlPoint(x, y);
+ }
+
+ @Override
+ public int compareTo(Object another) {
+ ControlPoint p = (ControlPoint) another;
+ if (p.x < x) {
+ return 1;
+ } else if (p.x > x) {
+ return -1;
+ }
+ return 0;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java b/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java
new file mode 100644
index 000000000..8ceb37599
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/EclipseControl.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2012 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.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.RectF;
+import android.graphics.Shader;
+
+import com.android.gallery3d.R;
+
+public class EclipseControl {
+ private float mCenterX = Float.NaN;
+ private float mCenterY = 0;
+ private float mRadiusX = 200;
+ private float mRadiusY = 300;
+ private static int MIN_TOUCH_DIST = 80;// should be a resource & in dips
+
+ private float[] handlex = new float[9];
+ private float[] handley = new float[9];
+ private int mSliderColor;
+ private int mCenterDotSize = 40;
+ private float mDownX;
+ private float mDownY;
+ private float mDownCenterX;
+ private float mDownCenterY;
+ private float mDownRadiusX;
+ private float mDownRadiusY;
+ private Matrix mScrToImg;
+
+ private boolean mShowReshapeHandles = true;
+ public final static int HAN_CENTER = 0;
+ public final static int HAN_NORTH = 7;
+ public final static int HAN_NE = 8;
+ public final static int HAN_EAST = 1;
+ public final static int HAN_SE = 2;
+ public final static int HAN_SOUTH = 3;
+ public final static int HAN_SW = 4;
+ public final static int HAN_WEST = 5;
+ public final static int HAN_NW = 6;
+
+ public EclipseControl(Context context) {
+ mSliderColor = Color.WHITE;
+ }
+
+ public void setRadius(float x, float y) {
+ mRadiusX = x;
+ mRadiusY = y;
+ }
+
+ public void setCenter(float x, float y) {
+ mCenterX = x;
+ mCenterY = y;
+ }
+
+ public int getCloseHandle(float x, float y) {
+ float min = Float.MAX_VALUE;
+ int handle = -1;
+ for (int i = 0; i < handlex.length; i++) {
+ float dx = handlex[i] - x;
+ float dy = handley[i] - y;
+ float dist = dx * dx + dy * dy;
+ if (dist < min) {
+ min = dist;
+ handle = i;
+ }
+ }
+
+ if (min < MIN_TOUCH_DIST * MIN_TOUCH_DIST) {
+ return handle;
+ }
+ for (int i = 0; i < handlex.length; i++) {
+ float dx = handlex[i] - x;
+ float dy = handley[i] - y;
+ float dist = (float) Math.sqrt(dx * dx + dy * dy);
+ }
+
+ return -1;
+ }
+
+ public void setScrToImageMatrix(Matrix scrToImg) {
+ mScrToImg = scrToImg;
+ }
+
+ public void actionDown(float x, float y, Oval oval) {
+ float[] point = new float[] {
+ x, y };
+ mScrToImg.mapPoints(point);
+ mDownX = point[0];
+ mDownY = point[1];
+ mDownCenterX = oval.getCenterX();
+ mDownCenterY = oval.getCenterY();
+ mDownRadiusX = oval.getRadiusX();
+ mDownRadiusY = oval.getRadiusY();
+ }
+
+ public void actionMove(int handle, float x, float y, Oval oval) {
+ float[] point = new float[] {
+ x, y };
+ mScrToImg.mapPoints(point);
+ x = point[0];
+ y = point[1];
+
+ // Test if the matrix is swapping x and y
+ point[0] = 0;
+ point[1] = 1;
+ mScrToImg.mapVectors(point);
+ boolean swapxy = (point[0] > 0.0f);
+
+ int sign = 1;
+ switch (handle) {
+ case HAN_CENTER:
+ float ctrdx = mDownX - mDownCenterX;
+ float ctrdy = mDownY - mDownCenterY;
+ oval.setCenter(x - ctrdx, y - ctrdy);
+ // setRepresentation(mVignetteRep);
+ break;
+ case HAN_NORTH:
+ sign = -1;
+ case HAN_SOUTH:
+ if (swapxy) {
+ float raddx = mDownRadiusY - Math.abs(mDownX - mDownCenterY);
+ oval.setRadiusY(Math.abs(x - oval.getCenterY() + sign * raddx));
+ } else {
+ float raddy = mDownRadiusY - Math.abs(mDownY - mDownCenterY);
+ oval.setRadiusY(Math.abs(y - oval.getCenterY() + sign * raddy));
+ }
+ break;
+ case HAN_EAST:
+ sign = -1;
+ case HAN_WEST:
+ if (swapxy) {
+ float raddy = mDownRadiusX - Math.abs(mDownY - mDownCenterX);
+ oval.setRadiusX(Math.abs(y - oval.getCenterX() + sign * raddy));
+ } else {
+ float raddx = mDownRadiusX - Math.abs(mDownX - mDownCenterX);
+ oval.setRadiusX(Math.abs(x - oval.getCenterX() - sign * raddx));
+ }
+ break;
+ case HAN_SE:
+ case HAN_NE:
+ case HAN_SW:
+ case HAN_NW:
+ float sin45 = (float) Math.sin(45);
+ float dr = (mDownRadiusX + mDownRadiusY) * sin45;
+ float ctr_dx = mDownX - mDownCenterX;
+ float ctr_dy = mDownY - mDownCenterY;
+ float downRad = Math.abs(ctr_dx) + Math.abs(ctr_dy) - dr;
+ float rx = oval.getRadiusX();
+ float ry = oval.getRadiusY();
+ float r = (Math.abs(rx) + Math.abs(ry)) * sin45;
+ float dx = x - oval.getCenterX();
+ float dy = y - oval.getCenterY();
+ float nr = Math.abs(Math.abs(dx) + Math.abs(dy) - downRad);
+ oval.setRadius(rx * nr / r, ry * nr / r);
+
+ break;
+ }
+ }
+
+ public void paintGrayPoint(Canvas canvas, float x, float y) {
+ if (x == Float.NaN) {
+ return;
+ }
+
+ Paint paint = new Paint();
+
+ paint.setStyle(Paint.Style.FILL);
+ paint.setColor(Color.BLUE);
+ int[] colors3 = new int[] {
+ Color.GRAY, Color.LTGRAY, 0x66000000, 0 };
+ RadialGradient g = new RadialGradient(x, y, mCenterDotSize, colors3, new float[] {
+ 0, .3f, .31f, 1 }, Shader.TileMode.CLAMP);
+ paint.setShader(g);
+ canvas.drawCircle(x, y, mCenterDotSize, paint);
+ }
+
+ public void paintPoint(Canvas canvas, float x, float y) {
+ if (x == Float.NaN) {
+ return;
+ }
+
+ Paint paint = new Paint();
+
+ paint.setStyle(Paint.Style.FILL);
+ paint.setColor(Color.BLUE);
+ int[] colors3 = new int[] {
+ mSliderColor, mSliderColor, 0x66000000, 0 };
+ RadialGradient g = new RadialGradient(x, y, mCenterDotSize, colors3, new float[] {
+ 0, .3f, .31f, 1 }, Shader.TileMode.CLAMP);
+ paint.setShader(g);
+ canvas.drawCircle(x, y, mCenterDotSize, paint);
+ }
+
+ void paintRadius(Canvas canvas, float cx, float cy, float rx, float ry) {
+ if (cx == Float.NaN) {
+ return;
+ }
+ int mSliderColor = 0xFF33B5E5;
+ Paint paint = new Paint();
+ RectF rect = new RectF(cx - rx, cy - ry, cx + rx, cy + ry);
+ paint.setAntiAlias(true);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(6);
+ paint.setColor(Color.BLACK);
+ paintOvallines(canvas, rect, paint, cx, cy, rx, ry);
+
+ paint.setStrokeWidth(3);
+ paint.setColor(Color.WHITE);
+ paintOvallines(canvas, rect, paint, cx, cy, rx, ry);
+ }
+
+ public void paintOvallines(
+ Canvas canvas, RectF rect, Paint paint, float cx, float cy, float rx, float ry) {
+ canvas.drawOval(rect, paint);
+ float da = 4;
+ float arclen = da + da;
+ if (mShowReshapeHandles) {
+ paint.setStyle(Paint.Style.STROKE);
+
+ for (int i = 0; i < 361; i += 90) {
+ float dx = rx + 10;
+ float dy = ry + 10;
+ rect.left = cx - dx;
+ rect.top = cy - dy;
+ rect.right = cx + dx;
+ rect.bottom = cy + dy;
+ canvas.drawArc(rect, i - da, arclen, false, paint);
+ dx = rx - 10;
+ dy = ry - 10;
+ rect.left = cx - dx;
+ rect.top = cy - dy;
+ rect.right = cx + dx;
+ rect.bottom = cy + dy;
+ canvas.drawArc(rect, i - da, arclen, false, paint);
+ }
+ }
+ da *= 2;
+ paint.setStyle(Paint.Style.FILL);
+
+ for (int i = 45; i < 361; i += 90) {
+ double angle = Math.PI * i / 180.;
+ float x = cx + (float) (rx * Math.cos(angle));
+ float y = cy + (float) (ry * Math.sin(angle));
+ canvas.drawRect(x - da, y - da, x + da, y + da, paint);
+ }
+ paint.setStyle(Paint.Style.STROKE);
+ rect.left = cx - rx;
+ rect.top = cy - ry;
+ rect.right = cx + rx;
+ rect.bottom = cy + ry;
+ }
+
+ public void fillHandles(Canvas canvas, float cx, float cy, float rx, float ry) {
+ handlex[0] = cx;
+ handley[0] = cy;
+ int k = 1;
+
+ for (int i = 0; i < 360; i += 45) {
+ double angle = Math.PI * i / 180.;
+
+ float x = cx + (float) (rx * Math.cos(angle));
+ float y = cy + (float) (ry * Math.sin(angle));
+ handlex[k] = x;
+ handley[k] = y;
+
+ k++;
+ }
+ }
+
+ public void draw(Canvas canvas) {
+ paintRadius(canvas, mCenterX, mCenterY, mRadiusX, mRadiusY);
+ fillHandles(canvas, mCenterX, mCenterY, mRadiusX, mRadiusY);
+ paintPoint(canvas, mCenterX, mCenterY);
+ }
+
+ public boolean isUndefined() {
+ return Float.isNaN(mCenterX);
+ }
+
+ public void setShowReshapeHandles(boolean showReshapeHandles) {
+ this.mShowReshapeHandles = showReshapeHandles;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java b/src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java
new file mode 100644
index 000000000..81394f142
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/GeometryMathUtils.java
@@ -0,0 +1,416 @@
+/*
+ * Copyright (C) 2012 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.filtershow.imageshow;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FilterCropRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation.Mirror;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation.Rotation;
+import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+public final class GeometryMathUtils {
+ private GeometryMathUtils() {};
+
+ // Holder class for Geometry data.
+ public static final class GeometryHolder {
+ public Rotation rotation = FilterRotateRepresentation.getNil();
+ public float straighten = FilterStraightenRepresentation.getNil();
+ public RectF crop = FilterCropRepresentation.getNil();
+ public Mirror mirror = FilterMirrorRepresentation.getNil();
+
+ public void set(GeometryHolder h) {
+ rotation = h.rotation;
+ straighten = h.straighten;
+ crop.set(h.crop);
+ mirror = h.mirror;
+ }
+
+ public void wipe() {
+ rotation = FilterRotateRepresentation.getNil();
+ straighten = FilterStraightenRepresentation.getNil();
+ crop = FilterCropRepresentation.getNil();
+ mirror = FilterMirrorRepresentation.getNil();
+ }
+
+ public boolean isNil() {
+ return rotation == FilterRotateRepresentation.getNil() &&
+ straighten == FilterStraightenRepresentation.getNil() &&
+ crop.equals(FilterCropRepresentation.getNil()) &&
+ mirror == FilterMirrorRepresentation.getNil();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof GeometryHolder)) {
+ return false;
+ }
+ GeometryHolder h = (GeometryHolder) o;
+ return rotation == h.rotation && straighten == h.straighten &&
+ ((crop == null && h.crop == null) || (crop != null && crop.equals(h.crop))) &&
+ mirror == h.mirror;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[" + "rotation:" + rotation.value()
+ + ",straighten:" + straighten + ",crop:" + crop.toString()
+ + ",mirror:" + mirror.value() + "]";
+ }
+ }
+
+ // Math operations for 2d vectors
+ public static float clamp(float i, float low, float high) {
+ return Math.max(Math.min(i, high), low);
+ }
+
+ public static float[] lineIntersect(float[] line1, float[] line2) {
+ float a0 = line1[0];
+ float a1 = line1[1];
+ float b0 = line1[2];
+ float b1 = line1[3];
+ float c0 = line2[0];
+ float c1 = line2[1];
+ float d0 = line2[2];
+ float d1 = line2[3];
+ float t0 = a0 - b0;
+ float t1 = a1 - b1;
+ float t2 = b0 - d0;
+ float t3 = d1 - b1;
+ float t4 = c0 - d0;
+ float t5 = c1 - d1;
+
+ float denom = t1 * t4 - t0 * t5;
+ if (denom == 0)
+ return null;
+ float u = (t3 * t4 + t5 * t2) / denom;
+ float[] intersect = {
+ b0 + u * t0, b1 + u * t1
+ };
+ return intersect;
+ }
+
+ public static float[] shortestVectorFromPointToLine(float[] point, float[] line) {
+ float x1 = line[0];
+ float x2 = line[2];
+ float y1 = line[1];
+ float y2 = line[3];
+ float xdelt = x2 - x1;
+ float ydelt = y2 - y1;
+ if (xdelt == 0 && ydelt == 0)
+ return null;
+ float u = ((point[0] - x1) * xdelt + (point[1] - y1) * ydelt)
+ / (xdelt * xdelt + ydelt * ydelt);
+ float[] ret = {
+ (x1 + u * (x2 - x1)), (y1 + u * (y2 - y1))
+ };
+ float[] vec = {
+ ret[0] - point[0], ret[1] - point[1]
+ };
+ return vec;
+ }
+
+ // A . B
+ public static float dotProduct(float[] a, float[] b) {
+ return a[0] * b[0] + a[1] * b[1];
+ }
+
+ public static float[] normalize(float[] a) {
+ float length = (float) Math.sqrt(a[0] * a[0] + a[1] * a[1]);
+ float[] b = {
+ a[0] / length, a[1] / length
+ };
+ return b;
+ }
+
+ // A onto B
+ public static float scalarProjection(float[] a, float[] b) {
+ float length = (float) Math.sqrt(b[0] * b[0] + b[1] * b[1]);
+ return dotProduct(a, b) / length;
+ }
+
+ public static float[] getVectorFromPoints(float[] point1, float[] point2) {
+ float[] p = {
+ point2[0] - point1[0], point2[1] - point1[1]
+ };
+ return p;
+ }
+
+ public static float[] getUnitVectorFromPoints(float[] point1, float[] point2) {
+ float[] p = {
+ point2[0] - point1[0], point2[1] - point1[1]
+ };
+ float length = (float) Math.sqrt(p[0] * p[0] + p[1] * p[1]);
+ p[0] = p[0] / length;
+ p[1] = p[1] / length;
+ return p;
+ }
+
+ public static void scaleRect(RectF r, float scale) {
+ r.set(r.left * scale, r.top * scale, r.right * scale, r.bottom * scale);
+ }
+
+ // A - B
+ public static float[] vectorSubtract(float[] a, float[] b) {
+ int len = a.length;
+ if (len != b.length)
+ return null;
+ float[] ret = new float[len];
+ for (int i = 0; i < len; i++) {
+ ret[i] = a[i] - b[i];
+ }
+ return ret;
+ }
+
+ public static float vectorLength(float[] a) {
+ return (float) Math.sqrt(a[0] * a[0] + a[1] * a[1]);
+ }
+
+ public static float scale(float oldWidth, float oldHeight, float newWidth, float newHeight) {
+ if (oldHeight == 0 || oldWidth == 0 || (oldWidth == newWidth && oldHeight == newHeight)) {
+ return 1;
+ }
+ return Math.min(newWidth / oldWidth, newHeight / oldHeight);
+ }
+
+ public static Rect roundNearest(RectF r) {
+ Rect q = new Rect(Math.round(r.left), Math.round(r.top), Math.round(r.right),
+ Math.round(r.bottom));
+ return q;
+ }
+
+ private static void concatMirrorMatrix(Matrix m, Mirror type) {
+ if (type == Mirror.HORIZONTAL) {
+ m.postScale(-1, 1);
+ } else if (type == Mirror.VERTICAL) {
+ m.postScale(1, -1);
+ } else if (type == Mirror.BOTH) {
+ m.postScale(1, -1);
+ m.postScale(-1, 1);
+ }
+ }
+
+ private static int getRotationForOrientation(int orientation) {
+ switch (orientation) {
+ case ImageLoader.ORI_ROTATE_90:
+ return 90;
+ case ImageLoader.ORI_ROTATE_180:
+ return 180;
+ case ImageLoader.ORI_ROTATE_270:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+
+ public static GeometryHolder unpackGeometry(Collection<FilterRepresentation> geometry) {
+ GeometryHolder holder = new GeometryHolder();
+ unpackGeometry(holder, geometry);
+ return holder;
+ }
+
+ public static void unpackGeometry(GeometryHolder out,
+ Collection<FilterRepresentation> geometry) {
+ out.wipe();
+ // Get geometry data from filters
+ for (FilterRepresentation r : geometry) {
+ if (r.isNil()) {
+ continue;
+ }
+ if (r.getSerializationName() == FilterRotateRepresentation.SERIALIZATION_NAME) {
+ out.rotation = ((FilterRotateRepresentation) r).getRotation();
+ } else if (r.getSerializationName() ==
+ FilterStraightenRepresentation.SERIALIZATION_NAME) {
+ out.straighten = ((FilterStraightenRepresentation) r).getStraighten();
+ } else if (r.getSerializationName() == FilterCropRepresentation.SERIALIZATION_NAME) {
+ ((FilterCropRepresentation) r).getCrop(out.crop);
+ } else if (r.getSerializationName() == FilterMirrorRepresentation.SERIALIZATION_NAME) {
+ out.mirror = ((FilterMirrorRepresentation) r).getMirror();
+ }
+ }
+ }
+
+ public static void replaceInstances(Collection<FilterRepresentation> geometry,
+ FilterRepresentation rep) {
+ Iterator<FilterRepresentation> iter = geometry.iterator();
+ while (iter.hasNext()) {
+ FilterRepresentation r = iter.next();
+ if (ImagePreset.sameSerializationName(rep, r)) {
+ iter.remove();
+ }
+ }
+ if (!rep.isNil()) {
+ geometry.add(rep);
+ }
+ }
+
+ public static void initializeHolder(GeometryHolder outHolder,
+ FilterRepresentation currentLocal) {
+ Collection<FilterRepresentation> geometry = MasterImage.getImage().getPreset()
+ .getGeometryFilters();
+ replaceInstances(geometry, currentLocal);
+ unpackGeometry(outHolder, geometry);
+ }
+
+ private static Bitmap applyFullGeometryMatrix(Bitmap image, GeometryHolder holder) {
+ int width = image.getWidth();
+ int height = image.getHeight();
+ RectF crop = getTrueCropRect(holder, width, height);
+ Rect frame = new Rect();
+ crop.roundOut(frame);
+ Matrix m = getCropSelectionToScreenMatrix(null, holder, width, height, frame.width(),
+ frame.height());
+ Bitmap temp = Bitmap.createBitmap(frame.width(), frame.height(), Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(temp);
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setFilterBitmap(true);
+ paint.setDither(true);
+ canvas.drawBitmap(image, m, paint);
+ return temp;
+ }
+
+ public static Matrix getImageToScreenMatrix(Collection<FilterRepresentation> geometry,
+ boolean reflectRotation, Rect bmapDimens, float viewWidth, float viewHeight) {
+ GeometryHolder h = unpackGeometry(geometry);
+ return GeometryMathUtils.getOriginalToScreen(h, reflectRotation, bmapDimens.width(),
+ bmapDimens.height(), viewWidth, viewHeight);
+ }
+
+ public static Matrix getOriginalToScreen(GeometryHolder holder, boolean rotate,
+ float originalWidth,
+ float originalHeight, float viewWidth, float viewHeight) {
+ int orientation = MasterImage.getImage().getZoomOrientation();
+ int rotation = getRotationForOrientation(orientation);
+ Rotation prev = holder.rotation;
+ rotation = (rotation + prev.value()) % 360;
+ holder.rotation = Rotation.fromValue(rotation);
+ Matrix m = getCropSelectionToScreenMatrix(null, holder, (int) originalWidth,
+ (int) originalHeight, (int) viewWidth, (int) viewHeight);
+ holder.rotation = prev;
+ return m;
+ }
+
+ public static Bitmap applyGeometryRepresentations(Collection<FilterRepresentation> res,
+ Bitmap image) {
+ GeometryHolder holder = unpackGeometry(res);
+ Bitmap bmap = image;
+ // If there are geometry changes, apply them to the image
+ if (!holder.isNil()) {
+ bmap = applyFullGeometryMatrix(bmap, holder);
+ }
+ return bmap;
+ }
+
+ public static RectF drawTransformedCropped(GeometryHolder holder, Canvas canvas,
+ Bitmap photo, int viewWidth, int viewHeight) {
+ if (photo == null) {
+ return null;
+ }
+ RectF crop = new RectF();
+ Matrix m = getCropSelectionToScreenMatrix(crop, holder, photo.getWidth(), photo.getHeight(),
+ viewWidth, viewHeight);
+ canvas.save();
+ canvas.clipRect(crop);
+ Paint p = new Paint();
+ p.setAntiAlias(true);
+ canvas.drawBitmap(photo, m, p);
+ canvas.restore();
+ return crop;
+ }
+
+ public static boolean needsDimensionSwap(Rotation rotation) {
+ switch (rotation) {
+ case NINETY:
+ case TWO_SEVENTY:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ // Gives matrix for rotated, straightened, mirrored bitmap centered at 0,0.
+ private static Matrix getFullGeometryMatrix(GeometryHolder holder, int bitmapWidth,
+ int bitmapHeight) {
+ float centerX = bitmapWidth / 2f;
+ float centerY = bitmapHeight / 2f;
+ Matrix m = new Matrix();
+ m.setTranslate(-centerX, -centerY);
+ m.postRotate(holder.straighten + holder.rotation.value());
+ concatMirrorMatrix(m, holder.mirror);
+ return m;
+ }
+
+ public static Matrix getFullGeometryToScreenMatrix(GeometryHolder holder, int bitmapWidth,
+ int bitmapHeight, int viewWidth, int viewHeight) {
+ float scale = GeometryMathUtils.scale(bitmapWidth, bitmapHeight, viewWidth, viewHeight);
+ Matrix m = getFullGeometryMatrix(holder, bitmapWidth, bitmapHeight);
+ m.postScale(scale, scale);
+ m.postTranslate(viewWidth / 2f, viewHeight / 2f);
+ return m;
+ }
+
+ public static RectF getTrueCropRect(GeometryHolder holder, int bitmapWidth, int bitmapHeight) {
+ RectF r = new RectF(holder.crop);
+ FilterCropRepresentation.findScaledCrop(r, bitmapWidth, bitmapHeight);
+ float s = holder.straighten;
+ holder.straighten = 0;
+ Matrix m1 = getFullGeometryMatrix(holder, bitmapWidth, bitmapHeight);
+ holder.straighten = s;
+ m1.mapRect(r);
+ return r;
+ }
+
+ public static Matrix getCropSelectionToScreenMatrix(RectF outCrop, GeometryHolder holder,
+ int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight) {
+ Matrix m = getFullGeometryMatrix(holder, bitmapWidth, bitmapHeight);
+ RectF crop = getTrueCropRect(holder, bitmapWidth, bitmapHeight);
+ float scale = GeometryMathUtils.scale(crop.width(), crop.height(), viewWidth, viewHeight);
+ m.postScale(scale, scale);
+ GeometryMathUtils.scaleRect(crop, scale);
+ m.postTranslate(viewWidth / 2f - crop.centerX(), viewHeight / 2f - crop.centerY());
+ if (outCrop != null) {
+ crop.offset(viewWidth / 2f - crop.centerX(), viewHeight / 2f - crop.centerY());
+ outCrop.set(crop);
+ }
+ return m;
+ }
+
+ public static Matrix getCropSelectionToScreenMatrix(RectF outCrop,
+ Collection<FilterRepresentation> res, int bitmapWidth, int bitmapHeight, int viewWidth,
+ int viewHeight) {
+ GeometryHolder holder = unpackGeometry(res);
+ return getCropSelectionToScreenMatrix(outCrop, holder, bitmapWidth, bitmapHeight,
+ viewWidth, viewHeight);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/GradControl.java b/src/com/android/gallery3d/filtershow/imageshow/GradControl.java
new file mode 100644
index 000000000..964da99e9
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/GradControl.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2012 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.filtershow.imageshow;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.DashPathEffect;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.Shader;
+
+import com.android.gallery3d.R;
+
+public class GradControl {
+ private float mPoint1X = Float.NaN; // used to flag parameters have not been set
+ private float mPoint1Y = 0;
+ private float mPoint2X = 200;
+ private float mPoint2Y = 300;
+ private int mMinTouchDist = 80;// should be a resource & in dips
+
+ private float[] handlex = new float[3];
+ private float[] handley = new float[3];
+ private int mSliderColor;
+ private int mCenterDotSize;
+ private float mDownX;
+ private float mDownY;
+ private float mDownPoint1X;
+ private float mDownPoint1Y;
+ private float mDownPoint2X;
+ private float mDownPoint2Y;
+ Rect mImageBounds;
+ int mImageHeight;
+ private Matrix mScrToImg;
+ Paint mPaint = new Paint();
+ DashPathEffect mDash = new DashPathEffect(new float[]{30, 30}, 0);
+ private boolean mShowReshapeHandles = true;
+ public final static int HAN_CENTER = 0;
+ public final static int HAN_NORTH = 2;
+ public final static int HAN_SOUTH = 1;
+ private int[] mPointColorPatern;
+ private int[] mGrayPointColorPatern;
+ private float[] mPointRadialPos = new float[]{0, .3f, .31f, 1};
+ private int mLineColor;
+ private int mlineShadowColor;
+
+ public GradControl(Context context) {
+
+ Resources res = context.getResources();
+ mCenterDotSize = (int) res.getDimension(R.dimen.gradcontrol_dot_size);
+ mMinTouchDist = (int) res.getDimension(R.dimen.gradcontrol_min_touch_dist);
+ int grayPointCenterColor = res.getColor(R.color.gradcontrol_graypoint_center);
+ int grayPointEdgeColor = res.getColor(R.color.gradcontrol_graypoint_edge);
+ int pointCenterColor = res.getColor(R.color.gradcontrol_point_center);
+ int pointEdgeColor = res.getColor(R.color.gradcontrol_point_edge);
+ int pointShadowStartColor = res.getColor(R.color.gradcontrol_point_shadow_start);
+ int pointShadowEndColor = res.getColor(R.color.gradcontrol_point_shadow_end);
+ mPointColorPatern = new int[]{
+ pointCenterColor, pointEdgeColor, pointShadowStartColor, pointShadowEndColor};
+ mGrayPointColorPatern = new int[]{
+ grayPointCenterColor, grayPointEdgeColor, pointShadowStartColor, pointShadowEndColor};
+ mSliderColor = Color.WHITE;
+ mLineColor = res.getColor(R.color.gradcontrol_line_color);
+ mlineShadowColor = res.getColor(R.color.gradcontrol_line_shadow);
+ }
+
+ public void setPoint2(float x, float y) {
+ mPoint2X = x;
+ mPoint2Y = y;
+ }
+
+ public void setPoint1(float x, float y) {
+ mPoint1X = x;
+ mPoint1Y = y;
+ }
+
+ public int getCloseHandle(float x, float y) {
+ float min = Float.MAX_VALUE;
+ int handle = -1;
+ for (int i = 0; i < handlex.length; i++) {
+ float dx = handlex[i] - x;
+ float dy = handley[i] - y;
+ float dist = dx * dx + dy * dy;
+ if (dist < min) {
+ min = dist;
+ handle = i;
+ }
+ }
+
+ if (min < mMinTouchDist * mMinTouchDist) {
+ return handle;
+ }
+ for (int i = 0; i < handlex.length; i++) {
+ float dx = handlex[i] - x;
+ float dy = handley[i] - y;
+ float dist = (float) Math.sqrt(dx * dx + dy * dy);
+ }
+
+ return -1;
+ }
+
+ public void setScrImageInfo(Matrix scrToImg, Rect imageBounds) {
+ mScrToImg = scrToImg;
+ mImageBounds = new Rect(imageBounds);
+ }
+
+ private boolean centerIsOutside(float x1, float y1, float x2, float y2) {
+ return (!mImageBounds.contains((int) ((x1 + x2) / 2), (int) ((y1 + y2) / 2)));
+ }
+
+ public void actionDown(float x, float y, Line line) {
+ float[] point = new float[]{
+ x, y};
+ mScrToImg.mapPoints(point);
+ mDownX = point[0];
+ mDownY = point[1];
+ mDownPoint1X = line.getPoint1X();
+ mDownPoint1Y = line.getPoint1Y();
+ mDownPoint2X = line.getPoint2X();
+ mDownPoint2Y = line.getPoint2Y();
+ }
+
+ public void actionMove(int handle, float x, float y, Line line) {
+ float[] point = new float[]{
+ x, y};
+ mScrToImg.mapPoints(point);
+ x = point[0];
+ y = point[1];
+
+ // Test if the matrix is swapping x and y
+ point[0] = 0;
+ point[1] = 1;
+ mScrToImg.mapVectors(point);
+ boolean swapxy = (point[0] > 0.0f);
+
+ int sign = 1;
+
+ float dx = x - mDownX;
+ float dy = y - mDownY;
+ switch (handle) {
+ case HAN_CENTER:
+ if (centerIsOutside(mDownPoint1X + dx, mDownPoint1Y + dy,
+ mDownPoint2X + dx, mDownPoint2Y + dy)) {
+ break;
+ }
+ line.setPoint1(mDownPoint1X + dx, mDownPoint1Y + dy);
+ line.setPoint2(mDownPoint2X + dx, mDownPoint2Y + dy);
+ break;
+ case HAN_SOUTH:
+ if (centerIsOutside(mDownPoint1X + dx, mDownPoint1Y + dy,
+ mDownPoint2X, mDownPoint2Y)) {
+ break;
+ }
+ line.setPoint1(mDownPoint1X + dx, mDownPoint1Y + dy);
+ break;
+ case HAN_NORTH:
+ if (centerIsOutside(mDownPoint1X, mDownPoint1Y,
+ mDownPoint2X + dx, mDownPoint2Y + dy)) {
+ break;
+ }
+ line.setPoint2(mDownPoint2X + dx, mDownPoint2Y + dy);
+ break;
+ }
+ }
+
+ public void paintGrayPoint(Canvas canvas, float x, float y) {
+ if (isUndefined()) {
+ return;
+ }
+
+ Paint paint = new Paint();
+ paint.setStyle(Paint.Style.FILL);
+ RadialGradient g = new RadialGradient(x, y, mCenterDotSize, mGrayPointColorPatern,
+ mPointRadialPos, Shader.TileMode.CLAMP);
+ paint.setShader(g);
+ canvas.drawCircle(x, y, mCenterDotSize, paint);
+ }
+
+ public void paintPoint(Canvas canvas, float x, float y) {
+ if (isUndefined()) {
+ return;
+ }
+
+ Paint paint = new Paint();
+ paint.setStyle(Paint.Style.FILL);
+ RadialGradient g = new RadialGradient(x, y, mCenterDotSize, mPointColorPatern,
+ mPointRadialPos, Shader.TileMode.CLAMP);
+ paint.setShader(g);
+ canvas.drawCircle(x, y, mCenterDotSize, paint);
+ }
+
+ void paintLines(Canvas canvas, float p1x, float p1y, float p2x, float p2y) {
+ if (isUndefined()) {
+ return;
+ }
+
+ mPaint.setAntiAlias(true);
+ mPaint.setStyle(Paint.Style.STROKE);
+
+ mPaint.setStrokeWidth(6);
+ mPaint.setColor(mlineShadowColor);
+ mPaint.setPathEffect(mDash);
+ paintOvallines(canvas, mPaint, p1x, p1y, p2x, p2y);
+
+ mPaint.setStrokeWidth(3);
+ mPaint.setColor(mLineColor);
+ mPaint.setPathEffect(mDash);
+ paintOvallines(canvas, mPaint, p1x, p1y, p2x, p2y);
+ }
+
+ public void paintOvallines(
+ Canvas canvas, Paint paint, float p1x, float p1y, float p2x, float p2y) {
+
+
+
+ canvas.drawLine(p1x, p1y, p2x, p2y, paint);
+
+ float cx = (p1x + p2x) / 2;
+ float cy = (p1y + p2y) / 2;
+ float dx = p1x - p2x;
+ float dy = p1y - p2y;
+ float len = (float) Math.sqrt(dx * dx + dy * dy);
+ dx *= 2048 / len;
+ dy *= 2048 / len;
+
+ canvas.drawLine(p1x + dy, p1y - dx, p1x - dy, p1y + dx, paint);
+ canvas.drawLine(p2x + dy, p2y - dx, p2x - dy, p2y + dx, paint);
+ }
+
+ public void fillHandles(Canvas canvas, float p1x, float p1y, float p2x, float p2y) {
+ float cx = (p1x + p2x) / 2;
+ float cy = (p1y + p2y) / 2;
+ handlex[0] = cx;
+ handley[0] = cy;
+ handlex[1] = p1x;
+ handley[1] = p1y;
+ handlex[2] = p2x;
+ handley[2] = p2y;
+
+ }
+
+ public void draw(Canvas canvas) {
+ paintLines(canvas, mPoint1X, mPoint1Y, mPoint2X, mPoint2Y);
+ fillHandles(canvas, mPoint1X, mPoint1Y, mPoint2X, mPoint2Y);
+ paintPoint(canvas, mPoint2X, mPoint2Y);
+ paintPoint(canvas, mPoint1X, mPoint1Y);
+ paintPoint(canvas, (mPoint1X + mPoint2X) / 2, (mPoint1Y + mPoint2Y) / 2);
+ }
+
+ public boolean isUndefined() {
+ return Float.isNaN(mPoint1X);
+ }
+
+ public void setShowReshapeHandles(boolean showReshapeHandles) {
+ this.mShowReshapeHandles = showReshapeHandles;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
new file mode 100644
index 000000000..7fee03188
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2013 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.filtershow.imageshow;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.crop.CropDrawingUtils;
+import com.android.gallery3d.filtershow.crop.CropMath;
+import com.android.gallery3d.filtershow.crop.CropObject;
+import com.android.gallery3d.filtershow.editors.EditorCrop;
+import com.android.gallery3d.filtershow.filters.FilterCropRepresentation;
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder;
+
+public class ImageCrop extends ImageShow {
+ private static final String TAG = ImageCrop.class.getSimpleName();
+ private RectF mImageBounds = new RectF();
+ private RectF mScreenCropBounds = new RectF();
+ private Paint mPaint = new Paint();
+ private CropObject mCropObj = null;
+ private GeometryHolder mGeometry = new GeometryHolder();
+ private GeometryHolder mUpdateHolder = new GeometryHolder();
+ private Drawable mCropIndicator;
+ private int mIndicatorSize;
+ private boolean mMovingBlock = false;
+ private Matrix mDisplayMatrix = null;
+ private Matrix mDisplayCropMatrix = null;
+ private Matrix mDisplayMatrixInverse = null;
+ private float mPrevX = 0;
+ private float mPrevY = 0;
+ private int mMinSideSize = 90;
+ private int mTouchTolerance = 40;
+ private enum Mode {
+ NONE, MOVE
+ }
+ private Mode mState = Mode.NONE;
+ private boolean mValidDraw = false;
+ FilterCropRepresentation mLocalRep = new FilterCropRepresentation();
+ EditorCrop mEditorCrop;
+
+ public ImageCrop(Context context) {
+ super(context);
+ setup(context);
+ }
+
+ public ImageCrop(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setup(context);
+ }
+
+ public ImageCrop(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setup(context);
+ }
+
+ private void setup(Context context) {
+ Resources rsc = context.getResources();
+ mCropIndicator = rsc.getDrawable(R.drawable.camera_crop);
+ mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size);
+ mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side);
+ mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance);
+ }
+
+ public void setFilterCropRepresentation(FilterCropRepresentation crop) {
+ mLocalRep = (crop == null) ? new FilterCropRepresentation() : crop;
+ GeometryMathUtils.initializeHolder(mUpdateHolder, mLocalRep);
+ mValidDraw = true;
+ }
+
+ public FilterCropRepresentation getFinalRepresentation() {
+ return mLocalRep;
+ }
+
+ private void internallyUpdateLocalRep(RectF crop, RectF image) {
+ FilterCropRepresentation
+ .findNormalizedCrop(crop, (int) image.width(), (int) image.height());
+ mGeometry.crop.set(crop);
+ mUpdateHolder.set(mGeometry);
+ mLocalRep.setCrop(crop);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+ if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
+ return true;
+ }
+ float[] touchPoint = {
+ x, y
+ };
+ mDisplayMatrixInverse.mapPoints(touchPoint);
+ x = touchPoint[0];
+ y = touchPoint[1];
+ switch (event.getActionMasked()) {
+ case (MotionEvent.ACTION_DOWN):
+ if (mState == Mode.NONE) {
+ if (!mCropObj.selectEdge(x, y)) {
+ mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK);
+ }
+ mPrevX = x;
+ mPrevY = y;
+ mState = Mode.MOVE;
+ }
+ break;
+ case (MotionEvent.ACTION_UP):
+ if (mState == Mode.MOVE) {
+ mCropObj.selectEdge(CropObject.MOVE_NONE);
+ mMovingBlock = false;
+ mPrevX = x;
+ mPrevY = y;
+ mState = Mode.NONE;
+ internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds());
+ }
+ break;
+ case (MotionEvent.ACTION_MOVE):
+ if (mState == Mode.MOVE) {
+ float dx = x - mPrevX;
+ float dy = y - mPrevY;
+ mCropObj.moveCurrentSelection(dx, dy);
+ mPrevX = x;
+ mPrevY = y;
+ }
+ break;
+ default:
+ break;
+ }
+ invalidate();
+ return true;
+ }
+
+ private void clearDisplay() {
+ mDisplayMatrix = null;
+ mDisplayMatrixInverse = null;
+ invalidate();
+ }
+
+ public void applyFreeAspect() {
+ mCropObj.unsetAspectRatio();
+ invalidate();
+ }
+
+ public void applyOriginalAspect() {
+ RectF outer = mCropObj.getOuterBounds();
+ float w = outer.width();
+ float h = outer.height();
+ if (w > 0 && h > 0) {
+ applyAspect(w, h);
+ mCropObj.resetBoundsTo(outer, outer);
+ internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds());
+ } else {
+ Log.w(TAG, "failed to set aspect ratio original");
+ }
+ invalidate();
+ }
+
+ public void applyAspect(float x, float y) {
+ if (x <= 0 || y <= 0) {
+ throw new IllegalArgumentException("Bad arguments to applyAspect");
+ }
+ // If we are rotated by 90 degrees from horizontal, swap x and y
+ if (GeometryMathUtils.needsDimensionSwap(mGeometry.rotation)) {
+ float tmp = x;
+ x = y;
+ y = tmp;
+ }
+ if (!mCropObj.setInnerAspectRatio(x, y)) {
+ Log.w(TAG, "failed to set aspect ratio");
+ }
+ internallyUpdateLocalRep(mCropObj.getInnerBounds(), mCropObj.getOuterBounds());
+ invalidate();
+ }
+
+ /**
+ * Rotates first d bits in integer x to the left some number of times.
+ */
+ private int bitCycleLeft(int x, int times, int d) {
+ int mask = (1 << d) - 1;
+ int mout = x & mask;
+ times %= d;
+ int hi = mout >> (d - times);
+ int low = (mout << times) & mask;
+ int ret = x & ~mask;
+ ret |= low;
+ ret |= hi;
+ return ret;
+ }
+
+ /**
+ * Find the selected edge or corner in screen coordinates.
+ */
+ private int decode(int movingEdges, float rotation) {
+ int rot = CropMath.constrainedRotation(rotation);
+ switch (rot) {
+ case 90:
+ return bitCycleLeft(movingEdges, 1, 4);
+ case 180:
+ return bitCycleLeft(movingEdges, 2, 4);
+ case 270:
+ return bitCycleLeft(movingEdges, 3, 4);
+ default:
+ return movingEdges;
+ }
+ }
+
+ private void forceStateConsistency() {
+ MasterImage master = MasterImage.getImage();
+ Bitmap image = master.getFiltersOnlyImage();
+ int width = image.getWidth();
+ int height = image.getHeight();
+ if (mCropObj == null || !mUpdateHolder.equals(mGeometry)
+ || mImageBounds.width() != width || mImageBounds.height() != height
+ || !mLocalRep.getCrop().equals(mUpdateHolder.crop)) {
+ mImageBounds.set(0, 0, width, height);
+ mGeometry.set(mUpdateHolder);
+ mLocalRep.setCrop(mUpdateHolder.crop);
+ RectF scaledCrop = new RectF(mUpdateHolder.crop);
+ FilterCropRepresentation.findScaledCrop(scaledCrop, width, height);
+ mCropObj = new CropObject(mImageBounds, scaledCrop, (int) mUpdateHolder.straighten);
+ mState = Mode.NONE;
+ clearDisplay();
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ clearDisplay();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ Bitmap bitmap = MasterImage.getImage().getFiltersOnlyImage();
+ if (!mValidDraw || bitmap == null) {
+ return;
+ }
+ forceStateConsistency();
+ mImageBounds.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ // If display matrix doesn't exist, create it and its dependencies
+ if (mDisplayCropMatrix == null || mDisplayMatrix == null || mDisplayMatrixInverse == null) {
+ mDisplayMatrix = GeometryMathUtils.getFullGeometryToScreenMatrix(mGeometry,
+ bitmap.getWidth(), bitmap.getHeight(), canvas.getWidth(), canvas.getHeight());
+ float straighten = mGeometry.straighten;
+ mGeometry.straighten = 0;
+ mDisplayCropMatrix = GeometryMathUtils.getFullGeometryToScreenMatrix(mGeometry,
+ bitmap.getWidth(), bitmap.getHeight(), canvas.getWidth(), canvas.getHeight());
+ mGeometry.straighten = straighten;
+ mDisplayMatrixInverse = new Matrix();
+ mDisplayMatrixInverse.reset();
+ if (!mDisplayCropMatrix.invert(mDisplayMatrixInverse)) {
+ Log.w(TAG, "could not invert display matrix");
+ mDisplayMatrixInverse = null;
+ return;
+ }
+ // Scale min side and tolerance by display matrix scale factor
+ mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize));
+ mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance));
+ }
+ // Draw actual bitmap
+ mPaint.reset();
+ mPaint.setAntiAlias(true);
+ mPaint.setFilterBitmap(true);
+ canvas.drawBitmap(bitmap, mDisplayMatrix, mPaint);
+ mCropObj.getInnerBounds(mScreenCropBounds);
+ RectF outer = mCropObj.getOuterBounds();
+ FilterCropRepresentation.findNormalizedCrop(mScreenCropBounds, (int) outer.width(),
+ (int) outer.height());
+ FilterCropRepresentation.findScaledCrop(mScreenCropBounds, bitmap.getWidth(),
+ bitmap.getHeight());
+ if (mDisplayCropMatrix.mapRect(mScreenCropBounds)) {
+ // Draw crop rect and markers
+ CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds);
+ CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds);
+ CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize,
+ mScreenCropBounds, mCropObj.isFixedAspect(),
+ decode(mCropObj.getSelectState(), mGeometry.rotation.value()));
+ }
+ }
+
+ public void setEditor(EditorCrop editorCrop) {
+ mEditorCrop = editorCrop;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java b/src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java
new file mode 100644
index 000000000..82c4b2fc7
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageCurves.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2012 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.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.os.AsyncTask;
+import android.util.AttributeSet;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.Editor;
+import com.android.gallery3d.filtershow.editors.EditorCurves;
+import com.android.gallery3d.filtershow.filters.FilterCurvesRepresentation;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.ImageFilterCurves;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+
+import java.util.HashMap;
+
+public class ImageCurves extends ImageShow {
+
+ private static final String LOGTAG = "ImageCurves";
+ Paint gPaint = new Paint();
+ Path gPathSpline = new Path();
+ HashMap<Integer, String> mIdStrLut;
+
+ private int mCurrentCurveIndex = Spline.RGB;
+ private boolean mDidAddPoint = false;
+ private boolean mDidDelete = false;
+ private ControlPoint mCurrentControlPoint = null;
+ private int mCurrentPick = -1;
+ private ImagePreset mLastPreset = null;
+ int[] redHistogram = new int[256];
+ int[] greenHistogram = new int[256];
+ int[] blueHistogram = new int[256];
+ Path gHistoPath = new Path();
+
+ boolean mDoingTouchMove = false;
+ private EditorCurves mEditorCurves;
+ private FilterCurvesRepresentation mFilterCurvesRepresentation;
+
+ public ImageCurves(Context context) {
+ super(context);
+ setLayerType(LAYER_TYPE_SOFTWARE, gPaint);
+ resetCurve();
+ }
+
+ public ImageCurves(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setLayerType(LAYER_TYPE_SOFTWARE, gPaint);
+ resetCurve();
+ }
+
+ @Override
+ protected boolean enableComparison() {
+ return false;
+ }
+
+ @Override
+ public boolean useUtilityPanel() {
+ return true;
+ }
+
+ private void showPopupMenu(LinearLayout accessoryViewList) {
+ final Button button = (Button) accessoryViewList.findViewById(
+ R.id.applyEffect);
+ if (button == null) {
+ return;
+ }
+ if (mIdStrLut == null){
+ mIdStrLut = new HashMap<Integer, String>();
+ mIdStrLut.put(R.id.curve_menu_rgb,
+ getContext().getString(R.string.curves_channel_rgb));
+ mIdStrLut.put(R.id.curve_menu_red,
+ getContext().getString(R.string.curves_channel_red));
+ mIdStrLut.put(R.id.curve_menu_green,
+ getContext().getString(R.string.curves_channel_green));
+ mIdStrLut.put(R.id.curve_menu_blue,
+ getContext().getString(R.string.curves_channel_blue));
+ }
+ PopupMenu popupMenu = new PopupMenu(getActivity(), button);
+ popupMenu.getMenuInflater().inflate(R.menu.filtershow_menu_curves, popupMenu.getMenu());
+ popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ setChannel(item.getItemId());
+ button.setText(mIdStrLut.get(item.getItemId()));
+ return true;
+ }
+ });
+ Editor.hackFixStrings(popupMenu.getMenu());
+ popupMenu.show();
+ }
+
+ @Override
+ public void openUtilityPanel(final LinearLayout accessoryViewList) {
+ Context context = accessoryViewList.getContext();
+ Button view = (Button) accessoryViewList.findViewById(R.id.applyEffect);
+ view.setText(context.getString(R.string.curves_channel_rgb));
+ view.setVisibility(View.VISIBLE);
+
+ view.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ showPopupMenu(accessoryViewList);
+ }
+ });
+
+ if (view != null) {
+ view.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void nextChannel() {
+ mCurrentCurveIndex = ((mCurrentCurveIndex + 1) % 4);
+ invalidate();
+ }
+
+ private ImageFilterCurves curves() {
+ String filterName = getFilterName();
+ ImagePreset p = getImagePreset();
+ if (p != null) {
+ return (ImageFilterCurves) FiltersManager.getManager().getFilter(ImageFilterCurves.class);
+ }
+ return null;
+ }
+
+ private Spline getSpline(int index) {
+ return mFilterCurvesRepresentation.getSpline(index);
+ }
+
+ @Override
+ public void resetParameter() {
+ super.resetParameter();
+ resetCurve();
+ mLastPreset = null;
+ invalidate();
+ }
+
+ public void resetCurve() {
+ if (mFilterCurvesRepresentation != null) {
+ mFilterCurvesRepresentation.reset();
+ updateCachedImage();
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mFilterCurvesRepresentation == null) {
+ return;
+ }
+
+ gPaint.setAntiAlias(true);
+
+ if (getImagePreset() != mLastPreset && getFilteredImage() != null) {
+ new ComputeHistogramTask().execute(getFilteredImage());
+ mLastPreset = getImagePreset();
+ }
+
+ if (curves() == null) {
+ return;
+ }
+
+ if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.RED) {
+ drawHistogram(canvas, redHistogram, Color.RED, PorterDuff.Mode.SCREEN);
+ }
+ if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.GREEN) {
+ drawHistogram(canvas, greenHistogram, Color.GREEN, PorterDuff.Mode.SCREEN);
+ }
+ if (mCurrentCurveIndex == Spline.RGB || mCurrentCurveIndex == Spline.BLUE) {
+ drawHistogram(canvas, blueHistogram, Color.BLUE, PorterDuff.Mode.SCREEN);
+ }
+ // We only display the other channels curves when showing the RGB curve
+ if (mCurrentCurveIndex == Spline.RGB) {
+ for (int i = 0; i < 4; i++) {
+ Spline spline = getSpline(i);
+ if (i != mCurrentCurveIndex && !spline.isOriginal()) {
+ // And we only display a curve if it has more than two
+ // points
+ spline.draw(canvas, Spline.colorForCurve(i), getWidth(),
+ getHeight(), false, mDoingTouchMove);
+ }
+ }
+ }
+ // ...but we always display the current curve.
+ getSpline(mCurrentCurveIndex)
+ .draw(canvas, Spline.colorForCurve(mCurrentCurveIndex), getWidth(), getHeight(),
+ true, mDoingTouchMove);
+
+ }
+
+ private int pickControlPoint(float x, float y) {
+ int pick = 0;
+ Spline spline = getSpline(mCurrentCurveIndex);
+ float px = spline.getPoint(0).x;
+ float py = spline.getPoint(0).y;
+ double delta = Math.sqrt((px - x) * (px - x) + (py - y) * (py - y));
+ for (int i = 1; i < spline.getNbPoints(); i++) {
+ px = spline.getPoint(i).x;
+ py = spline.getPoint(i).y;
+ double currentDelta = Math.sqrt((px - x) * (px - x) + (py - y)
+ * (py - y));
+ if (currentDelta < delta) {
+ delta = currentDelta;
+ pick = i;
+ }
+ }
+
+ if (!mDidAddPoint && (delta * getWidth() > 100)
+ && (spline.getNbPoints() < 10)) {
+ return -1;
+ }
+
+ return pick;
+ }
+
+ private String getFilterName() {
+ return "Curves";
+ }
+
+ @Override
+ public synchronized boolean onTouchEvent(MotionEvent e) {
+ if (e.getPointerCount() != 1) {
+ return true;
+ }
+
+ if (didFinishScalingOperation()) {
+ return true;
+ }
+
+ float margin = Spline.curveHandleSize() / 2;
+ float posX = e.getX();
+ if (posX < margin) {
+ posX = margin;
+ }
+ float posY = e.getY();
+ if (posY < margin) {
+ posY = margin;
+ }
+ if (posX > getWidth() - margin) {
+ posX = getWidth() - margin;
+ }
+ if (posY > getHeight() - margin) {
+ posY = getHeight() - margin;
+ }
+ posX = (posX - margin) / (getWidth() - 2 * margin);
+ posY = (posY - margin) / (getHeight() - 2 * margin);
+
+ if (e.getActionMasked() == MotionEvent.ACTION_UP) {
+ mCurrentControlPoint = null;
+ mCurrentPick = -1;
+ updateCachedImage();
+ mDidAddPoint = false;
+ if (mDidDelete) {
+ mDidDelete = false;
+ }
+ mDoingTouchMove = false;
+ return true;
+ }
+
+ if (mDidDelete) {
+ return true;
+ }
+
+ if (curves() == null) {
+ return true;
+ }
+
+ if (e.getActionMasked() == MotionEvent.ACTION_MOVE) {
+ mDoingTouchMove = true;
+ Spline spline = getSpline(mCurrentCurveIndex);
+ int pick = mCurrentPick;
+ if (mCurrentControlPoint == null) {
+ pick = pickControlPoint(posX, posY);
+ if (pick == -1) {
+ mCurrentControlPoint = new ControlPoint(posX, posY);
+ pick = spline.addPoint(mCurrentControlPoint);
+ mDidAddPoint = true;
+ } else {
+ mCurrentControlPoint = spline.getPoint(pick);
+ }
+ mCurrentPick = pick;
+ }
+
+ if (spline.isPointContained(posX, pick)) {
+ spline.movePoint(pick, posX, posY);
+ } else if (pick != -1 && spline.getNbPoints() > 2) {
+ spline.deletePoint(pick);
+ mDidDelete = true;
+ }
+ updateCachedImage();
+ invalidate();
+ }
+ return true;
+ }
+
+ public synchronized void updateCachedImage() {
+ if (getImagePreset() != null) {
+ resetImageCaches(this);
+ if (mEditorCurves != null) {
+ mEditorCurves.commitLocalRepresentation();
+ }
+ invalidate();
+ }
+ }
+
+ class ComputeHistogramTask extends AsyncTask<Bitmap, Void, int[]> {
+ @Override
+ protected int[] doInBackground(Bitmap... params) {
+ int[] histo = new int[256 * 3];
+ Bitmap bitmap = params[0];
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+ int[] pixels = new int[w * h];
+ bitmap.getPixels(pixels, 0, w, 0, 0, w, h);
+ for (int i = 0; i < w; i++) {
+ for (int j = 0; j < h; j++) {
+ int index = j * w + i;
+ int r = Color.red(pixels[index]);
+ int g = Color.green(pixels[index]);
+ int b = Color.blue(pixels[index]);
+ histo[r]++;
+ histo[256 + g]++;
+ histo[512 + b]++;
+ }
+ }
+ return histo;
+ }
+
+ @Override
+ protected void onPostExecute(int[] result) {
+ System.arraycopy(result, 0, redHistogram, 0, 256);
+ System.arraycopy(result, 256, greenHistogram, 0, 256);
+ System.arraycopy(result, 512, blueHistogram, 0, 256);
+ invalidate();
+ }
+ }
+
+ private void drawHistogram(Canvas canvas, int[] histogram, int color, PorterDuff.Mode mode) {
+ int max = 0;
+ for (int i = 0; i < histogram.length; i++) {
+ if (histogram[i] > max) {
+ max = histogram[i];
+ }
+ }
+ float w = getWidth() - Spline.curveHandleSize();
+ float h = getHeight() - Spline.curveHandleSize() / 2.0f;
+ float dx = Spline.curveHandleSize() / 2.0f;
+ float wl = w / histogram.length;
+ float wh = (0.3f * h) / max;
+ Paint paint = new Paint();
+ paint.setARGB(100, 255, 255, 255);
+ paint.setStrokeWidth((int) Math.ceil(wl));
+
+ Paint paint2 = new Paint();
+ paint2.setColor(color);
+ paint2.setStrokeWidth(6);
+ paint2.setXfermode(new PorterDuffXfermode(mode));
+ gHistoPath.reset();
+ gHistoPath.moveTo(dx, h);
+ boolean firstPointEncountered = false;
+ float prev = 0;
+ float last = 0;
+ for (int i = 0; i < histogram.length; i++) {
+ float x = i * wl + dx;
+ float l = histogram[i] * wh;
+ if (l != 0) {
+ float v = h - (l + prev) / 2.0f;
+ if (!firstPointEncountered) {
+ gHistoPath.lineTo(x, h);
+ firstPointEncountered = true;
+ }
+ gHistoPath.lineTo(x, v);
+ prev = l;
+ last = x;
+ }
+ }
+ gHistoPath.lineTo(last, h);
+ gHistoPath.lineTo(w, h);
+ gHistoPath.close();
+ canvas.drawPath(gHistoPath, paint2);
+ paint2.setStrokeWidth(2);
+ paint2.setStyle(Paint.Style.STROKE);
+ paint2.setARGB(255, 200, 200, 200);
+ canvas.drawPath(gHistoPath, paint2);
+ }
+
+ public void setChannel(int itemId) {
+ switch (itemId) {
+ case R.id.curve_menu_rgb: {
+ mCurrentCurveIndex = Spline.RGB;
+ break;
+ }
+ case R.id.curve_menu_red: {
+ mCurrentCurveIndex = Spline.RED;
+ break;
+ }
+ case R.id.curve_menu_green: {
+ mCurrentCurveIndex = Spline.GREEN;
+ break;
+ }
+ case R.id.curve_menu_blue: {
+ mCurrentCurveIndex = Spline.BLUE;
+ break;
+ }
+ }
+ mEditorCurves.commitLocalRepresentation();
+ invalidate();
+ }
+
+ public void setEditor(EditorCurves editorCurves) {
+ mEditorCurves = editorCurves;
+ }
+
+ public void setFilterDrawRepresentation(FilterCurvesRepresentation drawRep) {
+ mFilterCurvesRepresentation = drawRep;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java b/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java
new file mode 100644
index 000000000..9722034e0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageDraw.java
@@ -0,0 +1,139 @@
+
+package com.android.gallery3d.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.filtershow.editors.EditorDraw;
+import com.android.gallery3d.filtershow.filters.FilterDrawRepresentation;
+import com.android.gallery3d.filtershow.filters.ImageFilterDraw;
+
+public class ImageDraw extends ImageShow {
+
+ private static final String LOGTAG = "ImageDraw";
+ private int mCurrentColor = Color.RED;
+ final static float INITAL_STROKE_RADIUS = 40;
+ private float mCurrentSize = INITAL_STROKE_RADIUS;
+ private byte mType = 0;
+ private FilterDrawRepresentation mFRep;
+ private EditorDraw mEditorDraw;
+
+ public ImageDraw(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ resetParameter();
+ }
+
+ public ImageDraw(Context context) {
+ super(context);
+ resetParameter();
+ }
+
+ public void setEditor(EditorDraw editorDraw) {
+ mEditorDraw = editorDraw;
+ }
+ public void setFilterDrawRepresentation(FilterDrawRepresentation fr) {
+ mFRep = fr;
+ }
+
+ public Drawable getIcon(Context context) {
+
+ return null;
+ }
+
+ @Override
+ public void resetParameter() {
+ if (mFRep != null) {
+ mFRep.clear();
+ }
+ }
+
+ public void setColor(int color) {
+ mCurrentColor = color;
+ }
+
+ public void setSize(int size) {
+ mCurrentSize = size;
+ }
+
+ public void setStyle(byte style) {
+ mType = (byte) (style % ImageFilterDraw.NUMBER_OF_STYLES);
+ }
+
+ public int getStyle() {
+ return mType;
+ }
+
+ public int getSize() {
+ return (int) mCurrentSize;
+ }
+
+ float[] mTmpPoint = new float[2]; // so we do not malloc
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getPointerCount() > 1) {
+ boolean ret = super.onTouchEvent(event);
+ if (mFRep.getCurrentDrawing() != null) {
+ mFRep.clearCurrentSection();
+ mEditorDraw.commitLocalRepresentation();
+ }
+ return ret;
+ }
+ if (event.getAction() != MotionEvent.ACTION_DOWN) {
+ if (mFRep.getCurrentDrawing() == null) {
+ return super.onTouchEvent(event);
+ }
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ calcScreenMapping();
+ mTmpPoint[0] = event.getX();
+ mTmpPoint[1] = event.getY();
+ mToOrig.mapPoints(mTmpPoint);
+ mFRep.startNewSection(mType, mCurrentColor, mCurrentSize, mTmpPoint[0], mTmpPoint[1]);
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_MOVE) {
+
+ int historySize = event.getHistorySize();
+ for (int h = 0; h < historySize; h++) {
+ int p = 0;
+ {
+ mTmpPoint[0] = event.getHistoricalX(p, h);
+ mTmpPoint[1] = event.getHistoricalY(p, h);
+ mToOrig.mapPoints(mTmpPoint);
+ mFRep.addPoint(mTmpPoint[0], mTmpPoint[1]);
+ }
+ }
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ mTmpPoint[0] = event.getX();
+ mTmpPoint[1] = event.getY();
+ mToOrig.mapPoints(mTmpPoint);
+ mFRep.endSection(mTmpPoint[0], mTmpPoint[1]);
+ }
+ mEditorDraw.commitLocalRepresentation();
+ invalidate();
+ return true;
+ }
+
+ Matrix mRotateToScreen = new Matrix();
+ Matrix mToOrig;
+ private void calcScreenMapping() {
+ mToOrig = getScreenToImageMatrix(true);
+ mToOrig.invert(mRotateToScreen);
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ calcScreenMapping();
+
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java b/src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java
new file mode 100644
index 000000000..b55cc2bc4
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageGrad.java
@@ -0,0 +1,215 @@
+package com.android.gallery3d.filtershow.imageshow;
+/*
+ * Copyright (C) 2013 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.
+ */
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.editors.EditorGrad;
+import com.android.gallery3d.filtershow.filters.FilterGradRepresentation;
+
+public class ImageGrad extends ImageShow {
+ private static final String LOGTAG = "ImageGrad";
+ private FilterGradRepresentation mGradRep;
+ private EditorGrad mEditorGrad;
+ private float mMinTouchDist;
+ private int mActiveHandle = -1;
+ private GradControl mEllipse;
+
+ Matrix mToScr = new Matrix();
+ float[] mPointsX = new float[FilterGradRepresentation.MAX_POINTS];
+ float[] mPointsY = new float[FilterGradRepresentation.MAX_POINTS];
+
+ public ImageGrad(Context context) {
+ super(context);
+ Resources res = context.getResources();
+ mMinTouchDist = res.getDimensionPixelSize(R.dimen.gradcontrol_min_touch_dist);
+ mEllipse = new GradControl(context);
+ mEllipse.setShowReshapeHandles(false);
+ }
+
+ public ImageGrad(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ Resources res = context.getResources();
+ mMinTouchDist = res.getDimensionPixelSize(R.dimen.gradcontrol_min_touch_dist);
+ mEllipse = new GradControl(context);
+ mEllipse.setShowReshapeHandles(false);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ int mask = event.getActionMasked();
+
+ if (mActiveHandle == -1) {
+ if (MotionEvent.ACTION_DOWN != mask) {
+ return super.onTouchEvent(event);
+ }
+ if (event.getPointerCount() == 1) {
+ mActiveHandle = mEllipse.getCloseHandle(event.getX(), event.getY());
+ if (mActiveHandle == -1) {
+ float x = event.getX();
+ float y = event.getY();
+ float min_d = Float.MAX_VALUE;
+ int pos = -1;
+ for (int i = 0; i < mPointsX.length; i++) {
+ if (mPointsX[i] == -1) {
+ continue;
+ }
+ float d = (float) Math.hypot(x - mPointsX[i], y - mPointsY[i]);
+ if ( min_d > d) {
+ min_d = d;
+ pos = i;
+ }
+ }
+ if (min_d > mMinTouchDist){
+ pos = -1;
+ }
+
+ if (pos != -1) {
+ mGradRep.setSelectedPoint(pos);
+ resetImageCaches(this);
+ mEditorGrad.updateSeekBar(mGradRep);
+ mEditorGrad.commitLocalRepresentation();
+ invalidate();
+ }
+ }
+ }
+ if (mActiveHandle == -1) {
+ return super.onTouchEvent(event);
+ }
+ } else {
+ switch (mask) {
+ case MotionEvent.ACTION_UP: {
+
+ mActiveHandle = -1;
+ break;
+ }
+ case MotionEvent.ACTION_DOWN: {
+ break;
+ }
+ }
+ }
+ float x = event.getX();
+ float y = event.getY();
+
+ mEllipse.setScrImageInfo(getScreenToImageMatrix(true),
+ MasterImage.getImage().getOriginalBounds());
+
+ switch (mask) {
+ case (MotionEvent.ACTION_DOWN): {
+ mEllipse.actionDown(x, y, mGradRep);
+ break;
+ }
+ case (MotionEvent.ACTION_UP):
+ case (MotionEvent.ACTION_MOVE): {
+ mEllipse.actionMove(mActiveHandle, x, y, mGradRep);
+ setRepresentation(mGradRep);
+ break;
+ }
+ }
+ invalidate();
+ mEditorGrad.commitLocalRepresentation();
+ return true;
+ }
+
+ public void setRepresentation(FilterGradRepresentation pointRep) {
+ mGradRep = pointRep;
+ Matrix toImg = getScreenToImageMatrix(false);
+
+ toImg.invert(mToScr);
+
+ float[] c1 = new float[] { mGradRep.getPoint1X(), mGradRep.getPoint1Y() };
+ float[] c2 = new float[] { mGradRep.getPoint2X(), mGradRep.getPoint2Y() };
+
+ if (c1[0] == -1) {
+ float cx = MasterImage.getImage().getOriginalBounds().width() / 2;
+ float cy = MasterImage.getImage().getOriginalBounds().height() / 2;
+ float rx = Math.min(cx, cy) * .4f;
+
+ mGradRep.setPoint1(cx, cy-rx);
+ mGradRep.setPoint2(cx, cy+rx);
+ c1[0] = cx;
+ c1[1] = cy-rx;
+ mToScr.mapPoints(c1);
+ if (getWidth() != 0) {
+ mEllipse.setPoint1(c1[0], c1[1]);
+ c2[0] = cx;
+ c2[1] = cy+rx;
+ mToScr.mapPoints(c2);
+ mEllipse.setPoint2(c2[0], c2[1]);
+ }
+ mEditorGrad.commitLocalRepresentation();
+ } else {
+ mToScr.mapPoints(c1);
+ mToScr.mapPoints(c2);
+ mEllipse.setPoint1(c1[0], c1[1]);
+ mEllipse.setPoint2(c2[0], c2[1]);
+ }
+ }
+
+ public void drawOtherPoints(Canvas canvas) {
+ computCenterLocations();
+ for (int i = 0; i < mPointsX.length; i++) {
+ if (mPointsX[i] != -1) {
+ mEllipse.paintGrayPoint(canvas, mPointsX[i], mPointsY[i]);
+ }
+ }
+ }
+
+ public void computCenterLocations() {
+ int x1[] = mGradRep.getXPos1();
+ int y1[] = mGradRep.getYPos1();
+ int x2[] = mGradRep.getXPos2();
+ int y2[] = mGradRep.getYPos2();
+ int selected = mGradRep.getSelectedPoint();
+ boolean m[] = mGradRep.getMask();
+ float[] c = new float[2];
+ for (int i = 0; i < m.length; i++) {
+ if (selected == i || !m[i]) {
+ mPointsX[i] = -1;
+ continue;
+ }
+
+ c[0] = (x1[i]+x2[i])/2;
+ c[1] = (y1[i]+y2[i])/2;
+ mToScr.mapPoints(c);
+
+ mPointsX[i] = c[0];
+ mPointsY[i] = c[1];
+ }
+ }
+
+ public void setEditor(EditorGrad editorGrad) {
+ mEditorGrad = editorGrad;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mGradRep == null) {
+ return;
+ }
+ setRepresentation(mGradRep);
+ mEllipse.draw(canvas);
+ drawOtherPoints(canvas);
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java b/src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java
new file mode 100644
index 000000000..26c49b1a8
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageMirror.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2013 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.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.filtershow.editors.EditorMirror;
+import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation;
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder;
+
+public class ImageMirror extends ImageShow {
+ private static final String TAG = ImageMirror.class.getSimpleName();
+ private EditorMirror mEditorMirror;
+ private FilterMirrorRepresentation mLocalRep = new FilterMirrorRepresentation();
+ private GeometryHolder mDrawHolder = new GeometryHolder();
+
+ public ImageMirror(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ImageMirror(Context context) {
+ super(context);
+ }
+
+ public void setFilterMirrorRepresentation(FilterMirrorRepresentation rep) {
+ mLocalRep = (rep == null) ? new FilterMirrorRepresentation() : rep;
+ }
+
+ public void flip() {
+ mLocalRep.cycle();
+ invalidate();
+ }
+
+ public FilterMirrorRepresentation getFinalRepresentation() {
+ return mLocalRep;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // Treat event as handled.
+ return true;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ MasterImage master = MasterImage.getImage();
+ Bitmap image = master.getFiltersOnlyImage();
+ if (image == null) {
+ return;
+ }
+ GeometryMathUtils.initializeHolder(mDrawHolder, mLocalRep);
+ GeometryMathUtils.drawTransformedCropped(mDrawHolder, canvas, image, getWidth(),
+ getHeight());
+ }
+
+ public void setEditor(EditorMirror editorFlip) {
+ mEditorMirror = editorFlip;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java b/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java
new file mode 100644
index 000000000..fd5714139
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImagePoint.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2013 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.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.util.AttributeSet;
+
+import com.android.gallery3d.filtershow.editors.EditorRedEye;
+import com.android.gallery3d.filtershow.filters.FilterPoint;
+import com.android.gallery3d.filtershow.filters.FilterRedEyeRepresentation;
+import com.android.gallery3d.filtershow.filters.ImageFilterRedEye;
+
+public abstract class ImagePoint extends ImageShow {
+
+ private static final String LOGTAG = "ImageRedEyes";
+ protected EditorRedEye mEditorRedEye;
+ protected FilterRedEyeRepresentation mRedEyeRep;
+ protected static float mTouchPadding = 80;
+
+ public static void setTouchPadding(float padding) {
+ mTouchPadding = padding;
+ }
+
+ public ImagePoint(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ImagePoint(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void resetParameter() {
+ ImageFilterRedEye filter = (ImageFilterRedEye) getCurrentFilter();
+ if (filter != null) {
+ filter.clear();
+ }
+ invalidate();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ Paint paint = new Paint();
+ paint.setStyle(Style.STROKE);
+ paint.setColor(Color.RED);
+ paint.setStrokeWidth(2);
+
+ Matrix originalToScreen = getImageToScreenMatrix(false);
+ Matrix originalRotateToScreen = getImageToScreenMatrix(true);
+
+ if (mRedEyeRep != null) {
+ for (FilterPoint candidate : mRedEyeRep.getCandidates()) {
+ drawPoint(candidate, canvas, originalToScreen, originalRotateToScreen, paint);
+ }
+ }
+ }
+
+ protected abstract void drawPoint(
+ FilterPoint candidate, Canvas canvas, Matrix originalToScreen,
+ Matrix originalRotateToScreen, Paint paint);
+
+ public void setEditor(EditorRedEye editorRedEye) {
+ mEditorRedEye = editorRedEye;
+ }
+
+ public void setRepresentation(FilterRedEyeRepresentation redEyeRep) {
+ mRedEyeRep = redEyeRep;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java b/src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java
new file mode 100644
index 000000000..40433a02e
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageRedEye.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2013 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.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.RectF;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.filtershow.filters.FilterPoint;
+import com.android.gallery3d.filtershow.filters.RedEyeCandidate;
+
+public class ImageRedEye extends ImagePoint {
+ private static final String LOGTAG = "ImageRedEyes";
+ private RectF mCurrentRect = null;
+
+ public ImageRedEye(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void resetParameter() {
+ super.resetParameter();
+ invalidate();
+ }
+
+ @Override
+
+ public boolean onTouchEvent(MotionEvent event) {
+ super.onTouchEvent(event);
+
+ if (event.getPointerCount() > 1) {
+ return true;
+ }
+
+ if (didFinishScalingOperation()) {
+ return true;
+ }
+
+ float ex = event.getX();
+ float ey = event.getY();
+
+ // let's transform (ex, ey) to displayed image coordinates
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ mCurrentRect = new RectF();
+ mCurrentRect.left = ex - mTouchPadding;
+ mCurrentRect.top = ey - mTouchPadding;
+ }
+ if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ mCurrentRect.right = ex + mTouchPadding;
+ mCurrentRect.bottom = ey + mTouchPadding;
+ }
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ if (mCurrentRect != null) {
+ // transform to original coordinates
+ Matrix originalNoRotateToScreen = getImageToScreenMatrix(false);
+ Matrix originalToScreen = getImageToScreenMatrix(true);
+ Matrix invert = new Matrix();
+ originalToScreen.invert(invert);
+ RectF r = new RectF(mCurrentRect);
+ invert.mapRect(r);
+ RectF r2 = new RectF(mCurrentRect);
+ invert.reset();
+ originalNoRotateToScreen.invert(invert);
+ invert.mapRect(r2);
+ mRedEyeRep.addRect(r, r2);
+ this.resetImageCaches(this);
+ }
+ mCurrentRect = null;
+ }
+ mEditorRedEye.commitLocalRepresentation();
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ Paint paint = new Paint();
+ paint.setStyle(Style.STROKE);
+ paint.setColor(Color.RED);
+ paint.setStrokeWidth(2);
+ if (mCurrentRect != null) {
+ paint.setColor(Color.RED);
+ RectF drawRect = new RectF(mCurrentRect);
+ canvas.drawRect(drawRect, paint);
+ }
+ }
+
+ @Override
+ protected void drawPoint(FilterPoint point, Canvas canvas, Matrix originalToScreen,
+ Matrix originalRotateToScreen, Paint paint) {
+ RedEyeCandidate candidate = (RedEyeCandidate) point;
+ RectF rect = candidate.getRect();
+ RectF drawRect = new RectF();
+ originalToScreen.mapRect(drawRect, rect);
+ RectF fullRect = new RectF();
+ originalRotateToScreen.mapRect(fullRect, rect);
+ paint.setColor(Color.BLUE);
+ canvas.drawRect(fullRect, paint);
+ canvas.drawLine(fullRect.centerX(), fullRect.top,
+ fullRect.centerX(), fullRect.bottom, paint);
+ canvas.drawLine(fullRect.left, fullRect.centerY(),
+ fullRect.right, fullRect.centerY(), paint);
+ paint.setColor(Color.GREEN);
+ float dw = drawRect.width();
+ float dh = drawRect.height();
+ float dx = fullRect.centerX() - dw / 2;
+ float dy = fullRect.centerY() - dh / 2;
+ drawRect.set(dx, dy, dx + dw, dy + dh);
+ canvas.drawRect(drawRect, paint);
+ canvas.drawLine(drawRect.centerX(), drawRect.top,
+ drawRect.centerX(), drawRect.bottom, paint);
+ canvas.drawLine(drawRect.left, drawRect.centerY(),
+ drawRect.right, drawRect.centerY(), paint);
+ canvas.drawCircle(drawRect.centerX(), drawRect.centerY(),
+ mTouchPadding, paint);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
new file mode 100644
index 000000000..5186c09d7
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageRotate.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2012 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.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.filtershow.editors.EditorRotate;
+import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation;
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder;
+
+public class ImageRotate extends ImageShow {
+ private EditorRotate mEditorRotate;
+ private static final String TAG = ImageRotate.class.getSimpleName();
+ private FilterRotateRepresentation mLocalRep = new FilterRotateRepresentation();
+ private GeometryHolder mDrawHolder = new GeometryHolder();
+
+ public ImageRotate(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ImageRotate(Context context) {
+ super(context);
+ }
+
+ public void setFilterRotateRepresentation(FilterRotateRepresentation rep) {
+ mLocalRep = (rep == null) ? new FilterRotateRepresentation() : rep;
+ }
+
+ public void rotate() {
+ mLocalRep.rotateCW();
+ invalidate();
+ }
+
+ public FilterRotateRepresentation getFinalRepresentation() {
+ return mLocalRep;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // Treat event as handled.
+ return true;
+ }
+
+ public int getLocalValue() {
+ return mLocalRep.getRotation().value();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ MasterImage master = MasterImage.getImage();
+ Bitmap image = master.getFiltersOnlyImage();
+ if (image == null) {
+ return;
+ }
+ GeometryMathUtils.initializeHolder(mDrawHolder, mLocalRep);
+ GeometryMathUtils.drawTransformedCropped(mDrawHolder, canvas, image, canvas.getWidth(),
+ canvas.getHeight());
+ }
+
+ public void setEditor(EditorRotate editorRotate) {
+ mEditorRotate = editorRotate;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
new file mode 100644
index 000000000..6278b2ad4
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java
@@ -0,0 +1,578 @@
+/*
+ * Copyright (C) 2012 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.filtershow.imageshow;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.GestureDetector.OnDoubleTapListener;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.File;
+
+public class ImageShow extends View implements OnGestureListener,
+ ScaleGestureDetector.OnScaleGestureListener,
+ OnDoubleTapListener {
+
+ private static final String LOGTAG = "ImageShow";
+ private static final boolean ENABLE_ZOOMED_COMPARISON = false;
+
+ protected Paint mPaint = new Paint();
+ protected int mTextSize;
+ protected int mTextPadding;
+
+ protected int mBackgroundColor;
+
+ private GestureDetector mGestureDetector = null;
+ private ScaleGestureDetector mScaleGestureDetector = null;
+
+ protected Rect mImageBounds = new Rect();
+ private boolean mOriginalDisabled = false;
+ private boolean mTouchShowOriginal = false;
+ private long mTouchShowOriginalDate = 0;
+ private final long mTouchShowOriginalDelayMin = 200; // 200ms
+ private int mShowOriginalDirection = 0;
+ private static int UNVEIL_HORIZONTAL = 1;
+ private static int UNVEIL_VERTICAL = 2;
+
+ private Point mTouchDown = new Point();
+ private Point mTouch = new Point();
+ private boolean mFinishedScalingOperation = false;
+
+ private int mOriginalTextMargin;
+ private int mOriginalTextSize;
+ private String mOriginalText;
+ private boolean mZoomIn = false;
+ Point mOriginalTranslation = new Point();
+ float mOriginalScale;
+ float mStartFocusX, mStartFocusY;
+ private enum InteractionMode {
+ NONE,
+ SCALE,
+ MOVE
+ }
+ InteractionMode mInteractionMode = InteractionMode.NONE;
+
+ private FilterShowActivity mActivity = null;
+
+ public FilterShowActivity getActivity() {
+ return mActivity;
+ }
+
+ public boolean hasModifications() {
+ return MasterImage.getImage().hasModifications();
+ }
+
+ public void resetParameter() {
+ // TODO: implement reset
+ }
+
+ public void onNewValue(int parameter) {
+ invalidate();
+ }
+
+ public ImageShow(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setupImageShow(context);
+ }
+
+ public ImageShow(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setupImageShow(context);
+
+ }
+
+ public ImageShow(Context context) {
+ super(context);
+ setupImageShow(context);
+ }
+
+ private void setupImageShow(Context context) {
+ Resources res = context.getResources();
+ mTextSize = res.getDimensionPixelSize(R.dimen.photoeditor_text_size);
+ mTextPadding = res.getDimensionPixelSize(R.dimen.photoeditor_text_padding);
+ mOriginalTextMargin = res.getDimensionPixelSize(R.dimen.photoeditor_original_text_margin);
+ mOriginalTextSize = res.getDimensionPixelSize(R.dimen.photoeditor_original_text_size);
+ mBackgroundColor = res.getColor(R.color.background_screen);
+ mOriginalText = res.getString(R.string.original_picture_text);
+ setupGestureDetector(context);
+ mActivity = (FilterShowActivity) context;
+ MasterImage.getImage().addObserver(this);
+ }
+
+ public void setupGestureDetector(Context context) {
+ mGestureDetector = new GestureDetector(context, this);
+ mScaleGestureDetector = new ScaleGestureDetector(context, this);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int parentHeight = MeasureSpec.getSize(heightMeasureSpec);
+ setMeasuredDimension(parentWidth, parentHeight);
+ }
+
+ public ImageFilter getCurrentFilter() {
+ return MasterImage.getImage().getCurrentFilter();
+ }
+
+ /* consider moving the following 2 methods into a subclass */
+ /**
+ * This function calculates a Image to Screen Transformation matrix
+ *
+ * @param reflectRotation set true if you want the rotation encoded
+ * @return Image to Screen transformation matrix
+ */
+ protected Matrix getImageToScreenMatrix(boolean reflectRotation) {
+ MasterImage master = MasterImage.getImage();
+ if (master.getOriginalBounds() == null) {
+ return new Matrix();
+ }
+ Matrix m = GeometryMathUtils.getImageToScreenMatrix(master.getPreset().getGeometryFilters(),
+ reflectRotation, master.getOriginalBounds(), getWidth(), getHeight());
+ Point translate = master.getTranslation();
+ float scaleFactor = master.getScaleFactor();
+ m.postTranslate(translate.x, translate.y);
+ m.postScale(scaleFactor, scaleFactor, getWidth() / 2.0f, getHeight() / 2.0f);
+ return m;
+ }
+
+ /**
+ * This function calculates a to Screen Image Transformation matrix
+ *
+ * @param reflectRotation set true if you want the rotation encoded
+ * @return Screen to Image transformation matrix
+ */
+ protected Matrix getScreenToImageMatrix(boolean reflectRotation) {
+ Matrix m = getImageToScreenMatrix(reflectRotation);
+ Matrix invert = new Matrix();
+ m.invert(invert);
+ return invert;
+ }
+
+ public ImagePreset getImagePreset() {
+ return MasterImage.getImage().getPreset();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ MasterImage.getImage().setImageShowSize(getWidth(), getHeight());
+
+ float cx = canvas.getWidth()/2.0f;
+ float cy = canvas.getHeight()/2.0f;
+ float scaleFactor = MasterImage.getImage().getScaleFactor();
+ Point translation = MasterImage.getImage().getTranslation();
+
+ Matrix scalingMatrix = new Matrix();
+ scalingMatrix.postScale(scaleFactor, scaleFactor, cx, cy);
+ scalingMatrix.preTranslate(translation.x, translation.y);
+
+ RectF unscaledClipRect = new RectF(mImageBounds);
+ scalingMatrix.mapRect(unscaledClipRect, unscaledClipRect);
+
+ canvas.save();
+
+ boolean enablePartialRendering = false;
+
+ // For now, partial rendering is disabled for all filters,
+ // so no need to clip.
+ if (enablePartialRendering && !unscaledClipRect.isEmpty()) {
+ canvas.clipRect(unscaledClipRect);
+ }
+
+ canvas.save();
+ // TODO: center scale on gesture
+ canvas.scale(scaleFactor, scaleFactor, cx, cy);
+ canvas.translate(translation.x, translation.y);
+ drawImage(canvas, getFilteredImage(), true);
+ Bitmap highresPreview = MasterImage.getImage().getHighresImage();
+ if (highresPreview != null) {
+ drawImage(canvas, highresPreview, true);
+ }
+ canvas.restore();
+
+ Bitmap partialPreview = MasterImage.getImage().getPartialImage();
+ if (partialPreview != null) {
+ Rect src = new Rect(0, 0, partialPreview.getWidth(), partialPreview.getHeight());
+ Rect dest = new Rect(0, 0, getWidth(), getHeight());
+ canvas.drawBitmap(partialPreview, src, dest, mPaint);
+ }
+
+ canvas.save();
+ canvas.scale(scaleFactor, scaleFactor, cx, cy);
+ canvas.translate(translation.x, translation.y);
+ drawPartialImage(canvas, getGeometryOnlyImage());
+ canvas.restore();
+
+ canvas.restore();
+ }
+
+ public void resetImageCaches(ImageShow caller) {
+ MasterImage.getImage().updatePresets(true);
+ }
+
+ public Bitmap getFiltersOnlyImage() {
+ return MasterImage.getImage().getFiltersOnlyImage();
+ }
+
+ public Bitmap getGeometryOnlyImage() {
+ return MasterImage.getImage().getGeometryOnlyImage();
+ }
+
+ public Bitmap getFilteredImage() {
+ return MasterImage.getImage().getFilteredImage();
+ }
+
+ public void drawImage(Canvas canvas, Bitmap image, boolean updateBounds) {
+ if (image != null) {
+ Rect s = new Rect(0, 0, image.getWidth(),
+ image.getHeight());
+
+ float scale = GeometryMathUtils.scale(image.getWidth(), image.getHeight(), getWidth(),
+ getHeight());
+
+ float w = image.getWidth() * scale;
+ float h = image.getHeight() * scale;
+ float ty = (getHeight() - h) / 2.0f;
+ float tx = (getWidth() - w) / 2.0f;
+
+ Rect d = new Rect((int) tx, (int) ty, (int) (w + tx),
+ (int) (h + ty));
+ if (updateBounds) {
+ mImageBounds = d;
+ }
+ canvas.drawBitmap(image, s, d, mPaint);
+ }
+ }
+
+ public void drawPartialImage(Canvas canvas, Bitmap image) {
+ boolean showsOriginal = MasterImage.getImage().showsOriginal();
+ if (!showsOriginal && !mTouchShowOriginal)
+ return;
+ canvas.save();
+ if (image != null) {
+ if (mShowOriginalDirection == 0) {
+ if (Math.abs(mTouch.y - mTouchDown.y) > Math.abs(mTouch.x - mTouchDown.x)) {
+ mShowOriginalDirection = UNVEIL_VERTICAL;
+ } else {
+ mShowOriginalDirection = UNVEIL_HORIZONTAL;
+ }
+ }
+
+ int px = 0;
+ int py = 0;
+ if (mShowOriginalDirection == UNVEIL_VERTICAL) {
+ px = mImageBounds.width();
+ py = mTouch.y - mImageBounds.top;
+ } else {
+ px = mTouch.x - mImageBounds.left;
+ py = mImageBounds.height();
+ if (showsOriginal) {
+ px = mImageBounds.width();
+ }
+ }
+
+ Rect d = new Rect(mImageBounds.left, mImageBounds.top,
+ mImageBounds.left + px, mImageBounds.top + py);
+ canvas.clipRect(d);
+ drawImage(canvas, image, false);
+ Paint paint = new Paint();
+ paint.setColor(Color.BLACK);
+ paint.setStrokeWidth(3);
+
+ if (mShowOriginalDirection == UNVEIL_VERTICAL) {
+ canvas.drawLine(mImageBounds.left, mTouch.y,
+ mImageBounds.right, mTouch.y, paint);
+ } else {
+ canvas.drawLine(mTouch.x, mImageBounds.top,
+ mTouch.x, mImageBounds.bottom, paint);
+ }
+
+ Rect bounds = new Rect();
+ paint.setAntiAlias(true);
+ paint.setTextSize(mOriginalTextSize);
+ paint.getTextBounds(mOriginalText, 0, mOriginalText.length(), bounds);
+ paint.setColor(Color.BLACK);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(3);
+ canvas.drawText(mOriginalText, mImageBounds.left + mOriginalTextMargin,
+ mImageBounds.top + bounds.height() + mOriginalTextMargin, paint);
+ paint.setStyle(Paint.Style.FILL);
+ paint.setStrokeWidth(1);
+ paint.setColor(Color.WHITE);
+ canvas.drawText(mOriginalText, mImageBounds.left + mOriginalTextMargin,
+ mImageBounds.top + bounds.height() + mOriginalTextMargin, paint);
+ }
+ canvas.restore();
+ }
+
+ public void bindAsImageLoadListener() {
+ MasterImage.getImage().addListener(this);
+ }
+
+ public void updateImage() {
+ invalidate();
+ }
+
+ public void imageLoaded() {
+ updateImage();
+ }
+
+ public void saveImage(FilterShowActivity filterShowActivity, File file) {
+ SaveImage.saveImage(getImagePreset(), filterShowActivity, file);
+ }
+
+
+ public boolean scaleInProgress() {
+ return mScaleGestureDetector.isInProgress();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ super.onTouchEvent(event);
+ int action = event.getAction();
+ action = action & MotionEvent.ACTION_MASK;
+
+ mGestureDetector.onTouchEvent(event);
+ boolean scaleInProgress = scaleInProgress();
+ mScaleGestureDetector.onTouchEvent(event);
+ if (mInteractionMode == InteractionMode.SCALE) {
+ return true;
+ }
+ if (!scaleInProgress() && scaleInProgress) {
+ // If we were scaling, the scale will stop but we will
+ // still issue an ACTION_UP. Let the subclasses know.
+ mFinishedScalingOperation = true;
+ }
+
+ int ex = (int) event.getX();
+ int ey = (int) event.getY();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mInteractionMode = InteractionMode.MOVE;
+ mTouchDown.x = ex;
+ mTouchDown.y = ey;
+ mTouchShowOriginalDate = System.currentTimeMillis();
+ mShowOriginalDirection = 0;
+ MasterImage.getImage().setOriginalTranslation(MasterImage.getImage().getTranslation());
+ }
+
+ if (action == MotionEvent.ACTION_MOVE && mInteractionMode == InteractionMode.MOVE) {
+ mTouch.x = ex;
+ mTouch.y = ey;
+
+ float scaleFactor = MasterImage.getImage().getScaleFactor();
+ if (scaleFactor > 1 && (!ENABLE_ZOOMED_COMPARISON || event.getPointerCount() == 2)) {
+ float translateX = (mTouch.x - mTouchDown.x) / scaleFactor;
+ float translateY = (mTouch.y - mTouchDown.y) / scaleFactor;
+ Point originalTranslation = MasterImage.getImage().getOriginalTranslation();
+ Point translation = MasterImage.getImage().getTranslation();
+ translation.x = (int) (originalTranslation.x + translateX);
+ translation.y = (int) (originalTranslation.y + translateY);
+ constrainTranslation(translation, scaleFactor);
+ MasterImage.getImage().setTranslation(translation);
+ mTouchShowOriginal = false;
+ } else if (enableComparison() && !mOriginalDisabled
+ && (System.currentTimeMillis() - mTouchShowOriginalDate
+ > mTouchShowOriginalDelayMin)
+ && event.getPointerCount() == 1) {
+ mTouchShowOriginal = true;
+ }
+ }
+
+ if (action == MotionEvent.ACTION_UP) {
+ mInteractionMode = InteractionMode.NONE;
+ mTouchShowOriginal = false;
+ mTouchDown.x = 0;
+ mTouchDown.y = 0;
+ mTouch.x = 0;
+ mTouch.y = 0;
+ if (MasterImage.getImage().getScaleFactor() <= 1) {
+ MasterImage.getImage().setScaleFactor(1);
+ MasterImage.getImage().resetTranslation();
+ }
+ }
+ invalidate();
+ return true;
+ }
+
+ protected boolean enableComparison() {
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent arg0) {
+ mZoomIn = !mZoomIn;
+ float scale = 1.0f;
+ if (mZoomIn) {
+ scale = MasterImage.getImage().getMaxScaleFactor();
+ }
+ if (scale != MasterImage.getImage().getScaleFactor()) {
+ MasterImage.getImage().setScaleFactor(scale);
+ float translateX = (getWidth() / 2 - arg0.getX());
+ float translateY = (getHeight() / 2 - arg0.getY());
+ Point translation = MasterImage.getImage().getTranslation();
+ translation.x = (int) (mOriginalTranslation.x + translateX);
+ translation.y = (int) (mOriginalTranslation.y + translateY);
+ constrainTranslation(translation, scale);
+ MasterImage.getImage().setTranslation(translation);
+ invalidate();
+ }
+ return true;
+ }
+
+ private void constrainTranslation(Point translation, float scale) {
+ float maxTranslationX = getWidth() / scale;
+ float maxTranslationY = getHeight() / scale;
+ if (Math.abs(translation.x) > maxTranslationX) {
+ translation.x = (int) (Math.signum(translation.x) *
+ maxTranslationX);
+ if (Math.abs(translation.y) > maxTranslationY) {
+ translation.y = (int) (Math.signum(translation.y) *
+ maxTranslationY);
+ }
+
+ }
+ }
+
+ @Override
+ public boolean onDoubleTapEvent(MotionEvent arg0) {
+ return false;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent arg0) {
+ return false;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent arg0) {
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent startEvent, MotionEvent endEvent, float arg2, float arg3) {
+ if (mActivity == null) {
+ return false;
+ }
+ if (endEvent.getPointerCount() == 2) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent arg0) {
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) {
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent arg0) {
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent arg0) {
+ return false;
+ }
+
+ public boolean useUtilityPanel() {
+ return false;
+ }
+
+ public void openUtilityPanel(final LinearLayout accessoryViewList) {
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ MasterImage img = MasterImage.getImage();
+ float scaleFactor = img.getScaleFactor();
+
+ scaleFactor = scaleFactor * detector.getScaleFactor();
+ if (scaleFactor > MasterImage.getImage().getMaxScaleFactor()) {
+ scaleFactor = MasterImage.getImage().getMaxScaleFactor();
+ }
+ if (scaleFactor < 0.5) {
+ scaleFactor = 0.5f;
+ }
+ MasterImage.getImage().setScaleFactor(scaleFactor);
+ scaleFactor = img.getScaleFactor();
+ float focusx = detector.getFocusX();
+ float focusy = detector.getFocusY();
+ float translateX = (focusx - mStartFocusX) / scaleFactor;
+ float translateY = (focusy - mStartFocusY) / scaleFactor;
+ Point translation = MasterImage.getImage().getTranslation();
+ translation.x = (int) (mOriginalTranslation.x + translateX);
+ translation.y = (int) (mOriginalTranslation.y + translateY);
+ constrainTranslation(translation, scaleFactor);
+ MasterImage.getImage().setTranslation(translation);
+
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ Point pos = MasterImage.getImage().getTranslation();
+ mOriginalTranslation.x = pos.x;
+ mOriginalTranslation.y = pos.y;
+ mOriginalScale = MasterImage.getImage().getScaleFactor();
+ mStartFocusX = detector.getFocusX();
+ mStartFocusY = detector.getFocusY();
+ mInteractionMode = InteractionMode.SCALE;
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mInteractionMode = InteractionMode.NONE;
+ if (MasterImage.getImage().getScaleFactor() < 1) {
+ MasterImage.getImage().setScaleFactor(1);
+ invalidate();
+ }
+ }
+
+ public boolean didFinishScalingOperation() {
+ if (mFinishedScalingOperation) {
+ mFinishedScalingOperation = false;
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java b/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java
new file mode 100644
index 000000000..ff75dcc09
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageStraighten.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2012 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.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.filtershow.editors.EditorStraighten;
+import com.android.gallery3d.filtershow.filters.FilterCropRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation;
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils.GeometryHolder;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+
+public class ImageStraighten extends ImageShow {
+ private static final String TAG = ImageStraighten.class.getSimpleName();
+ private float mBaseAngle = 0;
+ private float mAngle = 0;
+ private float mInitialAngle = 0;
+ private boolean mFirstDrawSinceUp = false;
+ private EditorStraighten mEditorStraighten;
+ private FilterStraightenRepresentation mLocalRep = new FilterStraightenRepresentation();
+ private RectF mPriorCropAtUp = new RectF();
+ private RectF mDrawRect = new RectF();
+ private Path mDrawPath = new Path();
+ private GeometryHolder mDrawHolder = new GeometryHolder();
+ private enum MODES {
+ NONE, MOVE
+ }
+ private MODES mState = MODES.NONE;
+ private static final float MAX_STRAIGHTEN_ANGLE
+ = FilterStraightenRepresentation.MAX_STRAIGHTEN_ANGLE;
+ private static final float MIN_STRAIGHTEN_ANGLE
+ = FilterStraightenRepresentation.MIN_STRAIGHTEN_ANGLE;
+ private float mCurrentX;
+ private float mCurrentY;
+ private float mTouchCenterX;
+ private float mTouchCenterY;
+ private RectF mCrop = new RectF();
+ private final Paint mPaint = new Paint();
+
+ public ImageStraighten(Context context) {
+ super(context);
+ }
+
+ public ImageStraighten(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setFilterStraightenRepresentation(FilterStraightenRepresentation rep) {
+ mLocalRep = (rep == null) ? new FilterStraightenRepresentation() : rep;
+ mInitialAngle = mBaseAngle = mAngle = mLocalRep.getStraighten();
+ }
+
+ public Collection<FilterRepresentation> getFinalRepresentation() {
+ ArrayList<FilterRepresentation> reps = new ArrayList<FilterRepresentation>(2);
+ reps.add(mLocalRep);
+ if (mInitialAngle != mLocalRep.getStraighten()) {
+ reps.add(new FilterCropRepresentation(mCrop));
+ }
+ return reps;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+
+ switch (event.getActionMasked()) {
+ case (MotionEvent.ACTION_DOWN):
+ if (mState == MODES.NONE) {
+ mTouchCenterX = x;
+ mTouchCenterY = y;
+ mCurrentX = x;
+ mCurrentY = y;
+ mState = MODES.MOVE;
+ mBaseAngle = mAngle;
+ }
+ break;
+ case (MotionEvent.ACTION_UP):
+ if (mState == MODES.MOVE) {
+ mState = MODES.NONE;
+ mCurrentX = x;
+ mCurrentY = y;
+ computeValue();
+ mFirstDrawSinceUp = true;
+ }
+ break;
+ case (MotionEvent.ACTION_MOVE):
+ if (mState == MODES.MOVE) {
+ mCurrentX = x;
+ mCurrentY = y;
+ computeValue();
+ }
+ break;
+ default:
+ break;
+ }
+ invalidate();
+ return true;
+ }
+
+ private static float angleFor(float dx, float dy) {
+ return (float) (Math.atan2(dx, dy) * 180 / Math.PI);
+ }
+
+ private float getCurrentTouchAngle() {
+ float centerX = getWidth() / 2f;
+ float centerY = getHeight() / 2f;
+ if (mCurrentX == mTouchCenterX && mCurrentY == mTouchCenterY) {
+ return 0;
+ }
+ float dX1 = mTouchCenterX - centerX;
+ float dY1 = mTouchCenterY - centerY;
+ float dX2 = mCurrentX - centerX;
+ float dY2 = mCurrentY - centerY;
+ float angleA = angleFor(dX1, dY1);
+ float angleB = angleFor(dX2, dY2);
+ return (angleB - angleA) % 360;
+ }
+
+ private void computeValue() {
+ float angle = getCurrentTouchAngle();
+ mAngle = (mBaseAngle - angle) % 360;
+ mAngle = Math.max(MIN_STRAIGHTEN_ANGLE, mAngle);
+ mAngle = Math.min(MAX_STRAIGHTEN_ANGLE, mAngle);
+ }
+
+ private static void getUntranslatedStraightenCropBounds(RectF outRect, float straightenAngle) {
+ float deg = straightenAngle;
+ if (deg < 0) {
+ deg = -deg;
+ }
+ double a = Math.toRadians(deg);
+ double sina = Math.sin(a);
+ double cosa = Math.cos(a);
+ double rw = outRect.width();
+ double rh = outRect.height();
+ double h1 = rh * rh / (rw * sina + rh * cosa);
+ double h2 = rh * rw / (rw * cosa + rh * sina);
+ double hh = Math.min(h1, h2);
+ double ww = hh * rw / rh;
+ float left = (float) ((rw - ww) * 0.5f);
+ float top = (float) ((rh - hh) * 0.5f);
+ float right = (float) (left + ww);
+ float bottom = (float) (top + hh);
+ outRect.set(left, top, right, bottom);
+ }
+
+ private void updateCurrentCrop(Matrix m, GeometryHolder h, RectF tmp, int imageWidth,
+ int imageHeight, int viewWidth, int viewHeight) {
+ if (GeometryMathUtils.needsDimensionSwap(h.rotation)) {
+ tmp.set(0, 0, imageHeight, imageWidth);
+ } else {
+ tmp.set(0, 0, imageWidth, imageHeight);
+ }
+ float scale = GeometryMathUtils.scale(imageWidth, imageHeight, viewWidth, viewHeight);
+ GeometryMathUtils.scaleRect(tmp, scale);
+ getUntranslatedStraightenCropBounds(tmp, mAngle);
+ tmp.offset(viewWidth / 2f - tmp.centerX(), viewHeight / 2f - tmp.centerY());
+ h.straighten = 0;
+ Matrix m1 = GeometryMathUtils.getFullGeometryToScreenMatrix(h, imageWidth,
+ imageHeight, viewWidth, viewHeight);
+ m.reset();
+ m1.invert(m);
+ mCrop.set(tmp);
+ m.mapRect(mCrop);
+ FilterCropRepresentation.findNormalizedCrop(mCrop, imageWidth, imageHeight);
+ }
+
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ MasterImage master = MasterImage.getImage();
+ Bitmap image = master.getFiltersOnlyImage();
+ if (image == null) {
+ return;
+ }
+ GeometryMathUtils.initializeHolder(mDrawHolder, mLocalRep);
+ mDrawHolder.straighten = mAngle;
+ int imageWidth = image.getWidth();
+ int imageHeight = image.getHeight();
+ int viewWidth = canvas.getWidth();
+ int viewHeight = canvas.getHeight();
+
+ // Get matrix for drawing bitmap
+ Matrix m = GeometryMathUtils.getFullGeometryToScreenMatrix(mDrawHolder, imageWidth,
+ imageHeight, viewWidth, viewHeight);
+ mPaint.reset();
+ mPaint.setAntiAlias(true);
+ mPaint.setFilterBitmap(true);
+ canvas.drawBitmap(image, m, mPaint);
+
+ mPaint.setFilterBitmap(false);
+ mPaint.setColor(Color.WHITE);
+ mPaint.setStrokeWidth(2);
+ mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+ updateCurrentCrop(m, mDrawHolder, mDrawRect, imageWidth,
+ imageHeight, viewWidth, viewHeight);
+ if (mFirstDrawSinceUp) {
+ mPriorCropAtUp.set(mCrop);
+ mLocalRep.setStraighten(mAngle);
+ mFirstDrawSinceUp = false;
+ }
+
+ // Draw the grid
+ if (mState == MODES.MOVE) {
+ canvas.save();
+ canvas.clipRect(mDrawRect);
+ int n = 16;
+ float step = viewWidth / n;
+ float p = 0;
+ for (int i = 1; i < n; i++) {
+ p = i * step;
+ mPaint.setAlpha(60);
+ canvas.drawLine(p, 0, p, viewHeight, mPaint);
+ canvas.drawLine(0, p, viewHeight, p, mPaint);
+ }
+ canvas.restore();
+ }
+ mPaint.reset();
+ mPaint.setColor(Color.WHITE);
+ mPaint.setStyle(Style.STROKE);
+ mPaint.setStrokeWidth(3);
+ mDrawPath.reset();
+ mDrawPath.addRect(mDrawRect, Path.Direction.CW);
+ canvas.drawPath(mDrawPath, mPaint);
+ }
+
+ public void setEditor(EditorStraighten editorStraighten) {
+ mEditorStraighten = editorStraighten;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java b/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java
new file mode 100644
index 000000000..25a0a9073
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageTinyPlanet.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2012 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.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.ScaleGestureDetector.OnScaleGestureListener;
+
+import com.android.gallery3d.filtershow.editors.BasicEditor;
+import com.android.gallery3d.filtershow.editors.EditorTinyPlanet;
+import com.android.gallery3d.filtershow.filters.FilterTinyPlanetRepresentation;
+
+public class ImageTinyPlanet extends ImageShow {
+ private static final String LOGTAG = "ImageTinyPlanet";
+
+ private float mTouchCenterX = 0;
+ private float mTouchCenterY = 0;
+ private float mCurrentX = 0;
+ private float mCurrentY = 0;
+ private float mCenterX = 0;
+ private float mCenterY = 0;
+ private float mStartAngle = 0;
+ private FilterTinyPlanetRepresentation mTinyPlanetRep;
+ private EditorTinyPlanet mEditorTinyPlanet;
+ private ScaleGestureDetector mScaleGestureDetector = null;
+ boolean mInScale = false;
+ RectF mDestRect = new RectF();
+
+ OnScaleGestureListener mScaleGestureListener = new OnScaleGestureListener() {
+ private float mScale = 100;
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mInScale = false;
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ mInScale = true;
+ mScale = mTinyPlanetRep.getValue();
+ return true;
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ int value = mTinyPlanetRep.getValue();
+ mScale *= detector.getScaleFactor();
+ value = (int) (mScale);
+ value = Math.min(mTinyPlanetRep.getMaximum(), value);
+ value = Math.max(mTinyPlanetRep.getMinimum(), value);
+ mTinyPlanetRep.setValue(value);
+ invalidate();
+ mEditorTinyPlanet.commitLocalRepresentation();
+ mEditorTinyPlanet.updateUI();
+ return true;
+ }
+ };
+
+ public ImageTinyPlanet(Context context) {
+ super(context);
+ mScaleGestureDetector = new ScaleGestureDetector(context, mScaleGestureListener);
+ }
+
+ public ImageTinyPlanet(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mScaleGestureDetector = new ScaleGestureDetector(context,mScaleGestureListener );
+ }
+
+ protected static float angleFor(float dx, float dy) {
+ return (float) (Math.atan2(dx, dy) * 180 / Math.PI);
+ }
+
+ protected float getCurrentTouchAngle() {
+ if (mCurrentX == mTouchCenterX && mCurrentY == mTouchCenterY) {
+ return 0;
+ }
+ float dX1 = mTouchCenterX - mCenterX;
+ float dY1 = mTouchCenterY - mCenterY;
+ float dX2 = mCurrentX - mCenterX;
+ float dY2 = mCurrentY - mCenterY;
+
+ float angleA = angleFor(dX1, dY1);
+ float angleB = angleFor(dX2, dY2);
+ return (float) (((angleB - angleA) % 360) * Math.PI / 180);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+ mCurrentX = x;
+ mCurrentY = y;
+ mCenterX = getWidth() / 2;
+ mCenterY = getHeight() / 2;
+ mScaleGestureDetector.onTouchEvent(event);
+ if (mInScale) {
+ return true;
+ }
+ switch (event.getActionMasked()) {
+ case (MotionEvent.ACTION_DOWN):
+ mTouchCenterX = x;
+ mTouchCenterY = y;
+ mStartAngle = mTinyPlanetRep.getAngle();
+ break;
+
+ case (MotionEvent.ACTION_MOVE):
+ mTinyPlanetRep.setAngle(mStartAngle + getCurrentTouchAngle());
+ break;
+ }
+ invalidate();
+ mEditorTinyPlanet.commitLocalRepresentation();
+ return true;
+ }
+
+ public void setRepresentation(FilterTinyPlanetRepresentation tinyPlanetRep) {
+ mTinyPlanetRep = tinyPlanetRep;
+ }
+
+ public void setEditor(BasicEditor editorTinyPlanet) {
+ mEditorTinyPlanet = (EditorTinyPlanet) editorTinyPlanet;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ Bitmap bitmap = MasterImage.getImage().getHighresImage();
+ if (bitmap == null) {
+ bitmap = MasterImage.getImage().getFilteredImage();
+ }
+
+ if (bitmap != null) {
+ display(canvas, bitmap);
+ }
+ }
+
+ private void display(Canvas canvas, Bitmap bitmap) {
+ float sw = canvas.getWidth();
+ float sh = canvas.getHeight();
+ float iw = bitmap.getWidth();
+ float ih = bitmap.getHeight();
+ float nsw = sw;
+ float nsh = sh;
+
+ if (sw * ih > sh * iw) {
+ nsw = sh * iw / ih;
+ } else {
+ nsh = sw * ih / iw;
+ }
+
+ mDestRect.left = (sw - nsw) / 2;
+ mDestRect.top = (sh - nsh) / 2;
+ mDestRect.right = sw - mDestRect.left;
+ mDestRect.bottom = sh - mDestRect.top;
+
+ canvas.drawBitmap(bitmap, null, mDestRect, mPaint);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
new file mode 100644
index 000000000..518969ee1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageVignette.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2013 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.filtershow.imageshow;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.filtershow.editors.EditorVignette;
+import com.android.gallery3d.filtershow.filters.FilterVignetteRepresentation;
+
+public class ImageVignette extends ImageShow {
+ private static final String LOGTAG = "ImageVignette";
+
+ private FilterVignetteRepresentation mVignetteRep;
+ private EditorVignette mEditorVignette;
+
+ private int mActiveHandle = -1;
+
+ EclipseControl mElipse;
+
+ public ImageVignette(Context context) {
+ super(context);
+ mElipse = new EclipseControl(context);
+ }
+
+ public ImageVignette(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mElipse = new EclipseControl(context);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ int mask = event.getActionMasked();
+ if (mActiveHandle == -1) {
+ if (MotionEvent.ACTION_DOWN != mask) {
+ return super.onTouchEvent(event);
+ }
+ if (event.getPointerCount() == 1) {
+ mActiveHandle = mElipse.getCloseHandle(event.getX(), event.getY());
+ }
+ if (mActiveHandle == -1) {
+ return super.onTouchEvent(event);
+ }
+ } else {
+ switch (mask) {
+ case MotionEvent.ACTION_UP:
+ mActiveHandle = -1;
+ break;
+ case MotionEvent.ACTION_DOWN:
+ break;
+ }
+ }
+ float x = event.getX();
+ float y = event.getY();
+
+ mElipse.setScrToImageMatrix(getScreenToImageMatrix(true));
+
+ boolean didComputeEllipses = false;
+ switch (mask) {
+ case (MotionEvent.ACTION_DOWN):
+ mElipse.actionDown(x, y, mVignetteRep);
+ break;
+ case (MotionEvent.ACTION_UP):
+ case (MotionEvent.ACTION_MOVE):
+ mElipse.actionMove(mActiveHandle, x, y, mVignetteRep);
+ setRepresentation(mVignetteRep);
+ didComputeEllipses = true;
+ break;
+ }
+ if (!didComputeEllipses) {
+ computeEllipses();
+ }
+ invalidate();
+ return true;
+ }
+
+ public void setRepresentation(FilterVignetteRepresentation vignetteRep) {
+ mVignetteRep = vignetteRep;
+ computeEllipses();
+ }
+
+ public void computeEllipses() {
+ if (mVignetteRep == null) {
+ return;
+ }
+ Matrix toImg = getScreenToImageMatrix(false);
+ Matrix toScr = new Matrix();
+ toImg.invert(toScr);
+
+ float[] c = new float[] {
+ mVignetteRep.getCenterX(), mVignetteRep.getCenterY() };
+ if (Float.isNaN(c[0])) {
+ float cx = MasterImage.getImage().getOriginalBounds().width() / 2;
+ float cy = MasterImage.getImage().getOriginalBounds().height() / 2;
+ float rx = Math.min(cx, cy) * .8f;
+ float ry = rx;
+ mVignetteRep.setCenter(cx, cy);
+ mVignetteRep.setRadius(rx, ry);
+
+ c[0] = cx;
+ c[1] = cy;
+ toScr.mapPoints(c);
+ if (getWidth() != 0) {
+ mElipse.setCenter(c[0], c[1]);
+ mElipse.setRadius(c[0] * 0.8f, c[1] * 0.8f);
+ }
+ } else {
+
+ toScr.mapPoints(c);
+
+ mElipse.setCenter(c[0], c[1]);
+ mElipse.setRadius(toScr.mapRadius(mVignetteRep.getRadiusX()),
+ toScr.mapRadius(mVignetteRep.getRadiusY()));
+ }
+ mEditorVignette.commitLocalRepresentation();
+ }
+
+ public void setEditor(EditorVignette editorVignette) {
+ mEditorVignette = editorVignette;
+ }
+
+ @Override
+ public void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ computeEllipses();
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mVignetteRep == null) {
+ return;
+ }
+ Matrix toImg = getScreenToImageMatrix(false);
+ Matrix toScr = new Matrix();
+ toImg.invert(toScr);
+ float[] c = new float[] {
+ mVignetteRep.getCenterX(), mVignetteRep.getCenterY() };
+ toScr.mapPoints(c);
+ mElipse.setCenter(c[0], c[1]);
+ mElipse.setRadius(toScr.mapRadius(mVignetteRep.getRadiusX()),
+ toScr.mapRadius(mVignetteRep.getRadiusY()));
+
+ mElipse.draw(canvas);
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/Line.java b/src/com/android/gallery3d/filtershow/imageshow/Line.java
new file mode 100644
index 000000000..a767bd809
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/Line.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2012 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.filtershow.imageshow;
+
+public interface Line {
+ void setPoint1(float x, float y);
+ void setPoint2(float x, float y);
+ float getPoint1X();
+ float getPoint1Y();
+ float getPoint2X();
+ float getPoint2Y();
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
new file mode 100644
index 000000000..92e57bfc1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java
@@ -0,0 +1,581 @@
+/*
+ * Copyright (C) 2013 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.filtershow.imageshow;
+
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.history.HistoryItem;
+import com.android.gallery3d.filtershow.history.HistoryManager;
+import com.android.gallery3d.filtershow.pipeline.Buffer;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequest;
+import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller;
+import com.android.gallery3d.filtershow.pipeline.SharedBuffer;
+import com.android.gallery3d.filtershow.pipeline.SharedPreset;
+import com.android.gallery3d.filtershow.state.StateAdapter;
+
+import java.util.Vector;
+
+public class MasterImage implements RenderingRequestCaller {
+
+ private static final String LOGTAG = "MasterImage";
+ private boolean DEBUG = false;
+ private static final boolean DISABLEZOOM = false;
+ public static final int SMALL_BITMAP_DIM = 160;
+ public static final int MAX_BITMAP_DIM = 900;
+ private static MasterImage sMasterImage = null;
+
+ private boolean mSupportsHighRes = false;
+
+ private ImageFilter mCurrentFilter = null;
+ private ImagePreset mPreset = null;
+ private ImagePreset mLoadedPreset = null;
+ private ImagePreset mGeometryOnlyPreset = null;
+ private ImagePreset mFiltersOnlyPreset = null;
+
+ private SharedBuffer mPreviewBuffer = new SharedBuffer();
+ private SharedPreset mPreviewPreset = new SharedPreset();
+
+ private Bitmap mOriginalBitmapSmall = null;
+ private Bitmap mOriginalBitmapLarge = null;
+ private Bitmap mOriginalBitmapHighres = null;
+ private int mOrientation;
+ private Rect mOriginalBounds;
+ private final Vector<ImageShow> mLoadListeners = new Vector<ImageShow>();
+ private Uri mUri = null;
+ private int mZoomOrientation = ImageLoader.ORI_NORMAL;
+
+ private Bitmap mGeometryOnlyBitmap = null;
+ private Bitmap mFiltersOnlyBitmap = null;
+ private Bitmap mPartialBitmap = null;
+ private Bitmap mHighresBitmap = null;
+
+ private HistoryManager mHistory = null;
+ private StateAdapter mState = null;
+
+ private FilterShowActivity mActivity = null;
+
+ private Vector<ImageShow> mObservers = new Vector<ImageShow>();
+ private FilterRepresentation mCurrentFilterRepresentation;
+
+ private float mScaleFactor = 1.0f;
+ private float mMaxScaleFactor = 3.0f; // TODO: base this on the current view / image
+ private Point mTranslation = new Point();
+ private Point mOriginalTranslation = new Point();
+
+ private Point mImageShowSize = new Point();
+
+ private boolean mShowsOriginal;
+
+ private MasterImage() {
+ }
+
+ // TODO: remove singleton
+ public static void setMaster(MasterImage master) {
+ sMasterImage = master;
+ }
+
+ public static MasterImage getImage() {
+ if (sMasterImage == null) {
+ sMasterImage = new MasterImage();
+ }
+ return sMasterImage;
+ }
+
+ public Bitmap getOriginalBitmapSmall() {
+ return mOriginalBitmapSmall;
+ }
+
+ public Bitmap getOriginalBitmapLarge() {
+ return mOriginalBitmapLarge;
+ }
+
+ public Bitmap getOriginalBitmapHighres() {
+ return mOriginalBitmapHighres;
+ }
+
+ public void setOriginalBitmapHighres(Bitmap mOriginalBitmapHighres) {
+ this.mOriginalBitmapHighres = mOriginalBitmapHighres;
+ }
+
+ public int getOrientation() {
+ return mOrientation;
+ }
+
+ public Rect getOriginalBounds() {
+ return mOriginalBounds;
+ }
+
+ public void setOriginalBounds(Rect r) {
+ mOriginalBounds = r;
+ }
+
+ public Uri getUri() {
+ return mUri;
+ }
+
+ public void setUri(Uri uri) {
+ mUri = uri;
+ }
+
+ public int getZoomOrientation() {
+ return mZoomOrientation;
+ }
+
+ public void addListener(ImageShow imageShow) {
+ if (!mLoadListeners.contains(imageShow)) {
+ mLoadListeners.add(imageShow);
+ }
+ }
+
+ public void warnListeners() {
+ mActivity.runOnUiThread(mWarnListenersRunnable);
+ }
+
+ private Runnable mWarnListenersRunnable = new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < mLoadListeners.size(); i++) {
+ ImageShow imageShow = mLoadListeners.elementAt(i);
+ imageShow.imageLoaded();
+ }
+ invalidatePreview();
+ }
+ };
+
+ public boolean loadBitmap(Uri uri, int size) {
+ setUri(uri);
+ mOrientation = ImageLoader.getMetadataOrientation(mActivity, uri);
+ Rect originalBounds = new Rect();
+ mOriginalBitmapLarge = ImageLoader.loadOrientedConstrainedBitmap(uri, mActivity,
+ Math.min(MAX_BITMAP_DIM, size),
+ mOrientation, originalBounds);
+ setOriginalBounds(originalBounds);
+ if (mOriginalBitmapLarge == null) {
+ return false;
+ }
+ int sw = SMALL_BITMAP_DIM;
+ int sh = (int) (sw * (float) mOriginalBitmapLarge.getHeight() / mOriginalBitmapLarge
+ .getWidth());
+ mOriginalBitmapSmall = Bitmap.createScaledBitmap(mOriginalBitmapLarge, sw, sh, true);
+ mZoomOrientation = mOrientation;
+ warnListeners();
+ return true;
+ }
+
+ public void setSupportsHighRes(boolean value) {
+ mSupportsHighRes = value;
+ }
+
+ public void addObserver(ImageShow observer) {
+ if (mObservers.contains(observer)) {
+ return;
+ }
+ mObservers.add(observer);
+ }
+
+ public void setActivity(FilterShowActivity activity) {
+ mActivity = activity;
+ }
+
+ public FilterShowActivity getActivity() {
+ return mActivity;
+ }
+
+ public synchronized ImagePreset getPreset() {
+ return mPreset;
+ }
+
+ public synchronized ImagePreset getGeometryPreset() {
+ return mGeometryOnlyPreset;
+ }
+
+ public synchronized ImagePreset getFiltersOnlyPreset() {
+ return mFiltersOnlyPreset;
+ }
+
+ public synchronized void setPreset(ImagePreset preset,
+ FilterRepresentation change,
+ boolean addToHistory) {
+ if (DEBUG) {
+ preset.showFilters();
+ }
+ mPreset = preset;
+ mPreset.fillImageStateAdapter(mState);
+ if (addToHistory) {
+ HistoryItem historyItem = new HistoryItem(mPreset, change);
+ mHistory.addHistoryItem(historyItem);
+ }
+ updatePresets(true);
+ mActivity.updateCategories();
+ }
+
+ public void onHistoryItemClick(int position) {
+ HistoryItem historyItem = mHistory.getItem(position);
+ // We need a copy from the history
+ ImagePreset newPreset = new ImagePreset(historyItem.getImagePreset());
+ // don't need to add it to the history
+ setPreset(newPreset, historyItem.getFilterRepresentation(), false);
+ mHistory.setCurrentPreset(position);
+ }
+
+ public HistoryManager getHistory() {
+ return mHistory;
+ }
+
+ public StateAdapter getState() {
+ return mState;
+ }
+
+ public void setHistoryManager(HistoryManager adapter) {
+ mHistory = adapter;
+ }
+
+ public void setStateAdapter(StateAdapter adapter) {
+ mState = adapter;
+ }
+
+ public void setCurrentFilter(ImageFilter filter) {
+ mCurrentFilter = filter;
+ }
+
+ public ImageFilter getCurrentFilter() {
+ return mCurrentFilter;
+ }
+
+ public synchronized boolean hasModifications() {
+ // TODO: We need to have a better same effects check to see if two
+ // presets are functionally the same. Right now, we are relying on a
+ // stricter check as equals().
+ ImagePreset loadedPreset = getLoadedPreset();
+ if (mPreset == null) {
+ if (loadedPreset == null) {
+ return false;
+ } else {
+ return loadedPreset.hasModifications();
+ }
+ } else {
+ if (loadedPreset == null) {
+ return mPreset.hasModifications();
+ } else {
+ return !mPreset.equals(loadedPreset);
+ }
+ }
+ }
+
+ public SharedBuffer getPreviewBuffer() {
+ return mPreviewBuffer;
+ }
+
+ public SharedPreset getPreviewPreset() {
+ return mPreviewPreset;
+ }
+
+ public Bitmap getFilteredImage() {
+ mPreviewBuffer.swapConsumerIfNeeded(); // get latest bitmap
+ Buffer consumer = mPreviewBuffer.getConsumer();
+ if (consumer != null) {
+ return consumer.getBitmap();
+ }
+ return null;
+ }
+
+ public Bitmap getFiltersOnlyImage() {
+ return mFiltersOnlyBitmap;
+ }
+
+ public Bitmap getGeometryOnlyImage() {
+ return mGeometryOnlyBitmap;
+ }
+
+ public Bitmap getPartialImage() {
+ return mPartialBitmap;
+ }
+
+ public Bitmap getHighresImage() {
+ return mHighresBitmap;
+ }
+
+ public void notifyObservers() {
+ for (ImageShow observer : mObservers) {
+ observer.invalidate();
+ }
+ }
+
+ public void updatePresets(boolean force) {
+ if (force || mGeometryOnlyPreset == null) {
+ ImagePreset newPreset = new ImagePreset(mPreset);
+ newPreset.setDoApplyFilters(false);
+ newPreset.setDoApplyGeometry(true);
+ if (force || mGeometryOnlyPreset == null
+ || !newPreset.same(mGeometryOnlyPreset)) {
+ mGeometryOnlyPreset = newPreset;
+ RenderingRequest.post(mActivity, getOriginalBitmapLarge(),
+ mGeometryOnlyPreset, RenderingRequest.GEOMETRY_RENDERING, this);
+ }
+ }
+ if (force || mFiltersOnlyPreset == null) {
+ ImagePreset newPreset = new ImagePreset(mPreset);
+ newPreset.setDoApplyFilters(true);
+ newPreset.setDoApplyGeometry(false);
+ if (force || mFiltersOnlyPreset == null
+ || !newPreset.same(mFiltersOnlyPreset)) {
+ mFiltersOnlyPreset = newPreset;
+ RenderingRequest.post(mActivity, MasterImage.getImage().getOriginalBitmapLarge(),
+ mFiltersOnlyPreset, RenderingRequest.FILTERS_RENDERING, this);
+ }
+ }
+ invalidatePreview();
+ }
+
+ public FilterRepresentation getCurrentFilterRepresentation() {
+ return mCurrentFilterRepresentation;
+ }
+
+ public void setCurrentFilterRepresentation(FilterRepresentation currentFilterRepresentation) {
+ mCurrentFilterRepresentation = currentFilterRepresentation;
+ }
+
+ public void invalidateFiltersOnly() {
+ mFiltersOnlyPreset = null;
+ updatePresets(false);
+ }
+
+ public void invalidatePartialPreview() {
+ if (mPartialBitmap != null) {
+ mPartialBitmap = null;
+ notifyObservers();
+ }
+ }
+
+ public void invalidateHighresPreview() {
+ if (mHighresBitmap != null) {
+ mHighresBitmap = null;
+ notifyObservers();
+ }
+ }
+
+ public void invalidatePreview() {
+ mPreviewPreset.enqueuePreset(mPreset);
+ mPreviewBuffer.invalidate();
+ invalidatePartialPreview();
+ invalidateHighresPreview();
+ needsUpdatePartialPreview();
+ needsUpdateHighResPreview();
+ mActivity.getProcessingService().updatePreviewBuffer();
+ }
+
+ public void setImageShowSize(int w, int h) {
+ if (mImageShowSize.x != w || mImageShowSize.y != h) {
+ mImageShowSize.set(w, h);
+ needsUpdatePartialPreview();
+ needsUpdateHighResPreview();
+ }
+ }
+
+ private Matrix getImageToScreenMatrix(boolean reflectRotation) {
+ if (getOriginalBounds() == null || mImageShowSize.x == 0 || mImageShowSize.y == 0) {
+ return new Matrix();
+ }
+ Matrix m = GeometryMathUtils.getImageToScreenMatrix(mPreset.getGeometryFilters(),
+ reflectRotation, getOriginalBounds(), mImageShowSize.x, mImageShowSize.y);
+ if (m == null) {
+ m = new Matrix();
+ m.reset();
+ return m;
+ }
+ Point translate = getTranslation();
+ float scaleFactor = getScaleFactor();
+ m.postTranslate(translate.x, translate.y);
+ m.postScale(scaleFactor, scaleFactor, mImageShowSize.x / 2.0f, mImageShowSize.y / 2.0f);
+ return m;
+ }
+
+ private Matrix getScreenToImageMatrix(boolean reflectRotation) {
+ Matrix m = getImageToScreenMatrix(reflectRotation);
+ Matrix invert = new Matrix();
+ m.invert(invert);
+ return invert;
+ }
+
+ public void needsUpdateHighResPreview() {
+ if (!mSupportsHighRes) {
+ return;
+ }
+ if (mActivity.getProcessingService() == null) {
+ return;
+ }
+ mActivity.getProcessingService().postHighresRenderingRequest(mPreset,
+ getScaleFactor(), this);
+ invalidateHighresPreview();
+ }
+
+ public void needsUpdatePartialPreview() {
+ if (mPreset == null) {
+ return;
+ }
+ if (!mPreset.canDoPartialRendering()) {
+ invalidatePartialPreview();
+ return;
+ }
+ Matrix m = getScreenToImageMatrix(true);
+ RectF r = new RectF(0, 0, mImageShowSize.x, mImageShowSize.y);
+ RectF dest = new RectF();
+ m.mapRect(dest, r);
+ Rect bounds = new Rect();
+ dest.roundOut(bounds);
+ RenderingRequest.post(mActivity, null, mPreset, RenderingRequest.PARTIAL_RENDERING,
+ this, bounds, new Rect(0, 0, mImageShowSize.x, mImageShowSize.y));
+ invalidatePartialPreview();
+ }
+
+ @Override
+ public void available(RenderingRequest request) {
+ if (request.getBitmap() == null) {
+ return;
+ }
+
+ boolean needsCheckModification = false;
+ if (request.getType() == RenderingRequest.GEOMETRY_RENDERING) {
+ mGeometryOnlyBitmap = request.getBitmap();
+ needsCheckModification = true;
+ }
+ if (request.getType() == RenderingRequest.FILTERS_RENDERING) {
+ mFiltersOnlyBitmap = request.getBitmap();
+ notifyObservers();
+ needsCheckModification = true;
+ }
+ if (request.getType() == RenderingRequest.PARTIAL_RENDERING
+ && request.getScaleFactor() == getScaleFactor()) {
+ mPartialBitmap = request.getBitmap();
+ notifyObservers();
+ needsCheckModification = true;
+ }
+ if (request.getType() == RenderingRequest.HIGHRES_RENDERING) {
+ mHighresBitmap = request.getBitmap();
+ notifyObservers();
+ needsCheckModification = true;
+ }
+ if (needsCheckModification) {
+ mActivity.enableSave(hasModifications());
+ }
+ }
+
+ public static void reset() {
+ sMasterImage = null;
+ }
+
+ public float getScaleFactor() {
+ return mScaleFactor;
+ }
+
+ public void setScaleFactor(float scaleFactor) {
+ if (DISABLEZOOM) {
+ return;
+ }
+ if (scaleFactor == mScaleFactor) {
+ return;
+ }
+ mScaleFactor = scaleFactor;
+ invalidatePartialPreview();
+ }
+
+ public Point getTranslation() {
+ return mTranslation;
+ }
+
+ public void setTranslation(Point translation) {
+ if (DISABLEZOOM) {
+ mTranslation.x = 0;
+ mTranslation.y = 0;
+ return;
+ }
+ mTranslation.x = translation.x;
+ mTranslation.y = translation.y;
+ needsUpdatePartialPreview();
+ }
+
+ public Point getOriginalTranslation() {
+ return mOriginalTranslation;
+ }
+
+ public void setOriginalTranslation(Point originalTranslation) {
+ if (DISABLEZOOM) {
+ return;
+ }
+ mOriginalTranslation.x = originalTranslation.x;
+ mOriginalTranslation.y = originalTranslation.y;
+ }
+
+ public void resetTranslation() {
+ mTranslation.x = 0;
+ mTranslation.y = 0;
+ needsUpdatePartialPreview();
+ }
+
+ public Bitmap getThumbnailBitmap() {
+ return getOriginalBitmapSmall();
+ }
+
+ public Bitmap getLargeThumbnailBitmap() {
+ return getOriginalBitmapLarge();
+ }
+
+ public float getMaxScaleFactor() {
+ if (DISABLEZOOM) {
+ return 1;
+ }
+ return mMaxScaleFactor;
+ }
+
+ public void setMaxScaleFactor(float maxScaleFactor) {
+ mMaxScaleFactor = maxScaleFactor;
+ }
+
+ public boolean supportsHighRes() {
+ return mSupportsHighRes;
+ }
+
+ public void setShowsOriginal(boolean value) {
+ mShowsOriginal = value;
+ notifyObservers();
+ }
+
+ public boolean showsOriginal() {
+ return mShowsOriginal;
+ }
+
+ public void setLoadedPreset(ImagePreset preset) {
+ mLoadedPreset = preset;
+ }
+
+ public ImagePreset getLoadedPreset() {
+ return mLoadedPreset;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/Oval.java b/src/com/android/gallery3d/filtershow/imageshow/Oval.java
new file mode 100644
index 000000000..28f278f1c
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/Oval.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012 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.filtershow.imageshow;
+
+public interface Oval {
+ void setCenter(float x, float y);
+ void setRadius(float w, float h);
+ float getCenterX();
+ float getCenterY();
+ float getRadiusX();
+ float getRadiusY();
+ void setRadiusY(float y);
+ void setRadiusX(float x);
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/Spline.java b/src/com/android/gallery3d/filtershow/imageshow/Spline.java
new file mode 100644
index 000000000..3c27a4d0f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/imageshow/Spline.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2012 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.filtershow.imageshow;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.Vector;
+
+public class Spline {
+ private final Vector<ControlPoint> mPoints;
+ private static Drawable mCurveHandle;
+ private static int mCurveHandleSize;
+ private static int mCurveWidth;
+
+ public static final int RGB = 0;
+ public static final int RED = 1;
+ public static final int GREEN = 2;
+ public static final int BLUE = 3;
+ private static final String LOGTAG = "Spline";
+
+ private final Paint gPaint = new Paint();
+ private ControlPoint mCurrentControlPoint = null;
+
+ public Spline() {
+ mPoints = new Vector<ControlPoint>();
+ }
+
+ public Spline(Spline spline) {
+ mPoints = new Vector<ControlPoint>();
+ for (int i = 0; i < spline.mPoints.size(); i++) {
+ ControlPoint p = spline.mPoints.elementAt(i);
+ ControlPoint newPoint = new ControlPoint(p);
+ mPoints.add(newPoint);
+ if (spline.mCurrentControlPoint == p) {
+ mCurrentControlPoint = newPoint;
+ }
+ }
+ Collections.sort(mPoints);
+ }
+
+ public static void setCurveHandle(Drawable drawable, int size) {
+ mCurveHandle = drawable;
+ mCurveHandleSize = size;
+ }
+
+ public static void setCurveWidth(int width) {
+ mCurveWidth = width;
+ }
+
+ public static int curveHandleSize() {
+ return mCurveHandleSize;
+ }
+
+ public static int colorForCurve(int curveIndex) {
+ switch (curveIndex) {
+ case Spline.RED:
+ return Color.RED;
+ case GREEN:
+ return Color.GREEN;
+ case BLUE:
+ return Color.BLUE;
+ }
+ return Color.WHITE;
+ }
+
+ public boolean sameValues(Spline other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null) {
+ return false;
+ }
+
+ if (getNbPoints() != other.getNbPoints()) {
+ return false;
+ }
+
+ for (int i = 0; i < getNbPoints(); i++) {
+ ControlPoint p = mPoints.elementAt(i);
+ ControlPoint otherPoint = other.mPoints.elementAt(i);
+ if (!p.sameValues(otherPoint)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void didMovePoint(ControlPoint point) {
+ mCurrentControlPoint = point;
+ }
+
+ public void movePoint(int pick, float x, float y) {
+ if (pick < 0 || pick > mPoints.size() - 1) {
+ return;
+ }
+ ControlPoint point = mPoints.elementAt(pick);
+ point.x = x;
+ point.y = y;
+ didMovePoint(point);
+ }
+
+ public boolean isOriginal() {
+ if (this.getNbPoints() != 2) {
+ return false;
+ }
+ if (mPoints.elementAt(0).x != 0 || mPoints.elementAt(0).y != 1) {
+ return false;
+ }
+ if (mPoints.elementAt(1).x != 1 || mPoints.elementAt(1).y != 0) {
+ return false;
+ }
+ return true;
+ }
+
+ public void reset() {
+ mPoints.clear();
+ addPoint(0.0f, 1.0f);
+ addPoint(1.0f, 0.0f);
+ }
+
+ private void drawHandles(Canvas canvas, Drawable indicator, float centerX, float centerY) {
+ int left = (int) centerX - mCurveHandleSize / 2;
+ int top = (int) centerY - mCurveHandleSize / 2;
+ indicator.setBounds(left, top, left + mCurveHandleSize, top + mCurveHandleSize);
+ indicator.draw(canvas);
+ }
+
+ public float[] getAppliedCurve() {
+ float[] curve = new float[256];
+ ControlPoint[] points = new ControlPoint[mPoints.size()];
+ for (int i = 0; i < mPoints.size(); i++) {
+ ControlPoint p = mPoints.get(i);
+ points[i] = new ControlPoint(p.x, p.y);
+ }
+ double[] derivatives = solveSystem(points);
+ int start = 0;
+ int end = 256;
+ if (points[0].x != 0) {
+ start = (int) (points[0].x * 256);
+ }
+ if (points[points.length - 1].x != 1) {
+ end = (int) (points[points.length - 1].x * 256);
+ }
+ for (int i = 0; i < start; i++) {
+ curve[i] = 1.0f - points[0].y;
+ }
+ for (int i = end; i < 256; i++) {
+ curve[i] = 1.0f - points[points.length - 1].y;
+ }
+ for (int i = start; i < end; i++) {
+ ControlPoint cur = null;
+ ControlPoint next = null;
+ double x = i / 256.0;
+ int pivot = 0;
+ for (int j = 0; j < points.length - 1; j++) {
+ if (x >= points[j].x && x <= points[j + 1].x) {
+ pivot = j;
+ }
+ }
+ cur = points[pivot];
+ next = points[pivot + 1];
+ if (x <= next.x) {
+ double x1 = cur.x;
+ double x2 = next.x;
+ double y1 = cur.y;
+ double y2 = next.y;
+
+ // Use the second derivatives to apply the cubic spline
+ // equation:
+ double delta = (x2 - x1);
+ double delta2 = delta * delta;
+ double b = (x - x1) / delta;
+ double a = 1 - b;
+ double ta = a * y1;
+ double tb = b * y2;
+ double tc = (a * a * a - a) * derivatives[pivot];
+ double td = (b * b * b - b) * derivatives[pivot + 1];
+ double y = ta + tb + (delta2 / 6) * (tc + td);
+ if (y > 1.0f) {
+ y = 1.0f;
+ }
+ if (y < 0) {
+ y = 0;
+ }
+ curve[i] = (float) (1.0f - y);
+ } else {
+ curve[i] = 1.0f - next.y;
+ }
+ }
+ return curve;
+ }
+
+ private void drawGrid(Canvas canvas, float w, float h) {
+ // Grid
+ gPaint.setARGB(128, 150, 150, 150);
+ gPaint.setStrokeWidth(1);
+
+ float stepH = h / 9;
+ float stepW = w / 9;
+
+ // central diagonal
+ gPaint.setARGB(255, 100, 100, 100);
+ gPaint.setStrokeWidth(2);
+ canvas.drawLine(0, h, w, 0, gPaint);
+
+ gPaint.setARGB(128, 200, 200, 200);
+ gPaint.setStrokeWidth(4);
+ stepH = h / 3;
+ stepW = w / 3;
+ for (int j = 1; j < 3; j++) {
+ canvas.drawLine(0, j * stepH, w, j * stepH, gPaint);
+ canvas.drawLine(j * stepW, 0, j * stepW, h, gPaint);
+ }
+ canvas.drawLine(0, 0, 0, h, gPaint);
+ canvas.drawLine(w, 0, w, h, gPaint);
+ canvas.drawLine(0, 0, w, 0, gPaint);
+ canvas.drawLine(0, h, w, h, gPaint);
+ }
+
+ public void draw(Canvas canvas, int color, int canvasWidth, int canvasHeight,
+ boolean showHandles, boolean moving) {
+ float w = canvasWidth - mCurveHandleSize;
+ float h = canvasHeight - mCurveHandleSize;
+ float dx = mCurveHandleSize / 2;
+ float dy = mCurveHandleSize / 2;
+
+ // The cubic spline equation is (from numerical recipes in C):
+ // y = a(y_i) + b(y_i+1) + c(y"_i) + d(y"_i+1)
+ //
+ // with c(y"_i) and d(y"_i+1):
+ // c(y"_i) = 1/6 (a^3 - a) delta^2 (y"_i)
+ // d(y"_i_+1) = 1/6 (b^3 - b) delta^2 (y"_i+1)
+ //
+ // and delta:
+ // delta = x_i+1 - x_i
+ //
+ // To find the second derivatives y", we can rearrange the equation as:
+ // A(y"_i-1) + B(y"_i) + C(y"_i+1) = D
+ //
+ // With the coefficients A, B, C, D:
+ // A = 1/6 (x_i - x_i-1)
+ // B = 1/3 (x_i+1 - x_i-1)
+ // C = 1/6 (x_i+1 - x_i)
+ // D = (y_i+1 - y_i)/(x_i+1 - x_i) - (y_i - y_i-1)/(x_i - x_i-1)
+ //
+ // We can now easily solve the equation to find the second derivatives:
+ ControlPoint[] points = new ControlPoint[mPoints.size()];
+ for (int i = 0; i < mPoints.size(); i++) {
+ ControlPoint p = mPoints.get(i);
+ points[i] = new ControlPoint(p.x * w, p.y * h);
+ }
+ double[] derivatives = solveSystem(points);
+
+ Path path = new Path();
+ path.moveTo(0, points[0].y);
+ for (int i = 0; i < points.length - 1; i++) {
+ double x1 = points[i].x;
+ double x2 = points[i + 1].x;
+ double y1 = points[i].y;
+ double y2 = points[i + 1].y;
+
+ for (double x = x1; x < x2; x += 20) {
+ // Use the second derivatives to apply the cubic spline
+ // equation:
+ double delta = (x2 - x1);
+ double delta2 = delta * delta;
+ double b = (x - x1) / delta;
+ double a = 1 - b;
+ double ta = a * y1;
+ double tb = b * y2;
+ double tc = (a * a * a - a) * derivatives[i];
+ double td = (b * b * b - b) * derivatives[i + 1];
+ double y = ta + tb + (delta2 / 6) * (tc + td);
+ if (y > h) {
+ y = h;
+ }
+ if (y < 0) {
+ y = 0;
+ }
+ path.lineTo((float) x, (float) y);
+ }
+ }
+ canvas.save();
+ canvas.translate(dx, dy);
+ drawGrid(canvas, w, h);
+ ControlPoint lastPoint = points[points.length - 1];
+ path.lineTo(lastPoint.x, lastPoint.y);
+ path.lineTo(w, lastPoint.y);
+ Paint paint = new Paint();
+ paint.setAntiAlias(true);
+ paint.setFilterBitmap(true);
+ paint.setDither(true);
+ paint.setStyle(Paint.Style.STROKE);
+ int curveWidth = mCurveWidth;
+ if (showHandles) {
+ curveWidth *= 1.5;
+ }
+ paint.setStrokeWidth(curveWidth + 2);
+ paint.setColor(Color.BLACK);
+ canvas.drawPath(path, paint);
+
+ if (moving && mCurrentControlPoint != null) {
+ float px = mCurrentControlPoint.x * w;
+ float py = mCurrentControlPoint.y * h;
+ paint.setStrokeWidth(3);
+ paint.setColor(Color.BLACK);
+ canvas.drawLine(px, py, px, h, paint);
+ canvas.drawLine(0, py, px, py, paint);
+ paint.setStrokeWidth(1);
+ paint.setColor(color);
+ canvas.drawLine(px, py, px, h, paint);
+ canvas.drawLine(0, py, px, py, paint);
+ }
+
+ paint.setStrokeWidth(curveWidth);
+ paint.setColor(color);
+ canvas.drawPath(path, paint);
+ if (showHandles) {
+ for (int i = 0; i < points.length; i++) {
+ float x = points[i].x;
+ float y = points[i].y;
+ drawHandles(canvas, mCurveHandle, x, y);
+ }
+ }
+ canvas.restore();
+ }
+
+ double[] solveSystem(ControlPoint[] points) {
+ int n = points.length;
+ double[][] system = new double[n][3];
+ double[] result = new double[n]; // d
+ double[] solution = new double[n]; // returned coefficients
+ system[0][1] = 1;
+ system[n - 1][1] = 1;
+ double d6 = 1.0 / 6.0;
+ double d3 = 1.0 / 3.0;
+
+ // let's create a tridiagonal matrix representing the
+ // system, and apply the TDMA algorithm to solve it
+ // (see http://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
+ for (int i = 1; i < n - 1; i++) {
+ double deltaPrevX = points[i].x - points[i - 1].x;
+ double deltaX = points[i + 1].x - points[i - 1].x;
+ double deltaNextX = points[i + 1].x - points[i].x;
+ double deltaNextY = points[i + 1].y - points[i].y;
+ double deltaPrevY = points[i].y - points[i - 1].y;
+ system[i][0] = d6 * deltaPrevX; // a_i
+ system[i][1] = d3 * deltaX; // b_i
+ system[i][2] = d6 * deltaNextX; // c_i
+ result[i] = (deltaNextY / deltaNextX) - (deltaPrevY / deltaPrevX); // d_i
+ }
+
+ // Forward sweep
+ for (int i = 1; i < n; i++) {
+ // m = a_i/b_i-1
+ double m = system[i][0] / system[i - 1][1];
+ // b_i = b_i - m(c_i-1)
+ system[i][1] = system[i][1] - m * system[i - 1][2];
+ // d_i = d_i - m(d_i-1)
+ result[i] = result[i] - m * result[i - 1];
+ }
+
+ // Back substitution
+ solution[n - 1] = result[n - 1] / system[n - 1][1];
+ for (int i = n - 2; i >= 0; --i) {
+ solution[i] = (result[i] - system[i][2] * solution[i + 1]) / system[i][1];
+ }
+ return solution;
+ }
+
+ public int addPoint(float x, float y) {
+ return addPoint(new ControlPoint(x, y));
+ }
+
+ public int addPoint(ControlPoint v) {
+ mPoints.add(v);
+ Collections.sort(mPoints);
+ return mPoints.indexOf(v);
+ }
+
+ public void deletePoint(int n) {
+ mPoints.remove(n);
+ if (mPoints.size() < 2) {
+ reset();
+ }
+ Collections.sort(mPoints);
+ }
+
+ public int getNbPoints() {
+ return mPoints.size();
+ }
+
+ public ControlPoint getPoint(int n) {
+ return mPoints.elementAt(n);
+ }
+
+ public boolean isPointContained(float x, int n) {
+ for (int i = 0; i < n; i++) {
+ ControlPoint point = mPoints.elementAt(i);
+ if (point.x > x) {
+ return false;
+ }
+ }
+ for (int i = n + 1; i < mPoints.size(); i++) {
+ ControlPoint point = mPoints.elementAt(i);
+ if (point.x < x) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public Spline copy() {
+ Spline spline = new Spline();
+ for (int i = 0; i < mPoints.size(); i++) {
+ ControlPoint point = mPoints.elementAt(i);
+ spline.addPoint(point.copy());
+ }
+ return spline;
+ }
+
+ public void show() {
+ Log.v(LOGTAG, "show curve " + this);
+ for (int i = 0; i < mPoints.size(); i++) {
+ ControlPoint point = mPoints.elementAt(i);
+ Log.v(LOGTAG, "point " + i + " is (" + point.x + ", " + point.y + ")");
+ }
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/Buffer.java b/src/com/android/gallery3d/filtershow/pipeline/Buffer.java
new file mode 100644
index 000000000..744451229
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/Buffer.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import android.support.v8.renderscript.Allocation;
+import android.support.v8.renderscript.RenderScript;
+
+public class Buffer {
+ private static final String LOGTAG = "Buffer";
+ private Bitmap mBitmap;
+ private Allocation mAllocation;
+ private boolean mUseAllocation = false;
+ private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
+ private ImagePreset mPreset;
+
+ public Buffer(Bitmap bitmap) {
+ RenderScript rs = CachingPipeline.getRenderScriptContext();
+ if (bitmap != null) {
+ mBitmap = bitmap.copy(BITMAP_CONFIG, true);
+ }
+ if (mUseAllocation) {
+ // TODO: recreate the allocation when the RS context changes
+ mAllocation = Allocation.createFromBitmap(rs, mBitmap,
+ Allocation.MipmapControl.MIPMAP_NONE,
+ Allocation.USAGE_SHARED | Allocation.USAGE_SCRIPT);
+ }
+ }
+
+ public void setBitmap(Bitmap bitmap) {
+ mBitmap = bitmap.copy(BITMAP_CONFIG, true);
+ }
+
+ public Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ public Allocation getAllocation() {
+ return mAllocation;
+ }
+
+ public void sync() {
+ if (mUseAllocation) {
+ mAllocation.copyTo(mBitmap);
+ }
+ }
+
+ public ImagePreset getPreset() {
+ return mPreset;
+ }
+
+ public void setPreset(ImagePreset preset) {
+ if ((mPreset == null) || (!mPreset.same(preset))) {
+ mPreset = new ImagePreset(preset);
+ } else {
+ mPreset.updateWith(preset);
+ }
+ }
+}
+
diff --git a/src/com/android/gallery3d/filtershow/pipeline/CacheProcessing.java b/src/com/android/gallery3d/filtershow/pipeline/CacheProcessing.java
new file mode 100644
index 000000000..e0269e9bb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/CacheProcessing.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+
+import java.util.Vector;
+
+public class CacheProcessing {
+ private static final String LOGTAG = "CacheProcessing";
+ private static final boolean DEBUG = false;
+ private Vector<CacheStep> mSteps = new Vector<CacheStep>();
+
+ static class CacheStep {
+ FilterRepresentation representation;
+ Bitmap cache;
+ }
+
+ public Bitmap process(Bitmap originalBitmap,
+ Vector<FilterRepresentation> filters,
+ FilterEnvironment environment) {
+
+ if (filters.size() == 0) {
+ return originalBitmap;
+ }
+
+ // New set of filters, let's clear the cache and rebuild it.
+ if (filters.size() != mSteps.size()) {
+ mSteps.clear();
+ for (int i = 0; i < filters.size(); i++) {
+ FilterRepresentation representation = filters.elementAt(i);
+ CacheStep step = new CacheStep();
+ step.representation = representation.copy();
+ mSteps.add(step);
+ }
+ }
+
+ if (DEBUG) {
+ displayFilters(filters);
+ }
+
+ // First, let's find how similar we are in our cache
+ // compared to the current list of filters
+ int similarUpToIndex = -1;
+ for (int i = 0; i < filters.size(); i++) {
+ FilterRepresentation representation = filters.elementAt(i);
+ CacheStep step = mSteps.elementAt(i);
+ boolean similar = step.representation.equals(representation);
+ if (similar) {
+ similarUpToIndex = i;
+ } else {
+ break;
+ }
+ }
+ if (DEBUG) {
+ Log.v(LOGTAG, "similar up to index " + similarUpToIndex);
+ }
+
+ // Now, let's get the earliest cached result in our pipeline
+ Bitmap cacheBitmap = null;
+ int findBaseImageIndex = similarUpToIndex;
+ if (findBaseImageIndex > -1) {
+ while (findBaseImageIndex > 0
+ && mSteps.elementAt(findBaseImageIndex).cache == null) {
+ findBaseImageIndex--;
+ }
+ cacheBitmap = mSteps.elementAt(findBaseImageIndex).cache;
+ }
+ boolean emptyStack = false;
+ if (cacheBitmap == null) {
+ emptyStack = true;
+ // Damn, it's an empty stack, we have to start from scratch
+ // TODO: use a bitmap cache + RS allocation instead of Bitmap.copy()
+ cacheBitmap = originalBitmap.copy(Bitmap.Config.ARGB_8888, true);
+ if (findBaseImageIndex > -1) {
+ FilterRepresentation representation = filters.elementAt(findBaseImageIndex);
+ if (representation.getFilterType() != FilterRepresentation.TYPE_GEOMETRY) {
+ cacheBitmap = environment.applyRepresentation(representation, cacheBitmap);
+ }
+ mSteps.elementAt(findBaseImageIndex).representation = representation.copy();
+ mSteps.elementAt(findBaseImageIndex).cache = cacheBitmap;
+ }
+ if (DEBUG) {
+ Log.v(LOGTAG, "empty stack");
+ }
+ }
+
+ // Ok, so sadly the earliest cached result is before the index we want.
+ // We have to rebuild a new result for this position, and then cache it.
+ if (findBaseImageIndex != similarUpToIndex) {
+ if (DEBUG) {
+ Log.v(LOGTAG, "rebuild cacheBitmap from " + findBaseImageIndex
+ + " to " + similarUpToIndex);
+ }
+ // rebuild the cache image for this step
+ if (!emptyStack) {
+ cacheBitmap = cacheBitmap.copy(Bitmap.Config.ARGB_8888, true);
+ } else {
+ // if it was an empty stack, we already applied it
+ findBaseImageIndex ++;
+ }
+ for (int i = findBaseImageIndex; i <= similarUpToIndex; i++) {
+ FilterRepresentation representation = filters.elementAt(i);
+ if (representation.getFilterType() != FilterRepresentation.TYPE_GEOMETRY) {
+ cacheBitmap = environment.applyRepresentation(representation, cacheBitmap);
+ }
+ if (DEBUG) {
+ Log.v(LOGTAG, " - " + i + " => apply " + representation.getName());
+ }
+ }
+ // Let's cache it!
+ mSteps.elementAt(similarUpToIndex).cache = cacheBitmap;
+ }
+
+ if (DEBUG) {
+ Log.v(LOGTAG, "process pipeline from " + similarUpToIndex
+ + " to " + (filters.size() - 1));
+ }
+
+ // Now we are good to go, let's use the cacheBitmap as a starting point
+ for (int i = similarUpToIndex + 1; i < filters.size(); i++) {
+ FilterRepresentation representation = filters.elementAt(i);
+ CacheStep currentStep = mSteps.elementAt(i);
+ cacheBitmap = cacheBitmap.copy(Bitmap.Config.ARGB_8888, true);
+ if (representation.getFilterType() != FilterRepresentation.TYPE_GEOMETRY) {
+ cacheBitmap = environment.applyRepresentation(representation, cacheBitmap);
+ }
+ currentStep.representation = representation.copy();
+ currentStep.cache = cacheBitmap;
+ if (DEBUG) {
+ Log.v(LOGTAG, " - " + i + " => apply " + representation.getName());
+ }
+ }
+
+ if (DEBUG) {
+ Log.v(LOGTAG, "now let's cleanup the cache...");
+ displayNbBitmapsInCache();
+ }
+
+ // Let's see if we can cleanup the cache for unused bitmaps
+ for (int i = 0; i < similarUpToIndex; i++) {
+ CacheStep currentStep = mSteps.elementAt(i);
+ currentStep.cache = null;
+ }
+
+ if (DEBUG) {
+ Log.v(LOGTAG, "cleanup done...");
+ displayNbBitmapsInCache();
+ }
+ return cacheBitmap;
+ }
+
+ private void displayFilters(Vector<FilterRepresentation> filters) {
+ Log.v(LOGTAG, "------>>>");
+ for (int i = 0; i < filters.size(); i++) {
+ FilterRepresentation representation = filters.elementAt(i);
+ CacheStep step = mSteps.elementAt(i);
+ boolean similar = step.representation.equals(representation);
+ Log.v(LOGTAG, "[" + i + "] - " + representation.getName()
+ + " similar rep ? " + (similar ? "YES" : "NO")
+ + " -- bitmap: " + step.cache);
+ }
+ Log.v(LOGTAG, "<<<------");
+ }
+
+ private void displayNbBitmapsInCache() {
+ int nbBitmapsCached = 0;
+ for (int i = 0; i < mSteps.size(); i++) {
+ CacheStep step = mSteps.elementAt(i);
+ if (step.cache != null) {
+ nbBitmapsCached++;
+ }
+ }
+ Log.v(LOGTAG, "nb bitmaps in cache: " + nbBitmapsCached + " / " + mSteps.size());
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java b/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java
new file mode 100644
index 000000000..fc0d6ce49
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.support.v8.renderscript.Allocation;
+import android.support.v8.renderscript.RenderScript;
+import android.util.Log;
+
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+import java.util.Vector;
+
+public class CachingPipeline implements PipelineInterface {
+ private static final String LOGTAG = "CachingPipeline";
+ private boolean DEBUG = false;
+
+ private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
+
+ private static volatile RenderScript sRS = null;
+
+ private FiltersManager mFiltersManager = null;
+ private volatile Bitmap mOriginalBitmap = null;
+ private volatile Bitmap mResizedOriginalBitmap = null;
+
+ private FilterEnvironment mEnvironment = new FilterEnvironment();
+ private CacheProcessing mCachedProcessing = new CacheProcessing();
+
+
+ private volatile Allocation mOriginalAllocation = null;
+ private volatile Allocation mFiltersOnlyOriginalAllocation = null;
+
+ protected volatile Allocation mInPixelsAllocation;
+ protected volatile Allocation mOutPixelsAllocation;
+ private volatile int mWidth = 0;
+ private volatile int mHeight = 0;
+
+ private volatile float mPreviewScaleFactor = 1.0f;
+ private volatile float mHighResPreviewScaleFactor = 1.0f;
+ private volatile String mName = "";
+
+ public CachingPipeline(FiltersManager filtersManager, String name) {
+ mFiltersManager = filtersManager;
+ mName = name;
+ }
+
+ public static synchronized RenderScript getRenderScriptContext() {
+ return sRS;
+ }
+
+ public static synchronized void createRenderscriptContext(Context context) {
+ if (sRS != null) {
+ Log.w(LOGTAG, "A prior RS context exists when calling setRenderScriptContext");
+ destroyRenderScriptContext();
+ }
+ sRS = RenderScript.create(context);
+ }
+
+ public static synchronized void destroyRenderScriptContext() {
+ if (sRS != null) {
+ sRS.destroy();
+ }
+ sRS = null;
+ }
+
+ public void stop() {
+ mEnvironment.setStop(true);
+ }
+
+ public synchronized void reset() {
+ synchronized (CachingPipeline.class) {
+ if (getRenderScriptContext() == null) {
+ return;
+ }
+ mOriginalBitmap = null; // just a reference to the bitmap in ImageLoader
+ if (mResizedOriginalBitmap != null) {
+ mResizedOriginalBitmap.recycle();
+ mResizedOriginalBitmap = null;
+ }
+ if (mOriginalAllocation != null) {
+ mOriginalAllocation.destroy();
+ mOriginalAllocation = null;
+ }
+ if (mFiltersOnlyOriginalAllocation != null) {
+ mFiltersOnlyOriginalAllocation.destroy();
+ mFiltersOnlyOriginalAllocation = null;
+ }
+ mPreviewScaleFactor = 1.0f;
+ mHighResPreviewScaleFactor = 1.0f;
+
+ destroyPixelAllocations();
+ }
+ }
+
+ public Resources getResources() {
+ return sRS.getApplicationContext().getResources();
+ }
+
+ private synchronized void destroyPixelAllocations() {
+ if (DEBUG) {
+ Log.v(LOGTAG, "destroyPixelAllocations in " + getName());
+ }
+ if (mInPixelsAllocation != null) {
+ mInPixelsAllocation.destroy();
+ mInPixelsAllocation = null;
+ }
+ if (mOutPixelsAllocation != null) {
+ mOutPixelsAllocation.destroy();
+ mOutPixelsAllocation = null;
+ }
+ mWidth = 0;
+ mHeight = 0;
+ }
+
+ private String getType(RenderingRequest request) {
+ if (request.getType() == RenderingRequest.ICON_RENDERING) {
+ return "ICON_RENDERING";
+ }
+ if (request.getType() == RenderingRequest.FILTERS_RENDERING) {
+ return "FILTERS_RENDERING";
+ }
+ if (request.getType() == RenderingRequest.FULL_RENDERING) {
+ return "FULL_RENDERING";
+ }
+ if (request.getType() == RenderingRequest.GEOMETRY_RENDERING) {
+ return "GEOMETRY_RENDERING";
+ }
+ if (request.getType() == RenderingRequest.PARTIAL_RENDERING) {
+ return "PARTIAL_RENDERING";
+ }
+ if (request.getType() == RenderingRequest.HIGHRES_RENDERING) {
+ return "HIGHRES_RENDERING";
+ }
+ return "UNKNOWN TYPE!";
+ }
+
+ private void setupEnvironment(ImagePreset preset, boolean highResPreview) {
+ mEnvironment.setPipeline(this);
+ mEnvironment.setFiltersManager(mFiltersManager);
+ if (highResPreview) {
+ mEnvironment.setScaleFactor(mHighResPreviewScaleFactor);
+ } else {
+ mEnvironment.setScaleFactor(mPreviewScaleFactor);
+ }
+ mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW);
+ mEnvironment.setImagePreset(preset);
+ mEnvironment.setStop(false);
+ }
+
+ public void setOriginal(Bitmap bitmap) {
+ mOriginalBitmap = bitmap;
+ Log.v(LOGTAG,"setOriginal, size " + bitmap.getWidth() + " x " + bitmap.getHeight());
+ ImagePreset preset = MasterImage.getImage().getPreset();
+ setupEnvironment(preset, false);
+ updateOriginalAllocation(preset);
+ }
+
+ private synchronized boolean updateOriginalAllocation(ImagePreset preset) {
+ Bitmap originalBitmap = mOriginalBitmap;
+
+ if (originalBitmap == null) {
+ return false;
+ }
+
+ RenderScript RS = getRenderScriptContext();
+
+ Allocation filtersOnlyOriginalAllocation = mFiltersOnlyOriginalAllocation;
+ mFiltersOnlyOriginalAllocation = Allocation.createFromBitmap(RS, originalBitmap,
+ Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
+ if (filtersOnlyOriginalAllocation != null) {
+ filtersOnlyOriginalAllocation.destroy();
+ }
+
+ Allocation originalAllocation = mOriginalAllocation;
+ mResizedOriginalBitmap = preset.applyGeometry(originalBitmap, mEnvironment);
+ mOriginalAllocation = Allocation.createFromBitmap(RS, mResizedOriginalBitmap,
+ Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
+ if (originalAllocation != null) {
+ originalAllocation.destroy();
+ }
+
+ return true;
+ }
+
+ public void renderHighres(RenderingRequest request) {
+ synchronized (CachingPipeline.class) {
+ if (getRenderScriptContext() == null) {
+ return;
+ }
+ ImagePreset preset = request.getImagePreset();
+ setupEnvironment(preset, false);
+ Bitmap bitmap = MasterImage.getImage().getOriginalBitmapHighres();
+ if (bitmap == null) {
+ return;
+ }
+ // TODO: use a cache of bitmaps
+ bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true);
+ bitmap = preset.applyGeometry(bitmap, mEnvironment);
+
+ mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW);
+ Bitmap bmp = preset.apply(bitmap, mEnvironment);
+ if (!mEnvironment.needsStop()) {
+ request.setBitmap(bmp);
+ }
+ mFiltersManager.freeFilterResources(preset);
+ }
+ }
+
+ public synchronized void render(RenderingRequest request) {
+ synchronized (CachingPipeline.class) {
+ if (getRenderScriptContext() == null) {
+ return;
+ }
+ if (((request.getType() != RenderingRequest.PARTIAL_RENDERING
+ && request.getType() != RenderingRequest.HIGHRES_RENDERING)
+ && request.getBitmap() == null)
+ || request.getImagePreset() == null) {
+ return;
+ }
+
+ if (DEBUG) {
+ Log.v(LOGTAG, "render image of type " + getType(request));
+ }
+
+ Bitmap bitmap = request.getBitmap();
+ ImagePreset preset = request.getImagePreset();
+ setupEnvironment(preset,
+ request.getType() != RenderingRequest.HIGHRES_RENDERING);
+ mFiltersManager.freeFilterResources(preset);
+
+ if (request.getType() == RenderingRequest.PARTIAL_RENDERING) {
+ MasterImage master = MasterImage.getImage();
+ bitmap = ImageLoader.getScaleOneImageForPreset(master.getActivity(),
+ master.getUri(), request.getBounds(),
+ request.getDestination());
+ if (bitmap == null) {
+ Log.w(LOGTAG, "could not get bitmap for: " + getType(request));
+ return;
+ }
+ }
+
+ if (request.getType() == RenderingRequest.HIGHRES_RENDERING) {
+ bitmap = MasterImage.getImage().getOriginalBitmapHighres();
+ if (bitmap != null) {
+ bitmap = preset.applyGeometry(bitmap, mEnvironment);
+ }
+ }
+
+ if (request.getType() == RenderingRequest.FULL_RENDERING
+ || request.getType() == RenderingRequest.GEOMETRY_RENDERING
+ || request.getType() == RenderingRequest.FILTERS_RENDERING) {
+ updateOriginalAllocation(preset);
+ }
+
+ if (DEBUG) {
+ Log.v(LOGTAG, "after update, req bitmap (" + bitmap.getWidth() + "x" + bitmap.getHeight()
+ + " ? resizeOriginal (" + mResizedOriginalBitmap.getWidth() + "x"
+ + mResizedOriginalBitmap.getHeight());
+ }
+
+ if (request.getType() == RenderingRequest.FULL_RENDERING
+ || request.getType() == RenderingRequest.GEOMETRY_RENDERING) {
+ mOriginalAllocation.copyTo(bitmap);
+ } else if (request.getType() == RenderingRequest.FILTERS_RENDERING) {
+ mFiltersOnlyOriginalAllocation.copyTo(bitmap);
+ }
+
+ if (request.getType() == RenderingRequest.FULL_RENDERING
+ || request.getType() == RenderingRequest.FILTERS_RENDERING
+ || request.getType() == RenderingRequest.ICON_RENDERING
+ || request.getType() == RenderingRequest.PARTIAL_RENDERING
+ || request.getType() == RenderingRequest.HIGHRES_RENDERING
+ || request.getType() == RenderingRequest.STYLE_ICON_RENDERING) {
+
+ if (request.getType() == RenderingRequest.ICON_RENDERING) {
+ mEnvironment.setQuality(FilterEnvironment.QUALITY_ICON);
+ } else {
+ mEnvironment.setQuality(FilterEnvironment.QUALITY_PREVIEW);
+ }
+
+ Bitmap bmp = preset.apply(bitmap, mEnvironment);
+ if (!mEnvironment.needsStop()) {
+ request.setBitmap(bmp);
+ }
+ mFiltersManager.freeFilterResources(preset);
+ }
+ }
+ }
+
+ public synchronized void renderImage(ImagePreset preset, Allocation in, Allocation out) {
+ synchronized (CachingPipeline.class) {
+ if (getRenderScriptContext() == null) {
+ return;
+ }
+ setupEnvironment(preset, false);
+ mFiltersManager.freeFilterResources(preset);
+ preset.applyFilters(-1, -1, in, out, mEnvironment);
+ boolean copyOut = false;
+ if (preset.nbFilters() > 0) {
+ copyOut = true;
+ }
+ preset.applyBorder(in, out, copyOut, mEnvironment);
+ }
+ }
+
+ public synchronized Bitmap renderFinalImage(Bitmap bitmap, ImagePreset preset) {
+ synchronized (CachingPipeline.class) {
+ if (getRenderScriptContext() == null) {
+ return bitmap;
+ }
+ setupEnvironment(preset, false);
+ mEnvironment.setQuality(FilterEnvironment.QUALITY_FINAL);
+ mEnvironment.setScaleFactor(1.0f);
+ mFiltersManager.freeFilterResources(preset);
+ bitmap = preset.applyGeometry(bitmap, mEnvironment);
+ bitmap = preset.apply(bitmap, mEnvironment);
+ return bitmap;
+ }
+ }
+
+ public Bitmap renderGeometryIcon(Bitmap bitmap, ImagePreset preset) {
+ return GeometryMathUtils.applyGeometryRepresentations(preset.getGeometryFilters(), bitmap);
+ }
+
+ public void compute(SharedBuffer buffer, ImagePreset preset, int type) {
+ if (getRenderScriptContext() == null) {
+ return;
+ }
+ setupEnvironment(preset, false);
+ Vector<FilterRepresentation> filters = preset.getFilters();
+ Bitmap result = mCachedProcessing.process(mOriginalBitmap, filters, mEnvironment);
+ buffer.setProducer(result);
+ }
+
+ public synchronized void computeOld(SharedBuffer buffer, ImagePreset preset, int type) {
+ synchronized (CachingPipeline.class) {
+ if (getRenderScriptContext() == null) {
+ return;
+ }
+ if (DEBUG) {
+ Log.v(LOGTAG, "compute preset " + preset);
+ preset.showFilters();
+ }
+
+ String thread = Thread.currentThread().getName();
+ long time = System.currentTimeMillis();
+ setupEnvironment(preset, false);
+ mFiltersManager.freeFilterResources(preset);
+
+ Bitmap resizedOriginalBitmap = mResizedOriginalBitmap;
+ if (updateOriginalAllocation(preset) || buffer.getProducer() == null) {
+ resizedOriginalBitmap = mResizedOriginalBitmap;
+ buffer.setProducer(resizedOriginalBitmap);
+ mEnvironment.cache(buffer.getProducer());
+ }
+
+ Bitmap bitmap = buffer.getProducer().getBitmap();
+ long time2 = System.currentTimeMillis();
+
+ if (bitmap == null || (bitmap.getWidth() != resizedOriginalBitmap.getWidth())
+ || (bitmap.getHeight() != resizedOriginalBitmap.getHeight())) {
+ mEnvironment.cache(buffer.getProducer());
+ buffer.setProducer(resizedOriginalBitmap);
+ bitmap = buffer.getProducer().getBitmap();
+ }
+ mOriginalAllocation.copyTo(bitmap);
+
+ Bitmap tmpbitmap = preset.apply(bitmap, mEnvironment);
+ if (tmpbitmap != bitmap) {
+ mEnvironment.cache(buffer.getProducer());
+ buffer.setProducer(tmpbitmap);
+ }
+
+ mFiltersManager.freeFilterResources(preset);
+
+ time = System.currentTimeMillis() - time;
+ time2 = System.currentTimeMillis() - time2;
+ if (DEBUG) {
+ Log.v(LOGTAG, "Applying type " + type + " filters to bitmap "
+ + bitmap + " (" + bitmap.getWidth() + " x " + bitmap.getHeight()
+ + ") took " + time + " ms, " + time2 + " ms for the filter, on thread " + thread);
+ }
+ }
+ }
+
+ public boolean needsRepaint() {
+ SharedBuffer buffer = MasterImage.getImage().getPreviewBuffer();
+ return buffer.checkRepaintNeeded();
+ }
+
+ public void setPreviewScaleFactor(float previewScaleFactor) {
+ mPreviewScaleFactor = previewScaleFactor;
+ }
+
+ public void setHighResPreviewScaleFactor(float highResPreviewScaleFactor) {
+ mHighResPreviewScaleFactor = highResPreviewScaleFactor;
+ }
+
+ public synchronized boolean isInitialized() {
+ return getRenderScriptContext() != null && mOriginalBitmap != null;
+ }
+
+ public boolean prepareRenderscriptAllocations(Bitmap bitmap) {
+ RenderScript RS = getRenderScriptContext();
+ boolean needsUpdate = false;
+ if (mOutPixelsAllocation == null || mInPixelsAllocation == null ||
+ bitmap.getWidth() != mWidth || bitmap.getHeight() != mHeight) {
+ destroyPixelAllocations();
+ Bitmap bitmapBuffer = bitmap;
+ if (bitmap.getConfig() == null || bitmap.getConfig() != BITMAP_CONFIG) {
+ bitmapBuffer = bitmap.copy(BITMAP_CONFIG, true);
+ }
+ mOutPixelsAllocation = Allocation.createFromBitmap(RS, bitmapBuffer,
+ Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
+ mInPixelsAllocation = Allocation.createTyped(RS,
+ mOutPixelsAllocation.getType());
+ needsUpdate = true;
+ }
+ if (RS != null) {
+ mInPixelsAllocation.copyFrom(bitmap);
+ }
+ if (bitmap.getWidth() != mWidth
+ || bitmap.getHeight() != mHeight) {
+ mWidth = bitmap.getWidth();
+ mHeight = bitmap.getHeight();
+ needsUpdate = true;
+ }
+ if (DEBUG) {
+ Log.v(LOGTAG, "prepareRenderscriptAllocations: " + needsUpdate + " in " + getName());
+ }
+ return needsUpdate;
+ }
+
+ public synchronized Allocation getInPixelsAllocation() {
+ return mInPixelsAllocation;
+ }
+
+ public synchronized Allocation getOutPixelsAllocation() {
+ return mOutPixelsAllocation;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public RenderScript getRSContext() {
+ return CachingPipeline.getRenderScriptContext();
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/FilterEnvironment.java b/src/com/android/gallery3d/filtershow/pipeline/FilterEnvironment.java
new file mode 100644
index 000000000..4fac956be
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/FilterEnvironment.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.support.v8.renderscript.Allocation;
+
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation.Rotation;
+import com.android.gallery3d.filtershow.filters.FiltersManagerInterface;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+
+public class FilterEnvironment {
+ private static final String LOGTAG = "FilterEnvironment";
+ private ImagePreset mImagePreset;
+ private float mScaleFactor;
+ private int mQuality;
+ private FiltersManagerInterface mFiltersManager;
+ private PipelineInterface mPipeline;
+ private volatile boolean mStop = false;
+
+ public static final int QUALITY_ICON = 0;
+ public static final int QUALITY_PREVIEW = 1;
+ public static final int QUALITY_FINAL = 2;
+
+ public synchronized boolean needsStop() {
+ return mStop;
+ }
+
+ public synchronized void setStop(boolean stop) {
+ this.mStop = stop;
+ }
+
+ private HashMap<Long, WeakReference<Bitmap>>
+ bitmapCach = new HashMap<Long, WeakReference<Bitmap>>();
+
+ private HashMap<Integer, Integer>
+ generalParameters = new HashMap<Integer, Integer>();
+
+ public void cache(Buffer buffer) {
+ if (buffer == null) {
+ return;
+ }
+ Bitmap bitmap = buffer.getBitmap();
+ if (bitmap == null) {
+ return;
+ }
+ Long key = calcKey(bitmap.getWidth(), bitmap.getHeight());
+ bitmapCach.put(key, new WeakReference<Bitmap>(bitmap));
+ }
+
+ public Bitmap getBitmap(int w, int h) {
+ Long key = calcKey(w, h);
+ WeakReference<Bitmap> ref = bitmapCach.remove(key);
+ Bitmap bitmap = null;
+ if (ref != null) {
+ bitmap = ref.get();
+ }
+ if (bitmap == null) {
+ bitmap = Bitmap.createBitmap(
+ w, h, Bitmap.Config.ARGB_8888);
+ }
+ return bitmap;
+ }
+
+ private Long calcKey(long w, long h) {
+ return (w << 32) | (h << 32);
+ }
+
+ public void setImagePreset(ImagePreset imagePreset) {
+ mImagePreset = imagePreset;
+ }
+
+ public ImagePreset getImagePreset() {
+ return mImagePreset;
+ }
+
+ public void setScaleFactor(float scaleFactor) {
+ mScaleFactor = scaleFactor;
+ }
+
+ public float getScaleFactor() {
+ return mScaleFactor;
+ }
+
+ public void setQuality(int quality) {
+ mQuality = quality;
+ }
+
+ public int getQuality() {
+ return mQuality;
+ }
+
+ public void setFiltersManager(FiltersManagerInterface filtersManager) {
+ mFiltersManager = filtersManager;
+ }
+
+ public FiltersManagerInterface getFiltersManager() {
+ return mFiltersManager;
+ }
+
+ public void applyRepresentation(FilterRepresentation representation,
+ Allocation in, Allocation out) {
+ ImageFilter filter = mFiltersManager.getFilterForRepresentation(representation);
+ filter.useRepresentation(representation);
+ filter.setEnvironment(this);
+ if (filter.supportsAllocationInput()) {
+ filter.apply(in, out);
+ }
+ filter.setGeneralParameters();
+ filter.setEnvironment(null);
+ }
+
+ public Bitmap applyRepresentation(FilterRepresentation representation, Bitmap bitmap) {
+ if (representation instanceof FilterUserPresetRepresentation) {
+ // we allow instances of FilterUserPresetRepresentation in a preset only to know if one
+ // has been applied (so we can show this in the UI). But as all the filters in them are
+ // applied directly they do not themselves need to do any kind of filtering.
+ return bitmap;
+ }
+ ImageFilter filter = mFiltersManager.getFilterForRepresentation(representation);
+ filter.useRepresentation(representation);
+ filter.setEnvironment(this);
+ Bitmap ret = filter.apply(bitmap, mScaleFactor, mQuality);
+ filter.setGeneralParameters();
+ filter.setEnvironment(null);
+ return ret;
+ }
+
+ public PipelineInterface getPipeline() {
+ return mPipeline;
+ }
+
+ public void setPipeline(PipelineInterface cachingPipeline) {
+ mPipeline = cachingPipeline;
+ }
+
+ public synchronized void clearGeneralParameters() {
+ generalParameters = null;
+ }
+
+ public synchronized Integer getGeneralParameter(int id) {
+ if (generalParameters == null || !generalParameters.containsKey(id)) {
+ return null;
+ }
+ return generalParameters.get(id);
+ }
+
+ public synchronized void setGeneralParameter(int id, int value) {
+ if (generalParameters == null) {
+ generalParameters = new HashMap<Integer, Integer>();
+ }
+
+ generalParameters.put(id, value);
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/HighresRenderingRequestTask.java b/src/com/android/gallery3d/filtershow/pipeline/HighresRenderingRequestTask.java
new file mode 100644
index 000000000..5a0eb4d45
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/HighresRenderingRequestTask.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+
+public class HighresRenderingRequestTask extends ProcessingTask {
+
+ private CachingPipeline mHighresPreviewPipeline = null;
+ private boolean mPipelineIsOn = false;
+
+ public void setHighresPreviewScaleFactor(float highResPreviewScale) {
+ mHighresPreviewPipeline.setHighResPreviewScaleFactor(highResPreviewScale);
+ }
+
+ public void setPreviewScaleFactor(float previewScale) {
+ mHighresPreviewPipeline.setPreviewScaleFactor(previewScale);
+ }
+
+ static class Render implements Request {
+ RenderingRequest request;
+ }
+
+ static class RenderResult implements Result {
+ RenderingRequest request;
+ }
+
+ public HighresRenderingRequestTask() {
+ mHighresPreviewPipeline = new CachingPipeline(
+ FiltersManager.getHighresManager(), "Highres");
+ }
+
+ public void setOriginal(Bitmap bitmap) {
+ mHighresPreviewPipeline.setOriginal(bitmap);
+ }
+
+ public void setOriginalBitmapHighres(Bitmap originalHires) {
+ mPipelineIsOn = true;
+ }
+
+ public void stop() {
+ mHighresPreviewPipeline.stop();
+ }
+
+ public void postRenderingRequest(RenderingRequest request) {
+ if (!mPipelineIsOn) {
+ return;
+ }
+ Render render = new Render();
+ render.request = request;
+ postRequest(render);
+ }
+
+ @Override
+ public Result doInBackground(Request message) {
+ RenderingRequest request = ((Render) message).request;
+ RenderResult result = null;
+ mHighresPreviewPipeline.renderHighres(request);
+ result = new RenderResult();
+ result.request = request;
+ return result;
+ }
+
+ @Override
+ public void onResult(Result message) {
+ if (message == null) {
+ return;
+ }
+ RenderingRequest request = ((RenderResult) message).request;
+ request.markAvailable();
+ }
+
+ @Override
+ public boolean isDelayedTask() { return true; }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java b/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java
new file mode 100644
index 000000000..d34216ad6
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java
@@ -0,0 +1,694 @@
+/*
+ * Copyright (C) 2012 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.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.support.v8.renderscript.Allocation;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.BaseFiltersManager;
+import com.android.gallery3d.filtershow.filters.FilterCropRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterFxRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterImageBorderRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.state.State;
+import com.android.gallery3d.filtershow.state.StateAdapter;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Vector;
+
+public class ImagePreset {
+
+ private static final String LOGTAG = "ImagePreset";
+
+ private Vector<FilterRepresentation> mFilters = new Vector<FilterRepresentation>();
+
+ private boolean mDoApplyGeometry = true;
+ private boolean mDoApplyFilters = true;
+
+ private boolean mPartialRendering = false;
+ private Rect mPartialRenderingBounds;
+ private static final boolean DEBUG = false;
+
+ public ImagePreset() {
+ }
+
+ public ImagePreset(ImagePreset source) {
+ for (int i = 0; i < source.mFilters.size(); i++) {
+ FilterRepresentation sourceRepresentation = source.mFilters.elementAt(i);
+ mFilters.add(sourceRepresentation.copy());
+ }
+ }
+
+ public Vector<FilterRepresentation> getFilters() {
+ return mFilters;
+ }
+
+ public FilterRepresentation getFilterRepresentation(int position) {
+ FilterRepresentation representation = null;
+
+ representation = mFilters.elementAt(position).copy();
+
+ return representation;
+ }
+
+ private static boolean sameSerializationName(String a, String b) {
+ if (a != null && b != null) {
+ return a.equals(b);
+ } else {
+ return a == null && b == null;
+ }
+ }
+
+ public static boolean sameSerializationName(FilterRepresentation a, FilterRepresentation b) {
+ if (a == null || b == null) {
+ return false;
+ }
+ return sameSerializationName(a.getSerializationName(), b.getSerializationName());
+ }
+
+ public int getPositionForRepresentation(FilterRepresentation representation) {
+ for (int i = 0; i < mFilters.size(); i++) {
+ if (sameSerializationName(mFilters.elementAt(i), representation)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private FilterRepresentation getFilterRepresentationForType(int type) {
+ for (int i = 0; i < mFilters.size(); i++) {
+ if (mFilters.elementAt(i).getFilterType() == type) {
+ return mFilters.elementAt(i);
+ }
+ }
+ return null;
+ }
+
+ public int getPositionForType(int type) {
+ for (int i = 0; i < mFilters.size(); i++) {
+ if (mFilters.elementAt(i).getFilterType() == type) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public FilterRepresentation getFilterRepresentationCopyFrom(
+ FilterRepresentation filterRepresentation) {
+ // TODO: add concept of position in the filters (to allow multiple instances)
+ if (filterRepresentation == null) {
+ return null;
+ }
+ int position = getPositionForRepresentation(filterRepresentation);
+ if (position == -1) {
+ return null;
+ }
+ FilterRepresentation representation = mFilters.elementAt(position);
+ if (representation != null) {
+ representation = representation.copy();
+ }
+ return representation;
+ }
+
+ public void updateFilterRepresentations(Collection<FilterRepresentation> reps) {
+ for (FilterRepresentation r : reps) {
+ updateOrAddFilterRepresentation(r);
+ }
+ }
+
+ public void updateOrAddFilterRepresentation(FilterRepresentation rep) {
+ int pos = getPositionForRepresentation(rep);
+ if (pos != -1) {
+ mFilters.elementAt(pos).useParametersFrom(rep);
+ } else {
+ addFilter(rep.copy());
+ }
+ }
+
+ public void setDoApplyGeometry(boolean value) {
+ mDoApplyGeometry = value;
+ }
+
+ public void setDoApplyFilters(boolean value) {
+ mDoApplyFilters = value;
+ }
+
+ public boolean getDoApplyFilters() {
+ return mDoApplyFilters;
+ }
+
+ public boolean hasModifications() {
+ for (int i = 0; i < mFilters.size(); i++) {
+ FilterRepresentation filter = mFilters.elementAt(i);
+ if (!filter.isNil()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean isPanoramaSafe() {
+ for (FilterRepresentation representation : mFilters) {
+ if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY
+ && !representation.isNil()) {
+ return false;
+ }
+ if (representation.getFilterType() == FilterRepresentation.TYPE_BORDER
+ && !representation.isNil()) {
+ return false;
+ }
+ if (representation.getFilterType() == FilterRepresentation.TYPE_VIGNETTE
+ && !representation.isNil()) {
+ return false;
+ }
+ if (representation.getFilterType() == FilterRepresentation.TYPE_TINYPLANET
+ && !representation.isNil()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public boolean same(ImagePreset preset) {
+ if (preset == null) {
+ return false;
+ }
+
+ if (preset.mFilters.size() != mFilters.size()) {
+ return false;
+ }
+
+ if (mDoApplyGeometry != preset.mDoApplyGeometry) {
+ return false;
+ }
+
+ if (mDoApplyFilters != preset.mDoApplyFilters) {
+ if (mFilters.size() > 0 || preset.mFilters.size() > 0) {
+ return false;
+ }
+ }
+
+ if (mDoApplyFilters && preset.mDoApplyFilters) {
+ for (int i = 0; i < preset.mFilters.size(); i++) {
+ FilterRepresentation a = preset.mFilters.elementAt(i);
+ FilterRepresentation b = mFilters.elementAt(i);
+
+ if (!a.same(b)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public int similarUpTo(ImagePreset preset) {
+ for (int i = 0; i < preset.mFilters.size(); i++) {
+ FilterRepresentation a = preset.mFilters.elementAt(i);
+ if (i < mFilters.size()) {
+ FilterRepresentation b = mFilters.elementAt(i);
+ if (!a.same(b)) {
+ return i;
+ }
+ if (!a.equals(b)) {
+ return i;
+ }
+ } else {
+ return i;
+ }
+ }
+ return preset.mFilters.size();
+ }
+
+ public void showFilters() {
+ Log.v(LOGTAG, "\\\\\\ showFilters -- " + mFilters.size() + " filters");
+ int n = 0;
+ for (FilterRepresentation representation : mFilters) {
+ Log.v(LOGTAG, " filter " + n + " : " + representation.toString());
+ n++;
+ }
+ Log.v(LOGTAG, "/// showFilters -- " + mFilters.size() + " filters");
+ }
+
+ public FilterRepresentation getLastRepresentation() {
+ if (mFilters.size() > 0) {
+ return mFilters.lastElement();
+ }
+ return null;
+ }
+
+ public void removeFilter(FilterRepresentation filterRepresentation) {
+ if (filterRepresentation.getFilterType() == FilterRepresentation.TYPE_BORDER) {
+ for (int i = 0; i < mFilters.size(); i++) {
+ if (mFilters.elementAt(i).getFilterType()
+ == filterRepresentation.getFilterType()) {
+ mFilters.remove(i);
+ break;
+ }
+ }
+ } else {
+ for (int i = 0; i < mFilters.size(); i++) {
+ if (sameSerializationName(mFilters.elementAt(i), filterRepresentation)) {
+ mFilters.remove(i);
+ break;
+ }
+ }
+ }
+ }
+
+ // If the filter is an "None" effect or border, then just don't add this filter.
+ public void addFilter(FilterRepresentation representation) {
+ if (representation instanceof FilterUserPresetRepresentation) {
+ ImagePreset preset = ((FilterUserPresetRepresentation) representation).getImagePreset();
+ // user preset replace everything but geometry
+ mFilters.clear();
+ for (int i = 0; i < preset.nbFilters(); i++) {
+ addFilter(preset.getFilterRepresentation(i));
+ }
+ mFilters.add(representation);
+ } else if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
+ // Add geometry filter, removing duplicates and do-nothing operations.
+ for (int i = 0; i < mFilters.size(); i++) {
+ if (sameSerializationName(representation, mFilters.elementAt(i))) {
+ mFilters.remove(i);
+ }
+ }
+ if (!representation.isNil()) {
+ mFilters.add(representation);
+ }
+ } else if (representation.getFilterType() == FilterRepresentation.TYPE_BORDER) {
+ removeFilter(representation);
+ if (!isNoneBorderFilter(representation)) {
+ mFilters.add(representation);
+ }
+ } else if (representation.getFilterType() == FilterRepresentation.TYPE_FX) {
+ boolean found = false;
+ for (int i = 0; i < mFilters.size(); i++) {
+ FilterRepresentation current = mFilters.elementAt(i);
+ int type = current.getFilterType();
+ if (found) {
+ if (type != FilterRepresentation.TYPE_VIGNETTE) {
+ mFilters.remove(i);
+ continue;
+ }
+ }
+ if (type == FilterRepresentation.TYPE_FX) {
+ if (current instanceof FilterUserPresetRepresentation) {
+ ImagePreset preset = ((FilterUserPresetRepresentation) current)
+ .getImagePreset();
+ // If we had an existing user preset, let's remove all the presets that
+ // were added by it
+ for (int j = 0; j < preset.nbFilters(); j++) {
+ FilterRepresentation rep = preset.getFilterRepresentation(j);
+ int pos = getPositionForRepresentation(rep);
+ if (pos != -1) {
+ mFilters.remove(pos);
+ }
+ }
+ int pos = getPositionForRepresentation(current);
+ if (pos != -1) {
+ mFilters.remove(pos);
+ } else {
+ pos = 0;
+ }
+ if (!isNoneFxFilter(representation)) {
+ mFilters.add(pos, representation);
+ }
+
+ } else {
+ mFilters.remove(i);
+ if (!isNoneFxFilter(representation)) {
+ mFilters.add(i, representation);
+ }
+ }
+ found = true;
+ }
+ }
+ if (!found) {
+ if (!isNoneFxFilter(representation)) {
+ mFilters.add(representation);
+ }
+ }
+ } else {
+ mFilters.add(representation);
+ }
+ }
+
+ private boolean isNoneBorderFilter(FilterRepresentation representation) {
+ return representation instanceof FilterImageBorderRepresentation &&
+ ((FilterImageBorderRepresentation) representation).getDrawableResource() == 0;
+ }
+
+ private boolean isNoneFxFilter(FilterRepresentation representation) {
+ return representation instanceof FilterFxRepresentation &&
+ ((FilterFxRepresentation) representation).getNameResource() == R.string.none;
+ }
+
+ public FilterRepresentation getRepresentation(FilterRepresentation filterRepresentation) {
+ for (int i = 0; i < mFilters.size(); i++) {
+ FilterRepresentation representation = mFilters.elementAt(i);
+ if (sameSerializationName(representation, filterRepresentation)) {
+ return representation;
+ }
+ }
+ return null;
+ }
+
+ public Bitmap apply(Bitmap original, FilterEnvironment environment) {
+ Bitmap bitmap = original;
+ bitmap = applyFilters(bitmap, -1, -1, environment);
+ return applyBorder(bitmap, environment);
+ }
+
+ public Collection<FilterRepresentation> getGeometryFilters() {
+ ArrayList<FilterRepresentation> geometry = new ArrayList<FilterRepresentation>();
+ for (FilterRepresentation r : mFilters) {
+ if (r.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
+ geometry.add(r);
+ }
+ }
+ return geometry;
+ }
+
+ public FilterRepresentation getFilterWithSerializationName(String serializationName) {
+ for (FilterRepresentation r : mFilters) {
+ if (r != null) {
+ if (sameSerializationName(r.getSerializationName(), serializationName)) {
+ return r.copy();
+ }
+ }
+ }
+ return null;
+ }
+
+ public Bitmap applyGeometry(Bitmap bitmap, FilterEnvironment environment) {
+ // Apply any transform -- 90 rotate, flip, straighten, crop
+ // Returns a new bitmap.
+ if (mDoApplyGeometry) {
+ bitmap = GeometryMathUtils.applyGeometryRepresentations(getGeometryFilters(), bitmap);
+ }
+ return bitmap;
+ }
+
+ public Bitmap applyBorder(Bitmap bitmap, FilterEnvironment environment) {
+ // get the border from the list of filters.
+ FilterRepresentation border = getFilterRepresentationForType(
+ FilterRepresentation.TYPE_BORDER);
+ if (border != null && mDoApplyGeometry) {
+ bitmap = environment.applyRepresentation(border, bitmap);
+ if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) {
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+ "SaveBorder", border.getSerializationName(), 1);
+ }
+ }
+ return bitmap;
+ }
+
+ public int nbFilters() {
+ return mFilters.size();
+ }
+
+ public Bitmap applyFilters(Bitmap bitmap, int from, int to, FilterEnvironment environment) {
+ if (mDoApplyFilters) {
+ if (from < 0) {
+ from = 0;
+ }
+ if (to == -1) {
+ to = mFilters.size();
+ }
+ if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) {
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+ "SaveFilters", "Total", to - from + 1);
+ }
+ for (int i = from; i < to; i++) {
+ FilterRepresentation representation = mFilters.elementAt(i);
+ if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
+ // skip the geometry as it's already applied.
+ continue;
+ }
+ if (representation.getFilterType() == FilterRepresentation.TYPE_BORDER) {
+ // for now, let's skip the border as it will be applied in
+ // applyBorder()
+ // TODO: might be worth getting rid of applyBorder.
+ continue;
+ }
+ bitmap = environment.applyRepresentation(representation, bitmap);
+ if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) {
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+ "SaveFilter", representation.getSerializationName(), 1);
+ }
+ if (environment.needsStop()) {
+ return bitmap;
+ }
+ }
+ }
+
+ return bitmap;
+ }
+
+ public void applyBorder(Allocation in, Allocation out,
+ boolean copyOut, FilterEnvironment environment) {
+ FilterRepresentation border = getFilterRepresentationForType(
+ FilterRepresentation.TYPE_BORDER);
+ if (border != null && mDoApplyGeometry) {
+ // TODO: should keep the bitmap around
+ Allocation bitmapIn = in;
+ if (copyOut) {
+ bitmapIn = Allocation.createTyped(
+ CachingPipeline.getRenderScriptContext(), in.getType());
+ bitmapIn.copyFrom(out);
+ }
+ environment.applyRepresentation(border, bitmapIn, out);
+ }
+ }
+
+ public void applyFilters(int from, int to, Allocation in, Allocation out,
+ FilterEnvironment environment) {
+ if (mDoApplyFilters) {
+ if (from < 0) {
+ from = 0;
+ }
+ if (to == -1) {
+ to = mFilters.size();
+ }
+ for (int i = from; i < to; i++) {
+ FilterRepresentation representation = mFilters.elementAt(i);
+ if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY
+ || representation.getFilterType() == FilterRepresentation.TYPE_BORDER) {
+ continue;
+ }
+ if (i > from) {
+ in.copyFrom(out);
+ }
+ environment.applyRepresentation(representation, in, out);
+ }
+ }
+ }
+
+ public boolean canDoPartialRendering() {
+ if (MasterImage.getImage().getZoomOrientation() != ImageLoader.ORI_NORMAL) {
+ return false;
+ }
+ for (int i = 0; i < mFilters.size(); i++) {
+ FilterRepresentation representation = mFilters.elementAt(i);
+ if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY
+ && !representation.isNil()) {
+ return false;
+ }
+ if (!representation.supportsPartialRendering()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public void fillImageStateAdapter(StateAdapter imageStateAdapter) {
+ if (imageStateAdapter == null) {
+ return;
+ }
+ Vector<State> states = new Vector<State>();
+ for (FilterRepresentation filter : mFilters) {
+ if (filter.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
+ // TODO: supports Geometry representations in the state panel.
+ continue;
+ }
+ if (filter instanceof FilterUserPresetRepresentation) {
+ // do not show the user preset itself in the state panel
+ continue;
+ }
+ State state = new State(filter.getName());
+ state.setFilterRepresentation(filter);
+ states.add(state);
+ }
+ imageStateAdapter.fill(states);
+ }
+
+ public void setPartialRendering(boolean partialRendering, Rect bounds) {
+ mPartialRendering = partialRendering;
+ mPartialRenderingBounds = bounds;
+ }
+
+ public boolean isPartialRendering() {
+ return mPartialRendering;
+ }
+
+ public Rect getPartialRenderingBounds() {
+ return mPartialRenderingBounds;
+ }
+
+ public Vector<ImageFilter> getUsedFilters(BaseFiltersManager filtersManager) {
+ Vector<ImageFilter> usedFilters = new Vector<ImageFilter>();
+ for (int i = 0; i < mFilters.size(); i++) {
+ FilterRepresentation representation = mFilters.elementAt(i);
+ ImageFilter filter = filtersManager.getFilterForRepresentation(representation);
+ usedFilters.add(filter);
+ }
+ return usedFilters;
+ }
+
+ public String getJsonString(String name) {
+ StringWriter swriter = new StringWriter();
+ try {
+ JsonWriter writer = new JsonWriter(swriter);
+ writeJson(writer, name);
+ writer.close();
+ } catch (IOException e) {
+ return null;
+ }
+ return swriter.toString();
+ }
+
+ public void writeJson(JsonWriter writer, String name) {
+ int numFilters = mFilters.size();
+ try {
+ writer.beginObject();
+ for (int i = 0; i < numFilters; i++) {
+ FilterRepresentation filter = mFilters.get(i);
+ if (filter instanceof FilterUserPresetRepresentation) {
+ continue;
+ }
+ String sname = filter.getSerializationName();
+ if (DEBUG) {
+ Log.v(LOGTAG, "Serialization: " + sname);
+ if (sname == null) {
+ Log.v(LOGTAG, "Serialization name null for filter: " + filter);
+ }
+ }
+ writer.name(sname);
+ filter.serializeRepresentation(writer);
+ }
+ writer.endObject();
+
+ } catch (IOException e) {
+ Log.e(LOGTAG,"Error encoding JASON",e);
+ }
+ }
+
+ /**
+ * populates preset from JSON string
+ *
+ * @param filterString a JSON string
+ * @return true on success if false ImagePreset is undefined
+ */
+ public boolean readJsonFromString(String filterString) {
+ if (DEBUG) {
+ Log.v(LOGTAG, "reading preset: \"" + filterString + "\"");
+ }
+ StringReader sreader = new StringReader(filterString);
+ try {
+ JsonReader reader = new JsonReader(sreader);
+ boolean ok = readJson(reader);
+ if (!ok) {
+ reader.close();
+ return false;
+ }
+ reader.close();
+ } catch (Exception e) {
+ Log.e(LOGTAG, "parsing the filter parameters:", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * populates preset from JSON stream
+ *
+ * @param sreader a JSON string
+ * @return true on success if false ImagePreset is undefined
+ */
+ public boolean readJson(JsonReader sreader) throws IOException {
+ sreader.beginObject();
+
+ while (sreader.hasNext()) {
+ String name = sreader.nextName();
+ FilterRepresentation filter = creatFilterFromName(name);
+ if (filter == null) {
+ Log.w(LOGTAG, "UNKNOWN FILTER! " + name);
+ return false;
+ }
+ filter.deSerializeRepresentation(sreader);
+ addFilter(filter);
+ }
+ sreader.endObject();
+ return true;
+ }
+
+ FilterRepresentation creatFilterFromName(String name) {
+ if (FilterRotateRepresentation.SERIALIZATION_NAME.equals(name)) {
+ return new FilterRotateRepresentation();
+ } else if (FilterMirrorRepresentation.SERIALIZATION_NAME.equals(name)) {
+ return new FilterMirrorRepresentation();
+ } else if (FilterStraightenRepresentation.SERIALIZATION_NAME.equals(name)) {
+ return new FilterStraightenRepresentation();
+ } else if (FilterCropRepresentation.SERIALIZATION_NAME.equals(name)) {
+ return new FilterCropRepresentation();
+ }
+ FiltersManager filtersManager = FiltersManager.getManager();
+ return filtersManager.createFilterFromName(name);
+ }
+
+ public void updateWith(ImagePreset preset) {
+ if (preset.mFilters.size() != mFilters.size()) {
+ Log.e(LOGTAG, "Updating a preset with an incompatible one");
+ return;
+ }
+ for (int i = 0; i < mFilters.size(); i++) {
+ FilterRepresentation destRepresentation = mFilters.elementAt(i);
+ FilterRepresentation sourceRepresentation = preset.mFilters.elementAt(i);
+ destRepresentation.useParametersFrom(sourceRepresentation);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java b/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java
new file mode 100644
index 000000000..b760edd5a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.File;
+
+public class ImageSavingTask extends ProcessingTask {
+ private ProcessingService mProcessingService;
+
+ static class SaveRequest implements Request {
+ Uri sourceUri;
+ Uri selectedUri;
+ File destinationFile;
+ ImagePreset preset;
+ boolean flatten;
+ int quality;
+ }
+
+ static class UpdateBitmap implements Update {
+ Bitmap bitmap;
+ }
+
+ static class UpdateProgress implements Update {
+ int max;
+ int current;
+ }
+
+ static class URIResult implements Result {
+ Uri uri;
+ }
+
+ public ImageSavingTask(ProcessingService service) {
+ mProcessingService = service;
+ }
+
+ public void saveImage(Uri sourceUri, Uri selectedUri,
+ File destinationFile, ImagePreset preset, boolean flatten, int quality) {
+ SaveRequest request = new SaveRequest();
+ request.sourceUri = sourceUri;
+ request.selectedUri = selectedUri;
+ request.destinationFile = destinationFile;
+ request.preset = preset;
+ request.flatten = flatten;
+ request.quality = quality;
+ postRequest(request);
+ }
+
+ public Result doInBackground(Request message) {
+ SaveRequest request = (SaveRequest) message;
+ Uri sourceUri = request.sourceUri;
+ Uri selectedUri = request.selectedUri;
+ File destinationFile = request.destinationFile;
+ ImagePreset preset = request.preset;
+ boolean flatten = request.flatten;
+ // We create a small bitmap showing the result that we can
+ // give to the notification
+ UpdateBitmap updateBitmap = new UpdateBitmap();
+ updateBitmap.bitmap = createNotificationBitmap(sourceUri, preset);
+ postUpdate(updateBitmap);
+ SaveImage saveImage = new SaveImage(mProcessingService, sourceUri,
+ selectedUri, destinationFile,
+ new SaveImage.Callback() {
+ @Override
+ public void onProgress(int max, int current) {
+ UpdateProgress updateProgress = new UpdateProgress();
+ updateProgress.max = max;
+ updateProgress.current = current;
+ postUpdate(updateProgress);
+ }
+ });
+ Uri uri = saveImage.processAndSaveImage(preset, !flatten, request.quality);
+ URIResult result = new URIResult();
+ result.uri = uri;
+ return result;
+ }
+
+ @Override
+ public void onResult(Result message) {
+ URIResult result = (URIResult) message;
+ mProcessingService.completeSaveImage(result.uri);
+ }
+
+ @Override
+ public void onUpdate(Update message) {
+ if (message instanceof UpdateBitmap) {
+ Bitmap bitmap = ((UpdateBitmap) message).bitmap;
+ mProcessingService.updateNotificationWithBitmap(bitmap);
+ }
+ if (message instanceof UpdateProgress) {
+ UpdateProgress progress = (UpdateProgress) message;
+ mProcessingService.updateProgress(progress.max, progress.current);
+ }
+ }
+
+ private Bitmap createNotificationBitmap(Uri sourceUri, ImagePreset preset) {
+ int notificationBitmapSize = Resources.getSystem().getDimensionPixelSize(
+ android.R.dimen.notification_large_icon_width);
+ Bitmap bitmap = ImageLoader.loadConstrainedBitmap(sourceUri, getContext(),
+ notificationBitmapSize, null, true);
+ CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), "Thumb");
+ return pipeline.renderFinalImage(bitmap, preset);
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/PipelineInterface.java b/src/com/android/gallery3d/filtershow/pipeline/PipelineInterface.java
new file mode 100644
index 000000000..d53768c95
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/PipelineInterface.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.support.v8.renderscript.Allocation;
+import android.support.v8.renderscript.RenderScript;
+
+public interface PipelineInterface {
+ public String getName();
+ public Resources getResources();
+ public Allocation getInPixelsAllocation();
+ public Allocation getOutPixelsAllocation();
+ public boolean prepareRenderscriptAllocations(Bitmap bitmap);
+ public RenderScript getRSContext();
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java
new file mode 100644
index 000000000..d0504d11f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.filters.ImageFilter;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.File;
+
+public class ProcessingService extends Service {
+ private static final String LOGTAG = "ProcessingService";
+ private static final boolean SHOW_IMAGE = false;
+ private int mNotificationId;
+ private NotificationManager mNotifyMgr = null;
+ private Notification.Builder mBuilder = null;
+
+ private static final String PRESET = "preset";
+ private static final String QUALITY = "quality";
+ private static final String SOURCE_URI = "sourceUri";
+ private static final String SELECTED_URI = "selectedUri";
+ private static final String DESTINATION_FILE = "destinationFile";
+ private static final String SAVING = "saving";
+ private static final String FLATTEN = "flatten";
+
+ private ProcessingTaskController mProcessingTaskController;
+ private ImageSavingTask mImageSavingTask;
+ private UpdatePreviewTask mUpdatePreviewTask;
+ private HighresRenderingRequestTask mHighresRenderingRequestTask;
+ private RenderingRequestTask mRenderingRequestTask;
+
+ private final IBinder mBinder = new LocalBinder();
+ private FilterShowActivity mFiltershowActivity;
+
+ private boolean mSaving = false;
+ private boolean mNeedsAlive = false;
+
+ public void setFiltershowActivity(FilterShowActivity filtershowActivity) {
+ mFiltershowActivity = filtershowActivity;
+ }
+
+ public void setOriginalBitmap(Bitmap originalBitmap) {
+ if (mUpdatePreviewTask == null) {
+ return;
+ }
+ mUpdatePreviewTask.setOriginal(originalBitmap);
+ mHighresRenderingRequestTask.setOriginal(originalBitmap);
+ mRenderingRequestTask.setOriginal(originalBitmap);
+ }
+
+ public void updatePreviewBuffer() {
+ mHighresRenderingRequestTask.stop();
+ mUpdatePreviewTask.updatePreview();
+ }
+
+ public void postRenderingRequest(RenderingRequest request) {
+ mRenderingRequestTask.postRenderingRequest(request);
+ }
+
+ public void postHighresRenderingRequest(ImagePreset preset, float scaleFactor,
+ RenderingRequestCaller caller) {
+ RenderingRequest request = new RenderingRequest();
+ // TODO: use the triple buffer preset as UpdatePreviewTask does instead of creating a copy
+ ImagePreset passedPreset = new ImagePreset(preset);
+ request.setOriginalImagePreset(preset);
+ request.setScaleFactor(scaleFactor);
+ request.setImagePreset(passedPreset);
+ request.setType(RenderingRequest.HIGHRES_RENDERING);
+ request.setCaller(caller);
+ mHighresRenderingRequestTask.postRenderingRequest(request);
+ }
+
+ public void setHighresPreviewScaleFactor(float highResPreviewScale) {
+ mHighresRenderingRequestTask.setHighresPreviewScaleFactor(highResPreviewScale);
+ }
+
+ public void setPreviewScaleFactor(float previewScale) {
+ mHighresRenderingRequestTask.setPreviewScaleFactor(previewScale);
+ mRenderingRequestTask.setPreviewScaleFactor(previewScale);
+ }
+
+ public void setOriginalBitmapHighres(Bitmap originalHires) {
+ mHighresRenderingRequestTask.setOriginalBitmapHighres(originalHires);
+ }
+
+ public class LocalBinder extends Binder {
+ public ProcessingService getService() {
+ return ProcessingService.this;
+ }
+ }
+
+ public static Intent getSaveIntent(Context context, ImagePreset preset, File destination,
+ Uri selectedImageUri, Uri sourceImageUri, boolean doFlatten, int quality) {
+ Intent processIntent = new Intent(context, ProcessingService.class);
+ processIntent.putExtra(ProcessingService.SOURCE_URI,
+ sourceImageUri.toString());
+ processIntent.putExtra(ProcessingService.SELECTED_URI,
+ selectedImageUri.toString());
+ processIntent.putExtra(ProcessingService.QUALITY, quality);
+ if (destination != null) {
+ processIntent.putExtra(ProcessingService.DESTINATION_FILE, destination.toString());
+ }
+ processIntent.putExtra(ProcessingService.PRESET,
+ preset.getJsonString(context.getString(R.string.saved)));
+ processIntent.putExtra(ProcessingService.SAVING, true);
+ if (doFlatten) {
+ processIntent.putExtra(ProcessingService.FLATTEN, true);
+ }
+ return processIntent;
+ }
+
+
+ @Override
+ public void onCreate() {
+ mProcessingTaskController = new ProcessingTaskController(this);
+ mImageSavingTask = new ImageSavingTask(this);
+ mUpdatePreviewTask = new UpdatePreviewTask();
+ mHighresRenderingRequestTask = new HighresRenderingRequestTask();
+ mRenderingRequestTask = new RenderingRequestTask();
+ mProcessingTaskController.add(mImageSavingTask);
+ mProcessingTaskController.add(mUpdatePreviewTask);
+ mProcessingTaskController.add(mHighresRenderingRequestTask);
+ mProcessingTaskController.add(mRenderingRequestTask);
+ setupPipeline();
+ }
+
+ @Override
+ public void onDestroy() {
+ tearDownPipeline();
+ mProcessingTaskController.quit();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ mNeedsAlive = true;
+ if (intent != null && intent.getBooleanExtra(SAVING, false)) {
+ // we save using an intent to keep the service around after the
+ // activity has been destroyed.
+ String presetJson = intent.getStringExtra(PRESET);
+ String source = intent.getStringExtra(SOURCE_URI);
+ String selected = intent.getStringExtra(SELECTED_URI);
+ String destination = intent.getStringExtra(DESTINATION_FILE);
+ int quality = intent.getIntExtra(QUALITY, 100);
+ boolean flatten = intent.getBooleanExtra(FLATTEN, false);
+ Uri sourceUri = Uri.parse(source);
+ Uri selectedUri = null;
+ if (selected != null) {
+ selectedUri = Uri.parse(selected);
+ }
+ File destinationFile = null;
+ if (destination != null) {
+ destinationFile = new File(destination);
+ }
+ ImagePreset preset = new ImagePreset();
+ preset.readJsonFromString(presetJson);
+ mNeedsAlive = false;
+ mSaving = true;
+ handleSaveRequest(sourceUri, selectedUri, destinationFile, preset, flatten, quality);
+ }
+ return START_REDELIVER_INTENT;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ public void onStart() {
+ mNeedsAlive = true;
+ if (!mSaving && mFiltershowActivity != null) {
+ mFiltershowActivity.updateUIAfterServiceStarted();
+ }
+ }
+
+ public void handleSaveRequest(Uri sourceUri, Uri selectedUri,
+ File destinationFile, ImagePreset preset, boolean flatten, int quality) {
+ mNotifyMgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+
+ mNotificationId++;
+
+ mBuilder =
+ new Notification.Builder(this)
+ .setSmallIcon(R.drawable.filtershow_button_fx)
+ .setContentTitle(getString(R.string.filtershow_notification_label))
+ .setContentText(getString(R.string.filtershow_notification_message));
+
+ startForeground(mNotificationId, mBuilder.build());
+
+ updateProgress(SaveImage.MAX_PROCESSING_STEPS, 0);
+
+ // Process the image
+
+ mImageSavingTask.saveImage(sourceUri, selectedUri, destinationFile,
+ preset, flatten, quality);
+ }
+
+ public void updateNotificationWithBitmap(Bitmap bitmap) {
+ mBuilder.setLargeIcon(bitmap);
+ mNotifyMgr.notify(mNotificationId, mBuilder.build());
+ }
+
+ public void updateProgress(int max, int current) {
+ mBuilder.setProgress(max, current, false);
+ mNotifyMgr.notify(mNotificationId, mBuilder.build());
+ }
+
+ public void completeSaveImage(Uri result) {
+ if (SHOW_IMAGE) {
+ // TODO: we should update the existing image in Gallery instead
+ Intent viewImage = new Intent(Intent.ACTION_VIEW, result);
+ viewImage.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(viewImage);
+ }
+ stopForeground(true);
+ stopSelf();
+ if (mNeedsAlive) {
+ // If the app has been restarted while we were saving...
+ mFiltershowActivity.updateUIAfterServiceStarted();
+ } else if (mFiltershowActivity.isSimpleEditAction()) {
+ // terminate now
+ mFiltershowActivity.completeSaveImage(result);
+ }
+ }
+
+ private void setupPipeline() {
+ Resources res = getResources();
+ FiltersManager.setResources(res);
+ CachingPipeline.createRenderscriptContext(this);
+
+ FiltersManager filtersManager = FiltersManager.getManager();
+ filtersManager.addLooks(this);
+ filtersManager.addBorders(this);
+ filtersManager.addTools(this);
+ filtersManager.addEffects();
+
+ FiltersManager highresFiltersManager = FiltersManager.getHighresManager();
+ highresFiltersManager.addLooks(this);
+ highresFiltersManager.addBorders(this);
+ highresFiltersManager.addTools(this);
+ highresFiltersManager.addEffects();
+ }
+
+ private void tearDownPipeline() {
+ ImageFilter.resetStatics();
+ FiltersManager.getPreviewManager().freeRSFilterScripts();
+ FiltersManager.getManager().freeRSFilterScripts();
+ FiltersManager.getHighresManager().freeRSFilterScripts();
+ FiltersManager.reset();
+ CachingPipeline.destroyRenderScriptContext();
+ }
+
+ static {
+ System.loadLibrary("jni_filtershow_filters");
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java
new file mode 100644
index 000000000..8d3e8110f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+
+public abstract class ProcessingTask {
+ private ProcessingTaskController mTaskController;
+ private Handler mProcessingHandler;
+ private Handler mResultHandler;
+ private int mType;
+ private static final int DELAY = 300;
+
+ static interface Request {}
+ static interface Update {}
+ static interface Result {}
+
+ public boolean postRequest(Request message) {
+ Message msg = mProcessingHandler.obtainMessage(mType);
+ msg.obj = message;
+ if (isPriorityTask()) {
+ if (mProcessingHandler.hasMessages(getType())) {
+ return false;
+ }
+ mProcessingHandler.sendMessageAtFrontOfQueue(msg);
+ } else if (isDelayedTask()) {
+ if (mProcessingHandler.hasMessages(getType())) {
+ mProcessingHandler.removeMessages(getType());
+ }
+ mProcessingHandler.sendMessageDelayed(msg, DELAY);
+ } else {
+ mProcessingHandler.sendMessage(msg);
+ }
+ return true;
+ }
+
+ public void postUpdate(Update message) {
+ Message msg = mResultHandler.obtainMessage(mType);
+ msg.obj = message;
+ msg.arg1 = ProcessingTaskController.UPDATE;
+ mResultHandler.sendMessage(msg);
+ }
+
+ public void processRequest(Request message) {
+ Object result = doInBackground(message);
+ Message msg = mResultHandler.obtainMessage(mType);
+ msg.obj = result;
+ msg.arg1 = ProcessingTaskController.RESULT;
+ mResultHandler.sendMessage(msg);
+ }
+
+ public void added(ProcessingTaskController taskController) {
+ mTaskController = taskController;
+ mResultHandler = taskController.getResultHandler();
+ mProcessingHandler = taskController.getProcessingHandler();
+ mType = taskController.getReservedType();
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public Context getContext() {
+ return mTaskController.getContext();
+ }
+
+ public abstract Result doInBackground(Request message);
+ public abstract void onResult(Result message);
+ public void onUpdate(Update message) {}
+ public boolean isPriorityTask() { return false; }
+ public boolean isDelayedTask() { return false; }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java
new file mode 100644
index 000000000..b54bbb044
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.HashMap;
+
+public class ProcessingTaskController implements Handler.Callback {
+ private static final String LOGTAG = "ProcessingTaskController";
+
+ private Context mContext;
+ private HandlerThread mHandlerThread = null;
+ private Handler mProcessingHandler = null;
+ private int mCurrentType;
+ private HashMap<Integer, ProcessingTask> mTasks = new HashMap<Integer, ProcessingTask>();
+
+ public final static int RESULT = 1;
+ public final static int UPDATE = 2;
+
+ private final Handler mResultHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ ProcessingTask task = mTasks.get(msg.what);
+ if (task != null) {
+ if (msg.arg1 == RESULT) {
+ task.onResult((ProcessingTask.Result) msg.obj);
+ } else if (msg.arg1 == UPDATE) {
+ task.onUpdate((ProcessingTask.Update) msg.obj);
+ } else {
+ Log.w(LOGTAG, "received unknown message! " + msg.arg1);
+ }
+ }
+ }
+ };
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ ProcessingTask task = mTasks.get(msg.what);
+ if (task != null) {
+ task.processRequest((ProcessingTask.Request) msg.obj);
+ return true;
+ }
+ return false;
+ }
+
+ public ProcessingTaskController(Context context) {
+ mContext = context;
+ mHandlerThread = new HandlerThread("ProcessingTaskController",
+ android.os.Process.THREAD_PRIORITY_FOREGROUND);
+ mHandlerThread.start();
+ mProcessingHandler = new Handler(mHandlerThread.getLooper(), this);
+ }
+
+ public Handler getProcessingHandler() {
+ return mProcessingHandler;
+ }
+
+ public Handler getResultHandler() {
+ return mResultHandler;
+ }
+
+ public int getReservedType() {
+ return mCurrentType++;
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public void add(ProcessingTask task) {
+ task.added(this);
+ mTasks.put(task.getType(), task);
+ }
+
+ public void quit() {
+ mHandlerThread.quit();
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/RenderingRequest.java b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequest.java
new file mode 100644
index 000000000..ef4bb9bc0
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import com.android.gallery3d.app.Log;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class RenderingRequest {
+ private static final String LOGTAG = "RenderingRequest";
+ private boolean mIsDirect = false;
+ private Bitmap mBitmap = null;
+ private ImagePreset mImagePreset = null;
+ private ImagePreset mOriginalImagePreset = null;
+ private RenderingRequestCaller mCaller = null;
+ private float mScaleFactor = 1.0f;
+ private Rect mBounds = null;
+ private Rect mDestination = null;
+ private int mType = FULL_RENDERING;
+ public static final int FULL_RENDERING = 0;
+ public static final int FILTERS_RENDERING = 1;
+ public static final int GEOMETRY_RENDERING = 2;
+ public static final int ICON_RENDERING = 3;
+ public static final int PARTIAL_RENDERING = 4;
+ public static final int HIGHRES_RENDERING = 5;
+ public static final int STYLE_ICON_RENDERING = 6;
+
+ private static final Bitmap.Config mConfig = Bitmap.Config.ARGB_8888;
+
+ public static void post(Context context, Bitmap source, ImagePreset preset,
+ int type, RenderingRequestCaller caller) {
+ RenderingRequest.post(context, source, preset, type, caller, null, null);
+ }
+
+ public static void post(Context context, Bitmap source, ImagePreset preset, int type,
+ RenderingRequestCaller caller, Rect bounds, Rect destination) {
+ if (((type != PARTIAL_RENDERING && type != HIGHRES_RENDERING) && source == null)
+ || preset == null || caller == null) {
+ Log.v(LOGTAG, "something null: source: " + source
+ + " or preset: " + preset + " or caller: " + caller);
+ return;
+ }
+ RenderingRequest request = new RenderingRequest();
+ Bitmap bitmap = null;
+ if (type == FULL_RENDERING
+ || type == GEOMETRY_RENDERING
+ || type == ICON_RENDERING
+ || type == STYLE_ICON_RENDERING) {
+ CachingPipeline pipeline = new CachingPipeline(
+ FiltersManager.getManager(), "Icon");
+ bitmap = pipeline.renderGeometryIcon(source, preset);
+ } else if (type != PARTIAL_RENDERING && type != HIGHRES_RENDERING) {
+ bitmap = Bitmap.createBitmap(source.getWidth(), source.getHeight(), mConfig);
+ }
+
+ request.setBitmap(bitmap);
+ ImagePreset passedPreset = new ImagePreset(preset);
+ request.setOriginalImagePreset(preset);
+ request.setScaleFactor(MasterImage.getImage().getScaleFactor());
+
+ if (type == PARTIAL_RENDERING) {
+ request.setBounds(bounds);
+ request.setDestination(destination);
+ passedPreset.setPartialRendering(true, bounds);
+ }
+
+ request.setImagePreset(passedPreset);
+ request.setType(type);
+ request.setCaller(caller);
+ request.post(context);
+ }
+
+ public void post(Context context) {
+ if (context instanceof FilterShowActivity) {
+ FilterShowActivity activity = (FilterShowActivity) context;
+ ProcessingService service = activity.getProcessingService();
+ service.postRenderingRequest(this);
+ }
+ }
+
+ public void markAvailable() {
+ if (mBitmap == null || mImagePreset == null
+ || mCaller == null) {
+ return;
+ }
+ mCaller.available(this);
+ }
+
+ public boolean isDirect() {
+ return mIsDirect;
+ }
+
+ public void setDirect(boolean isDirect) {
+ mIsDirect = isDirect;
+ }
+
+ public Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ public void setBitmap(Bitmap bitmap) {
+ mBitmap = bitmap;
+ }
+
+ public ImagePreset getImagePreset() {
+ return mImagePreset;
+ }
+
+ public void setImagePreset(ImagePreset imagePreset) {
+ mImagePreset = imagePreset;
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public void setType(int type) {
+ mType = type;
+ }
+
+ public void setCaller(RenderingRequestCaller caller) {
+ mCaller = caller;
+ }
+
+ public Rect getBounds() {
+ return mBounds;
+ }
+
+ public void setBounds(Rect bounds) {
+ mBounds = bounds;
+ }
+
+ public void setScaleFactor(float scaleFactor) {
+ mScaleFactor = scaleFactor;
+ }
+
+ public float getScaleFactor() {
+ return mScaleFactor;
+ }
+
+ public Rect getDestination() {
+ return mDestination;
+ }
+
+ public void setDestination(Rect destination) {
+ mDestination = destination;
+ }
+
+ public ImagePreset getOriginalImagePreset() {
+ return mOriginalImagePreset;
+ }
+
+ public void setOriginalImagePreset(ImagePreset originalImagePreset) {
+ mOriginalImagePreset = originalImagePreset;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestCaller.java b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestCaller.java
new file mode 100644
index 000000000..b978e7040
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestCaller.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+public interface RenderingRequestCaller {
+ public void available(RenderingRequest request);
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestTask.java b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestTask.java
new file mode 100644
index 000000000..7a83f7072
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/RenderingRequestTask.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+
+public class RenderingRequestTask extends ProcessingTask {
+
+ private CachingPipeline mPreviewPipeline = null;
+ private boolean mPipelineIsOn = false;
+
+ public void setPreviewScaleFactor(float previewScale) {
+ mPreviewPipeline.setPreviewScaleFactor(previewScale);
+ }
+
+ static class Render implements Request {
+ RenderingRequest request;
+ }
+
+ static class RenderResult implements Result {
+ RenderingRequest request;
+ }
+
+ public RenderingRequestTask() {
+ mPreviewPipeline = new CachingPipeline(
+ FiltersManager.getManager(), "Normal");
+ }
+
+ public void setOriginal(Bitmap bitmap) {
+ mPreviewPipeline.setOriginal(bitmap);
+ mPipelineIsOn = true;
+ }
+
+ public void stop() {
+ mPreviewPipeline.stop();
+ }
+
+ public void postRenderingRequest(RenderingRequest request) {
+ if (!mPipelineIsOn) {
+ return;
+ }
+ Render render = new Render();
+ render.request = request;
+ postRequest(render);
+ }
+
+ @Override
+ public Result doInBackground(Request message) {
+ RenderingRequest request = ((Render) message).request;
+ RenderResult result = null;
+ mPreviewPipeline.render(request);
+ result = new RenderResult();
+ result.request = request;
+ return result;
+ }
+
+ @Override
+ public void onResult(Result message) {
+ if (message == null) {
+ return;
+ }
+ RenderingRequest request = ((RenderResult) message).request;
+ request.markAvailable();
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/SharedBuffer.java b/src/com/android/gallery3d/filtershow/pipeline/SharedBuffer.java
new file mode 100644
index 000000000..98e69f60e
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/SharedBuffer.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+
+public class SharedBuffer {
+
+ private static final String LOGTAG = "SharedBuffer";
+
+ private volatile Buffer mProducer = null;
+ private volatile Buffer mConsumer = null;
+ private volatile Buffer mIntermediate = null;
+
+ private volatile boolean mNeedsSwap = false;
+ private volatile boolean mNeedsRepaint = true;
+
+ public void setProducer(Bitmap producer) {
+ Buffer buffer = new Buffer(producer);
+ synchronized (this) {
+ mProducer = buffer;
+ }
+ }
+
+ public synchronized Buffer getProducer() {
+ return mProducer;
+ }
+
+ public synchronized Buffer getConsumer() {
+ return mConsumer;
+ }
+
+ public synchronized void swapProducer() {
+ Buffer intermediate = mIntermediate;
+ mIntermediate = mProducer;
+ mProducer = intermediate;
+ mNeedsSwap = true;
+ }
+
+ public synchronized void swapConsumerIfNeeded() {
+ if (!mNeedsSwap) {
+ return;
+ }
+ Buffer intermediate = mIntermediate;
+ mIntermediate = mConsumer;
+ mConsumer = intermediate;
+ mNeedsSwap = false;
+ }
+
+ public synchronized void invalidate() {
+ mNeedsRepaint = true;
+ }
+
+ public synchronized boolean checkRepaintNeeded() {
+ if (mNeedsRepaint) {
+ mNeedsRepaint = false;
+ return true;
+ }
+ return false;
+ }
+
+}
+
diff --git a/src/com/android/gallery3d/filtershow/pipeline/SharedPreset.java b/src/com/android/gallery3d/filtershow/pipeline/SharedPreset.java
new file mode 100644
index 000000000..3f850fed2
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/SharedPreset.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+public class SharedPreset {
+
+ private volatile ImagePreset mProducerPreset = null;
+ private volatile ImagePreset mConsumerPreset = null;
+ private volatile ImagePreset mIntermediatePreset = null;
+
+ public synchronized void enqueuePreset(ImagePreset preset) {
+ if (mProducerPreset == null || (!mProducerPreset.same(preset))) {
+ mProducerPreset = new ImagePreset(preset);
+ } else {
+ mProducerPreset.updateWith(preset);
+ }
+ ImagePreset temp = mIntermediatePreset;
+ mIntermediatePreset = mProducerPreset;
+ mProducerPreset = temp;
+ }
+
+ public synchronized ImagePreset dequeuePreset() {
+ ImagePreset temp = mConsumerPreset;
+ mConsumerPreset = mIntermediatePreset;
+ mIntermediatePreset = temp;
+ return mConsumerPreset;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/pipeline/UpdatePreviewTask.java b/src/com/android/gallery3d/filtershow/pipeline/UpdatePreviewTask.java
new file mode 100644
index 000000000..406cc9bf5
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/pipeline/UpdatePreviewTask.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2013 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.filtershow.pipeline;
+
+import android.graphics.Bitmap;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class UpdatePreviewTask extends ProcessingTask {
+ private CachingPipeline mPreviewPipeline = null;
+ private boolean mHasUnhandledPreviewRequest = false;
+ private boolean mPipelineIsOn = false;
+
+ public UpdatePreviewTask() {
+ mPreviewPipeline = new CachingPipeline(
+ FiltersManager.getPreviewManager(), "Preview");
+ }
+
+ public void setOriginal(Bitmap bitmap) {
+ mPreviewPipeline.setOriginal(bitmap);
+ mPipelineIsOn = true;
+ }
+
+ public void updatePreview() {
+ if (!mPipelineIsOn) {
+ return;
+ }
+ mHasUnhandledPreviewRequest = true;
+ if (postRequest(null)) {
+ mHasUnhandledPreviewRequest = false;
+ }
+ }
+
+ @Override
+ public boolean isPriorityTask() {
+ return true;
+ }
+
+ @Override
+ public Result doInBackground(Request message) {
+ SharedBuffer buffer = MasterImage.getImage().getPreviewBuffer();
+ SharedPreset preset = MasterImage.getImage().getPreviewPreset();
+ ImagePreset renderingPreset = preset.dequeuePreset();
+ if (renderingPreset != null) {
+ mPreviewPipeline.compute(buffer, renderingPreset, 0);
+ // set the preset we used in the buffer for later inspection UI-side
+ buffer.getProducer().setPreset(renderingPreset);
+ buffer.getProducer().sync();
+ buffer.swapProducer(); // push back the result
+ }
+ return null;
+ }
+
+ @Override
+ public void onResult(Result message) {
+ MasterImage.getImage().notifyObservers();
+ if (mHasUnhandledPreviewRequest) {
+ updatePreview();
+ }
+ }
+
+ public void setPipelineIsOn(boolean pipelineIsOn) {
+ mPipelineIsOn = pipelineIsOn;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/presets/PresetManagementDialog.java b/src/com/android/gallery3d/filtershow/presets/PresetManagementDialog.java
new file mode 100644
index 000000000..7ab61fcc9
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/presets/PresetManagementDialog.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2013 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.filtershow.presets;
+
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+
+public class PresetManagementDialog extends DialogFragment implements View.OnClickListener {
+ private UserPresetsAdapter mAdapter;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.filtershow_presets_management_dialog, container);
+
+ FilterShowActivity activity = (FilterShowActivity) getActivity();
+ mAdapter = activity.getUserPresetsAdapter();
+ ListView panel = (ListView) view.findViewById(R.id.listItems);
+ panel.setAdapter(mAdapter);
+
+ view.findViewById(R.id.cancel).setOnClickListener(this);
+ view.findViewById(R.id.addpreset).setOnClickListener(this);
+ view.findViewById(R.id.ok).setOnClickListener(this);
+ getDialog().setTitle(getString(R.string.filtershow_manage_preset));
+ return view;
+ }
+
+ @Override
+ public void onClick(View v) {
+ FilterShowActivity activity = (FilterShowActivity) getActivity();
+ switch (v.getId()) {
+ case R.id.cancel:
+ mAdapter.clearChangedRepresentations();
+ mAdapter.clearDeletedRepresentations();
+ activity.updateUserPresetsFromAdapter(mAdapter);
+ dismiss();
+ break;
+ case R.id.addpreset:
+ activity.saveCurrentImagePreset();
+ dismiss();
+ break;
+ case R.id.ok:
+ mAdapter.updateCurrent();
+ activity.updateUserPresetsFromAdapter(mAdapter);
+ dismiss();
+ break;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/presets/UserPresetsAdapter.java b/src/com/android/gallery3d/filtershow/presets/UserPresetsAdapter.java
new file mode 100644
index 000000000..dab9ea454
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/presets/UserPresetsAdapter.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2013 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.filtershow.presets;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.category.Action;
+import com.android.gallery3d.filtershow.category.CategoryView;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
+
+import java.util.ArrayList;
+
+public class UserPresetsAdapter extends ArrayAdapter<Action>
+ implements View.OnClickListener, View.OnFocusChangeListener {
+ private static final String LOGTAG = "UserPresetsAdapter";
+ private LayoutInflater mInflater;
+ private int mIconSize = 160;
+ private ArrayList<FilterUserPresetRepresentation> mDeletedRepresentations =
+ new ArrayList<FilterUserPresetRepresentation>();
+ private ArrayList<FilterUserPresetRepresentation> mChangedRepresentations =
+ new ArrayList<FilterUserPresetRepresentation>();
+ private EditText mCurrentEditText;
+
+ public UserPresetsAdapter(Context context, int textViewResourceId) {
+ super(context, textViewResourceId);
+ mInflater = LayoutInflater.from(context);
+ mIconSize = context.getResources().getDimensionPixelSize(R.dimen.category_panel_icon_size);
+ }
+
+ public UserPresetsAdapter(Context context) {
+ this(context, 0);
+ }
+
+ @Override
+ public void add(Action action) {
+ super.add(action);
+ action.setAdapter(this);
+ }
+
+ private void deletePreset(Action action) {
+ FilterRepresentation rep = action.getRepresentation();
+ if (rep instanceof FilterUserPresetRepresentation) {
+ mDeletedRepresentations.add((FilterUserPresetRepresentation) rep);
+ }
+ remove(action);
+ notifyDataSetChanged();
+ }
+
+ private void changePreset(Action action) {
+ FilterRepresentation rep = action.getRepresentation();
+ rep.setName(action.getName());
+ if (rep instanceof FilterUserPresetRepresentation) {
+ mChangedRepresentations.add((FilterUserPresetRepresentation) rep);
+ }
+ }
+
+ public void updateCurrent() {
+ if (mCurrentEditText != null) {
+ updateActionFromEditText(mCurrentEditText);
+ }
+ }
+
+ static class UserPresetViewHolder {
+ ImageView imageView;
+ EditText editText;
+ ImageButton deleteButton;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ UserPresetViewHolder viewHolder;
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.filtershow_presets_management_row, null);
+ viewHolder = new UserPresetViewHolder();
+ viewHolder.imageView = (ImageView) convertView.findViewById(R.id.imageView);
+ viewHolder.editText = (EditText) convertView.findViewById(R.id.editView);
+ viewHolder.deleteButton = (ImageButton) convertView.findViewById(R.id.deleteUserPreset);
+ viewHolder.editText.setOnClickListener(this);
+ viewHolder.editText.setOnFocusChangeListener(this);
+ viewHolder.deleteButton.setOnClickListener(this);
+ convertView.setTag(viewHolder);
+ } else {
+ viewHolder = (UserPresetViewHolder) convertView.getTag();
+ }
+ Action action = getItem(position);
+ viewHolder.imageView.setImageBitmap(action.getImage());
+ if (action.getImage() == null) {
+ // queue image rendering for this action
+ action.setImageFrame(new Rect(0, 0, mIconSize, mIconSize), CategoryView.VERTICAL);
+ }
+ viewHolder.deleteButton.setTag(action);
+ viewHolder.editText.setTag(action);
+ viewHolder.editText.setHint(action.getName());
+
+ return convertView;
+ }
+
+ public ArrayList<FilterUserPresetRepresentation> getDeletedRepresentations() {
+ return mDeletedRepresentations;
+ }
+
+ public void clearDeletedRepresentations() {
+ mDeletedRepresentations.clear();
+ }
+
+ public ArrayList<FilterUserPresetRepresentation> getChangedRepresentations() {
+ return mChangedRepresentations;
+ }
+
+ public void clearChangedRepresentations() {
+ mChangedRepresentations.clear();
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.editView:
+ v.requestFocus();
+ break;
+ case R.id.deleteUserPreset:
+ Action action = (Action) v.getTag();
+ deletePreset(action);
+ break;
+ }
+ }
+
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (v.getId() != R.id.editView) {
+ return;
+ }
+ EditText editText = (EditText) v;
+ if (!hasFocus) {
+ updateActionFromEditText(editText);
+ } else {
+ mCurrentEditText = editText;
+ }
+ }
+
+ private void updateActionFromEditText(EditText editText) {
+ Action action = (Action) editText.getTag();
+ String newName = editText.getText().toString();
+ if (newName.length() > 0) {
+ action.setName(editText.getText().toString());
+ changePreset(action);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/provider/SharedImageProvider.java b/src/com/android/gallery3d/filtershow/provider/SharedImageProvider.java
new file mode 100644
index 000000000..bc17a6e03
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/provider/SharedImageProvider.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012 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.filtershow.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.ConditionVariable;
+import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
+import android.provider.MediaStore;
+import android.provider.OpenableColumns;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+public class SharedImageProvider extends ContentProvider {
+
+ private static final String LOGTAG = "SharedImageProvider";
+
+ public static final String MIME_TYPE = "image/jpeg";
+ public static final String AUTHORITY = "com.android.gallery3d.filtershow.provider.SharedImageProvider";
+ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/image");
+ public static final String PREPARE = "prepare";
+
+ private final String[] mMimeStreamType = {
+ MIME_TYPE
+ };
+
+ private static ConditionVariable mImageReadyCond = new ConditionVariable(false);
+
+ @Override
+ public int delete(Uri arg0, String arg1, String[] arg2) {
+ return 0;
+ }
+
+ @Override
+ public String getType(Uri arg0) {
+ return MIME_TYPE;
+ }
+
+ @Override
+ public String[] getStreamTypes(Uri arg0, String mimeTypeFilter) {
+ return mMimeStreamType;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ if (values.containsKey(PREPARE)) {
+ if (values.getAsBoolean(PREPARE)) {
+ mImageReadyCond.close();
+ } else {
+ mImageReadyCond.open();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
+ return 0;
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ String uriPath = uri.getLastPathSegment();
+ if (uriPath == null) {
+ return null;
+ }
+ if (projection == null) {
+ projection = new String[] {
+ BaseColumns._ID,
+ MediaStore.MediaColumns.DATA,
+ OpenableColumns.DISPLAY_NAME,
+ OpenableColumns.SIZE
+ };
+ }
+ // If we receive a query on display name or size,
+ // we should block until the image is ready
+ mImageReadyCond.block();
+
+ File path = new File(uriPath);
+
+ MatrixCursor cursor = new MatrixCursor(projection);
+ Object[] columns = new Object[projection.length];
+ for (int i = 0; i < projection.length; i++) {
+ if (projection[i].equalsIgnoreCase(BaseColumns._ID)) {
+ columns[i] = 0;
+ } else if (projection[i].equalsIgnoreCase(MediaStore.MediaColumns.DATA)) {
+ columns[i] = uri;
+ } else if (projection[i].equalsIgnoreCase(OpenableColumns.DISPLAY_NAME)) {
+ columns[i] = path.getName();
+ } else if (projection[i].equalsIgnoreCase(OpenableColumns.SIZE)) {
+ columns[i] = path.length();
+ }
+ }
+ cursor.addRow(columns);
+
+ return cursor;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode)
+ throws FileNotFoundException {
+ String uriPath = uri.getLastPathSegment();
+ if (uriPath == null) {
+ return null;
+ }
+ // Here we need to block until the image is ready
+ mImageReadyCond.block();
+ File path = new File(uriPath);
+ int imode = 0;
+ imode |= ParcelFileDescriptor.MODE_READ_ONLY;
+ return ParcelFileDescriptor.open(path, imode);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/state/DragListener.java b/src/com/android/gallery3d/filtershow/state/DragListener.java
new file mode 100644
index 000000000..1aa81ed69
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/DragListener.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2013 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.filtershow.state;
+
+import android.view.DragEvent;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+
+class DragListener implements View.OnDragListener {
+
+ private static final String LOGTAG = "DragListener";
+ private PanelTrack mStatePanelTrack;
+ private static float sSlope = 0.2f;
+
+ public DragListener(PanelTrack statePanelTrack) {
+ mStatePanelTrack = statePanelTrack;
+ }
+
+ private void setState(DragEvent event) {
+ float translation = event.getY() - mStatePanelTrack.getTouchPoint().y;
+ float alpha = 1.0f - (Math.abs(translation)
+ / mStatePanelTrack.getCurrentView().getHeight());
+ if (mStatePanelTrack.getOrientation() == LinearLayout.VERTICAL) {
+ translation = event.getX() - mStatePanelTrack.getTouchPoint().x;
+ alpha = 1.0f - (Math.abs(translation)
+ / mStatePanelTrack.getCurrentView().getWidth());
+ mStatePanelTrack.getCurrentView().setTranslationX(translation);
+ } else {
+ mStatePanelTrack.getCurrentView().setTranslationY(translation);
+ }
+ mStatePanelTrack.getCurrentView().setBackgroundAlpha(alpha);
+ }
+
+ @Override
+ public boolean onDrag(View v, DragEvent event) {
+ switch (event.getAction()) {
+ case DragEvent.ACTION_DRAG_STARTED: {
+ break;
+ }
+ case DragEvent.ACTION_DRAG_LOCATION: {
+ if (mStatePanelTrack.getCurrentView() != null) {
+ setState(event);
+ View over = mStatePanelTrack.findChildAt((int) event.getX(),
+ (int) event.getY());
+ if (over != null && over != mStatePanelTrack.getCurrentView()) {
+ StateView stateView = (StateView) over;
+ if (stateView != mStatePanelTrack.getCurrentView()) {
+ int pos = mStatePanelTrack.findChild(over);
+ int origin = mStatePanelTrack.findChild(
+ mStatePanelTrack.getCurrentView());
+ ArrayAdapter array = (ArrayAdapter) mStatePanelTrack.getAdapter();
+ if (origin != -1 && pos != -1) {
+ State current = (State) array.getItem(origin);
+ array.remove(current);
+ array.insert(current, pos);
+ mStatePanelTrack.fillContent(false);
+ mStatePanelTrack.setCurrentView(mStatePanelTrack.getChildAt(pos));
+ }
+ }
+ }
+ }
+ break;
+ }
+ case DragEvent.ACTION_DRAG_ENTERED: {
+ mStatePanelTrack.setExited(false);
+ if (mStatePanelTrack.getCurrentView() != null) {
+ mStatePanelTrack.getCurrentView().setVisibility(View.VISIBLE);
+ }
+ return true;
+ }
+ case DragEvent.ACTION_DRAG_EXITED: {
+ if (mStatePanelTrack.getCurrentView() != null) {
+ setState(event);
+ mStatePanelTrack.getCurrentView().setVisibility(View.INVISIBLE);
+ }
+ mStatePanelTrack.setExited(true);
+ break;
+ }
+ case DragEvent.ACTION_DROP: {
+ break;
+ }
+ case DragEvent.ACTION_DRAG_ENDED: {
+ if (mStatePanelTrack.getCurrentView() != null
+ && mStatePanelTrack.getCurrentView().getAlpha() > sSlope) {
+ setState(event);
+ }
+ mStatePanelTrack.checkEndState();
+ break;
+ }
+ default:
+ break;
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/state/PanelTrack.java b/src/com/android/gallery3d/filtershow/state/PanelTrack.java
new file mode 100644
index 000000000..d02207d9b
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/PanelTrack.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2013 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.filtershow.state;
+
+import android.graphics.Point;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.Adapter;
+
+public interface PanelTrack {
+ public int getOrientation();
+ public void onTouch(MotionEvent event, StateView view);
+ public StateView getCurrentView();
+ public void setCurrentView(View view);
+ public Point getTouchPoint();
+ public View findChildAt(int x, int y);
+ public int findChild(View view);
+ public Adapter getAdapter();
+ public void fillContent(boolean value);
+ public View getChildAt(int pos);
+ public void setExited(boolean value);
+ public void checkEndState();
+}
diff --git a/src/com/android/gallery3d/filtershow/state/State.java b/src/com/android/gallery3d/filtershow/state/State.java
new file mode 100644
index 000000000..e7dedd6a2
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/State.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2013 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.filtershow.state;
+
+import com.android.gallery3d.filtershow.filters.FilterFxRepresentation;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+
+public class State {
+ private String mText;
+ private int mType;
+ private FilterRepresentation mFilterRepresentation;
+
+ public State(State state) {
+ this(state.getText(), state.getType());
+ }
+
+ public State(String text) {
+ this(text, StateView.DEFAULT);
+ }
+
+ public State(String text, int type) {
+ mText = text;
+ mType = type;
+ }
+
+ public boolean equals(State state) {
+ if (mFilterRepresentation.getFilterClass()
+ != state.mFilterRepresentation.getFilterClass()) {
+ return false;
+ }
+ if (mFilterRepresentation instanceof FilterFxRepresentation) {
+ return mFilterRepresentation.equals(state.getFilterRepresentation());
+ }
+ return true;
+ }
+
+ public boolean isDraggable() {
+ return mFilterRepresentation != null;
+ }
+
+ String getText() {
+ return mText;
+ }
+
+ void setText(String text) {
+ mText = text;
+ }
+
+ int getType() {
+ return mType;
+ }
+
+ void setType(int type) {
+ mType = type;
+ }
+
+ public FilterRepresentation getFilterRepresentation() {
+ return mFilterRepresentation;
+ }
+
+ public void setFilterRepresentation(FilterRepresentation filterRepresentation) {
+ mFilterRepresentation = filterRepresentation;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/state/StateAdapter.java b/src/com/android/gallery3d/filtershow/state/StateAdapter.java
new file mode 100644
index 000000000..522585280
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/StateAdapter.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2013 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.filtershow.state;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+import java.util.Vector;
+
+public class StateAdapter extends ArrayAdapter<State> {
+
+ private static final String LOGTAG = "StateAdapter";
+ private int mOrientation;
+ private String mOriginalText;
+ private String mResultText;
+
+ public StateAdapter(Context context, int textViewResourceId) {
+ super(context, textViewResourceId);
+ mOriginalText = context.getString(R.string.state_panel_original);
+ mResultText = context.getString(R.string.state_panel_result);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ StateView view = null;
+ if (convertView == null) {
+ convertView = new StateView(getContext());
+ }
+ view = (StateView) convertView;
+ State state = getItem(position);
+ view.setState(state);
+ view.setOrientation(mOrientation);
+ FilterRepresentation currentRep = MasterImage.getImage().getCurrentFilterRepresentation();
+ FilterRepresentation stateRep = state.getFilterRepresentation();
+ if (currentRep != null && stateRep != null
+ && currentRep.getFilterClass() == stateRep.getFilterClass()
+ && currentRep.getEditorId() != ImageOnlyEditor.ID) {
+ view.setSelected(true);
+ } else {
+ view.setSelected(false);
+ }
+ return view;
+ }
+
+ public boolean contains(State state) {
+ for (int i = 0; i < getCount(); i++) {
+ if (state == getItem(i)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void setOrientation(int orientation) {
+ mOrientation = orientation;
+ }
+
+ public void addOriginal() {
+ add(new State(mOriginalText));
+ }
+
+ public boolean same(Vector<State> states) {
+ // we have the original state in addition
+ if (states.size() + 1 != getCount()) {
+ return false;
+ }
+ for (int i = 1; i < getCount(); i++) {
+ State state = getItem(i);
+ if (!state.equals(states.elementAt(i-1))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public void fill(Vector<State> states) {
+ if (same(states)) {
+ return;
+ }
+ clear();
+ addOriginal();
+ addAll(states);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void remove(State state) {
+ super.remove(state);
+ FilterRepresentation filterRepresentation = state.getFilterRepresentation();
+ FilterShowActivity activity = (FilterShowActivity) getContext();
+ activity.removeFilterRepresentation(filterRepresentation);
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/state/StatePanel.java b/src/com/android/gallery3d/filtershow/state/StatePanel.java
new file mode 100644
index 000000000..df470f23e
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/StatePanel.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2013 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.filtershow.state;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class StatePanel extends Fragment {
+ private static final String LOGTAG = "StatePanel";
+ private StatePanelTrack track;
+ private LinearLayout mMainView;
+ public static final String FRAGMENT_TAG = "StatePanel";
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mMainView = (LinearLayout) inflater.inflate(R.layout.filtershow_state_panel_new, null);
+ View panel = mMainView.findViewById(R.id.listStates);
+ track = (StatePanelTrack) panel;
+ track.setAdapter(MasterImage.getImage().getState());
+ return mMainView;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/state/StatePanelTrack.java b/src/com/android/gallery3d/filtershow/state/StatePanelTrack.java
new file mode 100644
index 000000000..fff7e7f5f
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/StatePanelTrack.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2013 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.filtershow.state;
+
+import android.animation.LayoutTransition;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.LinearLayout;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.editors.ImageOnlyEditor;
+import com.android.gallery3d.filtershow.filters.FilterRepresentation;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class StatePanelTrack extends LinearLayout implements PanelTrack {
+
+ private static final String LOGTAG = "StatePanelTrack";
+ private Point mTouchPoint;
+ private StateView mCurrentView;
+ private StateView mCurrentSelectedView;
+ private boolean mExited = false;
+ private boolean mStartedDrag = false;
+ private StateAdapter mAdapter;
+ private DragListener mDragListener = new DragListener(this);
+ private float mDeleteSlope = 0.2f;
+ private GestureDetector mGestureDetector;
+ private int mElemWidth;
+ private int mElemHeight;
+ private int mElemSize;
+ private int mElemEndSize;
+ private int mEndElemWidth;
+ private int mEndElemHeight;
+ private long mTouchTime;
+ private int mMaxTouchDelay = 300; // 300ms delay for touch
+ private static final boolean ALLOWS_DRAG = false;
+ private DataSetObserver mObserver = new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ fillContent(false);
+ }
+
+ @Override
+ public void onInvalidated() {
+ super.onInvalidated();
+ fillContent(false);
+ }
+ };
+
+ public StatePanelTrack(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.StatePanelTrack);
+ mElemSize = a.getDimensionPixelSize(R.styleable.StatePanelTrack_elemSize, 0);
+ mElemEndSize = a.getDimensionPixelSize(R.styleable.StatePanelTrack_elemEndSize, 0);
+ if (getOrientation() == LinearLayout.HORIZONTAL) {
+ mElemWidth = mElemSize;
+ mElemHeight = LayoutParams.MATCH_PARENT;
+ mEndElemWidth = mElemEndSize;
+ mEndElemHeight = LayoutParams.MATCH_PARENT;
+ } else {
+ mElemWidth = LayoutParams.MATCH_PARENT;
+ mElemHeight = mElemSize;
+ mEndElemWidth = LayoutParams.MATCH_PARENT;
+ mEndElemHeight = mElemEndSize;
+ }
+ GestureDetector.SimpleOnGestureListener simpleOnGestureListener
+ = new GestureDetector.SimpleOnGestureListener(){
+ @Override
+ public void onLongPress(MotionEvent e) {
+ longPress(e);
+ }
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ addDuplicate(e);
+ return true;
+ }
+ };
+ mGestureDetector = new GestureDetector(context, simpleOnGestureListener);
+ }
+
+ private void addDuplicate(MotionEvent e) {
+ if (mCurrentSelectedView == null) {
+ return;
+ }
+ int pos = findChild(mCurrentSelectedView);
+ if (pos != -1) {
+ mAdapter.insert(new State(mCurrentSelectedView.getState()), pos);
+ fillContent(true);
+ }
+ }
+
+ private void longPress(MotionEvent e) {
+ View view = findChildAt((int) e.getX(), (int) e.getY());
+ if (view == null) {
+ return;
+ }
+ if (view instanceof StateView) {
+ StateView stateView = (StateView) view;
+ stateView.setDuplicateButton(true);
+ }
+ }
+
+ public void setAdapter(StateAdapter adapter) {
+ mAdapter = adapter;
+ mAdapter.registerDataSetObserver(mObserver);
+ mAdapter.setOrientation(getOrientation());
+ fillContent(false);
+ requestLayout();
+ }
+
+ public StateView findChildWithState(State state) {
+ for (int i = 0; i < getChildCount(); i++) {
+ StateView view = (StateView) getChildAt(i);
+ if (view.getState() == state) {
+ return view;
+ }
+ }
+ return null;
+ }
+
+ public void fillContent(boolean animate) {
+ if (!animate) {
+ this.setLayoutTransition(null);
+ }
+ int n = mAdapter.getCount();
+ for (int i = 0; i < getChildCount(); i++) {
+ StateView child = (StateView) getChildAt(i);
+ child.resetPosition();
+ if (!mAdapter.contains(child.getState())) {
+ removeView(child);
+ }
+ }
+ LayoutParams params = new LayoutParams(mElemWidth, mElemHeight);
+ for (int i = 0; i < n; i++) {
+ State s = mAdapter.getItem(i);
+ if (findChildWithState(s) == null) {
+ View view = mAdapter.getView(i, null, this);
+ addView(view, i, params);
+ }
+ }
+
+ for (int i = 0; i < n; i++) {
+ State state = mAdapter.getItem(i);
+ StateView view = (StateView) getChildAt(i);
+ view.setState(state);
+ if (i == 0) {
+ view.setType(StateView.BEGIN);
+ } else if (i == n - 1) {
+ view.setType(StateView.END);
+ } else {
+ view.setType(StateView.DEFAULT);
+ }
+ view.resetPosition();
+ }
+
+ if (!animate) {
+ this.setLayoutTransition(new LayoutTransition());
+ }
+ }
+
+ public void onTouch(MotionEvent event, StateView view) {
+ if (!view.isDraggable()) {
+ return;
+ }
+ mCurrentView = view;
+ if (mCurrentSelectedView == mCurrentView) {
+ return;
+ }
+ if (mCurrentSelectedView != null) {
+ mCurrentSelectedView.setSelected(false);
+ }
+ // We changed the current view -- let's reset the
+ // gesture detector.
+ MotionEvent cancelEvent = MotionEvent.obtain(event);
+ cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
+ mGestureDetector.onTouchEvent(cancelEvent);
+ mCurrentSelectedView = mCurrentView;
+ // We have to send the event to the gesture detector
+ mGestureDetector.onTouchEvent(event);
+ mTouchTime = System.currentTimeMillis();
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (mCurrentView != null) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (mCurrentView == null) {
+ return false;
+ }
+ if (mTouchTime == 0) {
+ mTouchTime = System.currentTimeMillis();
+ }
+ mGestureDetector.onTouchEvent(event);
+ if (mTouchPoint == null) {
+ mTouchPoint = new Point();
+ mTouchPoint.x = (int) event.getX();
+ mTouchPoint.y = (int) event.getY();
+ }
+
+ if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
+ float translation = event.getY() - mTouchPoint.y;
+ float alpha = 1.0f - (Math.abs(translation) / mCurrentView.getHeight());
+ if (getOrientation() == LinearLayout.VERTICAL) {
+ translation = event.getX() - mTouchPoint.x;
+ alpha = 1.0f - (Math.abs(translation) / mCurrentView.getWidth());
+ mCurrentView.setTranslationX(translation);
+ } else {
+ mCurrentView.setTranslationY(translation);
+ }
+ mCurrentView.setBackgroundAlpha(alpha);
+ if (ALLOWS_DRAG && alpha < 0.7) {
+ setOnDragListener(mDragListener);
+ DragShadowBuilder shadowBuilder = new DragShadowBuilder(mCurrentView);
+ mCurrentView.startDrag(null, shadowBuilder, mCurrentView, 0);
+ mStartedDrag = true;
+ }
+ }
+ if (!mExited && mCurrentView != null
+ && mCurrentView.getBackgroundAlpha() > mDeleteSlope
+ && event.getActionMasked() == MotionEvent.ACTION_UP
+ && System.currentTimeMillis() - mTouchTime < mMaxTouchDelay) {
+ FilterRepresentation representation = mCurrentView.getState().getFilterRepresentation();
+ mCurrentView.setSelected(true);
+ if (representation != MasterImage.getImage().getCurrentFilterRepresentation()) {
+ FilterShowActivity activity = (FilterShowActivity) getContext();
+ activity.showRepresentation(representation);
+ mCurrentView.setSelected(false);
+ }
+ }
+ if (event.getActionMasked() == MotionEvent.ACTION_UP
+ || (!mStartedDrag && event.getActionMasked() == MotionEvent.ACTION_CANCEL)) {
+ checkEndState();
+ if (mCurrentView != null) {
+ FilterRepresentation representation = mCurrentView.getState().getFilterRepresentation();
+ if (representation.getEditorId() == ImageOnlyEditor.ID) {
+ mCurrentView.setSelected(false);
+ }
+ }
+ }
+ return true;
+ }
+
+ public void checkEndState() {
+ mTouchPoint = null;
+ mTouchTime = 0;
+ if (mExited || mCurrentView.getBackgroundAlpha() < mDeleteSlope) {
+ int origin = findChild(mCurrentView);
+ if (origin != -1) {
+ State current = mAdapter.getItem(origin);
+ FilterRepresentation currentRep = MasterImage.getImage().getCurrentFilterRepresentation();
+ FilterRepresentation removedRep = current.getFilterRepresentation();
+ mAdapter.remove(current);
+ fillContent(true);
+ if (currentRep != null && removedRep != null
+ && currentRep.getFilterClass() == removedRep.getFilterClass()) {
+ FilterShowActivity activity = (FilterShowActivity) getContext();
+ activity.backToMain();
+ return;
+ }
+ }
+ } else {
+ mCurrentView.setBackgroundAlpha(1.0f);
+ mCurrentView.setTranslationX(0);
+ mCurrentView.setTranslationY(0);
+ }
+ if (mCurrentSelectedView != null) {
+ mCurrentSelectedView.invalidate();
+ }
+ if (mCurrentView != null) {
+ mCurrentView.invalidate();
+ }
+ mCurrentView = null;
+ mExited = false;
+ mStartedDrag = false;
+ }
+
+ public View findChildAt(int x, int y) {
+ Rect frame = new Rect();
+ int scrolledXInt = getScrollX() + x;
+ int scrolledYInt = getScrollY() + y;
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ child.getHitRect(frame);
+ if (frame.contains(scrolledXInt, scrolledYInt)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ public int findChild(View view) {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (child == view) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public StateView getCurrentView() {
+ return mCurrentView;
+ }
+
+ public void setCurrentView(View currentView) {
+ mCurrentView = (StateView) currentView;
+ }
+
+ public void setExited(boolean value) {
+ mExited = value;
+ }
+
+ public Point getTouchPoint() {
+ return mTouchPoint;
+ }
+
+ public Adapter getAdapter() {
+ return mAdapter;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/state/StateView.java b/src/com/android/gallery3d/filtershow/state/StateView.java
new file mode 100644
index 000000000..73d57846a
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/state/StateView.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2013 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.filtershow.state;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.*;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewParent;
+import android.widget.LinearLayout;
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+
+public class StateView extends View {
+
+ private static final String LOGTAG = "StateView";
+ private Path mPath = new Path();
+ private Paint mPaint = new Paint();
+
+ public static int DEFAULT = 0;
+ public static int BEGIN = 1;
+ public static int END = 2;
+
+ public static int UP = 1;
+ public static int DOWN = 2;
+ public static int LEFT = 3;
+ public static int RIGHT = 4;
+
+ private int mType = DEFAULT;
+ private float mAlpha = 1.0f;
+ private String mText = "Default";
+ private float mTextSize = 32;
+ private static int sMargin = 16;
+ private static int sArrowHeight = 16;
+ private static int sArrowWidth = 8;
+ private int mOrientation = LinearLayout.VERTICAL;
+ private int mDirection = DOWN;
+ private boolean mDuplicateButton;
+ private State mState;
+
+ private int mEndsBackgroundColor;
+ private int mEndsTextColor;
+ private int mBackgroundColor;
+ private int mTextColor;
+ private int mSelectedBackgroundColor;
+ private int mSelectedTextColor;
+ private Rect mTextBounds = new Rect();
+
+ public StateView(Context context) {
+ this(context, DEFAULT);
+ }
+
+ public StateView(Context context, int type) {
+ super(context);
+ mType = type;
+ Resources res = getResources();
+ mEndsBackgroundColor = res.getColor(R.color.filtershow_stateview_end_background);
+ mEndsTextColor = res.getColor(R.color.filtershow_stateview_end_text);
+ mBackgroundColor = res.getColor(R.color.filtershow_stateview_background);
+ mTextColor = res.getColor(R.color.filtershow_stateview_text);
+ mSelectedBackgroundColor = res.getColor(R.color.filtershow_stateview_selected_background);
+ mSelectedTextColor = res.getColor(R.color.filtershow_stateview_selected_text);
+ mTextSize = res.getDimensionPixelSize(R.dimen.state_panel_text_size);
+ }
+
+ public String getText() {
+ return mText;
+ }
+
+ public void setText(String text) {
+ mText = text;
+ invalidate();
+ }
+
+ public void setType(int type) {
+ mType = type;
+ invalidate();
+ }
+
+ @Override
+ public void setSelected(boolean value) {
+ super.setSelected(value);
+ if (!value) {
+ mDuplicateButton = false;
+ }
+ invalidate();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ ViewParent parent = getParent();
+ if (parent instanceof PanelTrack) {
+ ((PanelTrack) getParent()).onTouch(event, this);
+ }
+ if (mType == BEGIN) {
+ MasterImage.getImage().setShowsOriginal(true);
+ }
+ }
+ if (event.getActionMasked() == MotionEvent.ACTION_UP
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
+ MasterImage.getImage().setShowsOriginal(false);
+ }
+ return true;
+ }
+
+ public void drawText(Canvas canvas) {
+ if (mText == null) {
+ return;
+ }
+ mPaint.reset();
+ if (isSelected()) {
+ mPaint.setColor(mSelectedTextColor);
+ } else {
+ mPaint.setColor(mTextColor);
+ }
+ if (mType == BEGIN) {
+ mPaint.setColor(mEndsTextColor);
+ }
+ mPaint.setTypeface(Typeface.DEFAULT_BOLD);
+ mPaint.setAntiAlias(true);
+ mPaint.setTextSize(mTextSize);
+ mPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
+ int x = (canvas.getWidth() - mTextBounds.width()) / 2;
+ int y = mTextBounds.height() + (canvas.getHeight() - mTextBounds.height()) / 2;
+ canvas.drawText(mText, x, y, mPaint);
+ }
+
+ public void onDraw(Canvas canvas) {
+ canvas.drawARGB(0, 0, 0, 0);
+ mPaint.reset();
+ mPath.reset();
+
+ float w = canvas.getWidth();
+ float h = canvas.getHeight();
+ float r = sArrowHeight;
+ float d = sArrowWidth;
+
+ if (mOrientation == LinearLayout.HORIZONTAL) {
+ drawHorizontalPath(w, h, r, d);
+ } else {
+ if (mDirection == DOWN) {
+ drawVerticalDownPath(w, h, r, d);
+ } else {
+ drawVerticalPath(w, h, r, d);
+ }
+ }
+
+ if (mType == DEFAULT || mType == END) {
+ if (mDuplicateButton) {
+ mPaint.setARGB(255, 200, 0, 0);
+ } else if (isSelected()) {
+ mPaint.setColor(mSelectedBackgroundColor);
+ } else {
+ mPaint.setColor(mBackgroundColor);
+ }
+ } else {
+ mPaint.setColor(mEndsBackgroundColor);
+ }
+ canvas.drawPath(mPath, mPaint);
+ drawText(canvas);
+ }
+
+ private void drawHorizontalPath(float w, float h, float r, float d) {
+ mPath.moveTo(0, 0);
+ if (mType == END) {
+ mPath.lineTo(w, 0);
+ mPath.lineTo(w, h);
+ } else {
+ mPath.lineTo(w - d, 0);
+ mPath.lineTo(w - d, r);
+ mPath.lineTo(w, r + d);
+ mPath.lineTo(w - d, r + d + r);
+ mPath.lineTo(w - d, h);
+ }
+ mPath.lineTo(0, h);
+ if (mType != BEGIN) {
+ mPath.lineTo(0, r + d + r);
+ mPath.lineTo(d, r + d);
+ mPath.lineTo(0, r);
+ }
+ mPath.close();
+ }
+
+ private void drawVerticalPath(float w, float h, float r, float d) {
+ if (mType == BEGIN) {
+ mPath.moveTo(0, 0);
+ mPath.lineTo(w, 0);
+ } else {
+ mPath.moveTo(0, d);
+ mPath.lineTo(r, d);
+ mPath.lineTo(r + d, 0);
+ mPath.lineTo(r + d + r, d);
+ mPath.lineTo(w, d);
+ }
+ mPath.lineTo(w, h);
+ if (mType != END) {
+ mPath.lineTo(r + d + r, h);
+ mPath.lineTo(r + d, h - d);
+ mPath.lineTo(r, h);
+ }
+ mPath.lineTo(0, h);
+ mPath.close();
+ }
+
+ private void drawVerticalDownPath(float w, float h, float r, float d) {
+ mPath.moveTo(0, 0);
+ if (mType != BEGIN) {
+ mPath.lineTo(r, 0);
+ mPath.lineTo(r + d, d);
+ mPath.lineTo(r + d + r, 0);
+ }
+ mPath.lineTo(w, 0);
+
+ if (mType != END) {
+ mPath.lineTo(w, h - d);
+
+ mPath.lineTo(r + d + r, h - d);
+ mPath.lineTo(r + d, h);
+ mPath.lineTo(r, h - d);
+
+ mPath.lineTo(0, h - d);
+ } else {
+ mPath.lineTo(w, h);
+ mPath.lineTo(0, h);
+ }
+
+ mPath.close();
+ }
+
+ public void setBackgroundAlpha(float alpha) {
+ if (mType == BEGIN) {
+ return;
+ }
+ mAlpha = alpha;
+ setAlpha(alpha);
+ invalidate();
+ }
+
+ public float getBackgroundAlpha() {
+ return mAlpha;
+ }
+
+ public void setOrientation(int orientation) {
+ mOrientation = orientation;
+ }
+
+ public void setDuplicateButton(boolean b) {
+ mDuplicateButton = b;
+ invalidate();
+ }
+
+ public State getState() {
+ return mState;
+ }
+
+ public void setState(State state) {
+ mState = state;
+ mText = mState.getText().toUpperCase();
+ mType = mState.getType();
+ invalidate();
+ }
+
+ public void resetPosition() {
+ setTranslationX(0);
+ setTranslationY(0);
+ setBackgroundAlpha(1.0f);
+ }
+
+ public boolean isDraggable() {
+ return mState.isDraggable();
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/tools/IconFactory.java b/src/com/android/gallery3d/filtershow/tools/IconFactory.java
new file mode 100644
index 000000000..9e39f27fc
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/tools/IconFactory.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2013 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.filtershow.tools;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+/**
+ * A factory class for producing bitmaps to use as UI icons.
+ */
+public class IconFactory {
+
+ /**
+ * Builds an icon with the dimensions iconWidth:iconHeight. If scale is set
+ * the source image is stretched to fit within the given dimensions;
+ * otherwise, the source image is cropped to the proper aspect ratio.
+ *
+ * @param sourceImage image to create an icon from.
+ * @param iconWidth width of the icon bitmap.
+ * @param iconHeight height of the icon bitmap.
+ * @param scale if true, stretch sourceImage to fit the icon dimensions.
+ * @return an icon bitmap with the dimensions iconWidth:iconHeight.
+ */
+ public static Bitmap createIcon(Bitmap sourceImage, int iconWidth, int iconHeight,
+ boolean scale) {
+ if (sourceImage == null) {
+ throw new IllegalArgumentException("Null argument to buildIcon");
+ }
+
+ int sourceWidth = sourceImage.getWidth();
+ int sourceHeight = sourceImage.getHeight();
+
+ if (sourceWidth == 0 || sourceHeight == 0 || iconWidth == 0 || iconHeight == 0) {
+ throw new IllegalArgumentException("Bitmap with dimension 0 used as input");
+ }
+
+ Bitmap icon = Bitmap.createBitmap(iconWidth, iconHeight,
+ Bitmap.Config.ARGB_8888);
+ drawIcon(icon, sourceImage, scale);
+ return icon;
+ }
+
+ /**
+ * Draws an icon in the destination bitmap. If scale is set the source image
+ * is stretched to fit within the destination dimensions; otherwise, the
+ * source image is cropped to the proper aspect ratio.
+ *
+ * @param dest bitmap into which to draw the icon.
+ * @param sourceImage image to create an icon from.
+ * @param scale if true, stretch sourceImage to fit the destination.
+ */
+ public static void drawIcon(Bitmap dest, Bitmap sourceImage, boolean scale) {
+ if (dest == null || sourceImage == null) {
+ throw new IllegalArgumentException("Null argument to buildIcon");
+ }
+
+ int sourceWidth = sourceImage.getWidth();
+ int sourceHeight = sourceImage.getHeight();
+ int iconWidth = dest.getWidth();
+ int iconHeight = dest.getHeight();
+
+ if (sourceWidth == 0 || sourceHeight == 0 || iconWidth == 0 || iconHeight == 0) {
+ throw new IllegalArgumentException("Bitmap with dimension 0 used as input");
+ }
+
+ Rect destRect = new Rect(0, 0, iconWidth, iconHeight);
+ Canvas canvas = new Canvas(dest);
+
+ Rect srcRect = null;
+ if (scale) {
+ // scale image to fit in icon (stretches if aspect isn't the same)
+ srcRect = new Rect(0, 0, sourceWidth, sourceHeight);
+ } else {
+ // crop image to aspect ratio iconWidth:iconHeight
+ float wScale = sourceWidth / (float) iconWidth;
+ float hScale = sourceHeight / (float) iconHeight;
+ float s = Math.min(hScale, wScale);
+
+ float iw = iconWidth * s;
+ float ih = iconHeight * s;
+
+ float borderW = (sourceWidth - iw) / 2.0f;
+ float borderH = (sourceHeight - ih) / 2.0f;
+ RectF rec = new RectF(borderW, borderH, borderW + iw, borderH + ih);
+ srcRect = new Rect();
+ rec.roundOut(srcRect);
+ }
+
+ canvas.drawBitmap(sourceImage, srcRect, destRect, new Paint(Paint.FILTER_BITMAP_FLAG));
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/tools/MatrixFit.java b/src/com/android/gallery3d/filtershow/tools/MatrixFit.java
new file mode 100644
index 000000000..3b815673c
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/tools/MatrixFit.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2013 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.filtershow.tools;
+
+import android.util.Log;
+
+public class MatrixFit {
+ // Simple implementation of a matrix fit in N dimensions.
+
+ private static final String LOGTAG = "MatrixFit";
+
+ private double[][] mMatrix;
+ private int mDimension;
+ private boolean mValid = false;
+ private static double sEPS = 1.0f/10000000000.0f;
+
+ public MatrixFit(double[][] from, double[][] to) {
+ mValid = fit(from, to);
+ }
+
+ public int getDimension() {
+ return mDimension;
+ }
+
+ public boolean isValid() {
+ return mValid;
+ }
+
+ public double[][] getMatrix() {
+ return mMatrix;
+ }
+
+ public boolean fit(double[][] from, double[][] to) {
+ if ((from.length != to.length) || (from.length < 1)) {
+ Log.e(LOGTAG, "from and to must be of same size");
+ return false;
+ }
+
+ mDimension = from[0].length;
+ mMatrix = new double[mDimension +1][mDimension + mDimension +1];
+
+ if (from.length < mDimension) {
+ Log.e(LOGTAG, "Too few points => under-determined system");
+ return false;
+ }
+
+ double[][] q = new double[from.length][mDimension];
+ for (int i = 0; i < from.length; i++) {
+ for (int j = 0; j < mDimension; j++) {
+ q[i][j] = from[i][j];
+ }
+ }
+
+ double[][] p = new double[to.length][mDimension];
+ for (int i = 0; i < to.length; i++) {
+ for (int j = 0; j < mDimension; j++) {
+ p[i][j] = to[i][j];
+ }
+ }
+
+ // Make an empty (dim) x (dim + 1) matrix and fill it
+ double[][] c = new double[mDimension+1][mDimension];
+ for (int j = 0; j < mDimension; j++) {
+ for (int k = 0; k < mDimension + 1; k++) {
+ for (int i = 0; i < q.length; i++) {
+ double qt = 1;
+ if (k < mDimension) {
+ qt = q[i][k];
+ }
+ c[k][j] += qt * p[i][j];
+ }
+ }
+ }
+
+ // Make an empty (dim+1) x (dim+1) matrix and fill it
+ double[][] Q = new double[mDimension+1][mDimension+1];
+ for (int qi = 0; qi < q.length; qi++) {
+ double[] qt = new double[mDimension + 1];
+ for (int i = 0; i < mDimension; i++) {
+ qt[i] = q[qi][i];
+ }
+ qt[mDimension] = 1;
+ for (int i = 0; i < mDimension + 1; i++) {
+ for (int j = 0; j < mDimension + 1; j++) {
+ Q[i][j] += qt[i] * qt[j];
+ }
+ }
+ }
+
+ // Use a gaussian elimination to solve the linear system
+ for (int i = 0; i < mDimension + 1; i++) {
+ for (int j = 0; j < mDimension + 1; j++) {
+ mMatrix[i][j] = Q[i][j];
+ }
+ for (int j = 0; j < mDimension; j++) {
+ mMatrix[i][mDimension + 1 + j] = c[i][j];
+ }
+ }
+ if (!gaussianElimination(mMatrix)) {
+ return false;
+ }
+ return true;
+ }
+
+ public double[] apply(double[] point) {
+ if (mDimension != point.length) {
+ return null;
+ }
+ double[] res = new double[mDimension];
+ for (int j = 0; j < mDimension; j++) {
+ for (int i = 0; i < mDimension; i++) {
+ res[j] += point[i] * mMatrix[i][j+ mDimension +1];
+ }
+ res[j] += mMatrix[mDimension][j+ mDimension +1];
+ }
+ return res;
+ }
+
+ public void printEquation() {
+ for (int j = 0; j < mDimension; j++) {
+ String str = "x" + j + "' = ";
+ for (int i = 0; i < mDimension; i++) {
+ str += "x" + i + " * " + mMatrix[i][j+mDimension+1] + " + ";
+ }
+ str += mMatrix[mDimension][j+mDimension+1];
+ Log.v(LOGTAG, str);
+ }
+ }
+
+ private void printMatrix(String name, double[][] matrix) {
+ Log.v(LOGTAG, "name: " + name);
+ for (int i = 0; i < matrix.length; i++) {
+ String str = "";
+ for (int j = 0; j < matrix[0].length; j++) {
+ str += "" + matrix[i][j] + " ";
+ }
+ Log.v(LOGTAG, str);
+ }
+ }
+
+ /*
+ * Transforms the given matrix into a row echelon matrix
+ */
+ private boolean gaussianElimination(double[][] m) {
+ int h = m.length;
+ int w = m[0].length;
+
+ for (int y = 0; y < h; y++) {
+ int maxrow = y;
+ for (int y2 = y + 1; y2 < h; y2++) { // Find max pivot
+ if (Math.abs(m[y2][y]) > Math.abs(m[maxrow][y])) {
+ maxrow = y2;
+ }
+ }
+ // swap
+ for (int i = 0; i < mDimension; i++) {
+ double t = m[y][i];
+ m[y][i] = m[maxrow][i];
+ m[maxrow][i] = t;
+ }
+
+ if (Math.abs(m[y][y]) <= sEPS) { // Singular Matrix
+ return false;
+ }
+ for (int y2 = y + 1; y2 < h; y2++) { // Eliminate column y
+ double c = m[y2][y] / m[y][y];
+ for (int x = y; x < w; x++) {
+ m[y2][x] -= m[y][x] * c;
+ }
+ }
+ }
+ for (int y = h -1; y > -1; y--) { // Back substitution
+ double c = m[y][y];
+ for (int y2 = 0; y2 < y; y2++) {
+ for (int x = w - 1; x > y - 1; x--) {
+ m[y2][x] -= m[y][x] * m[y2][y] / c;
+ }
+ }
+ m[y][y] /= c;
+ for (int x = h; x < w; x++) { // Normalize row y
+ m[y][x] /= c;
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/tools/SaveImage.java b/src/com/android/gallery3d/filtershow/tools/SaveImage.java
new file mode 100644
index 000000000..83cbd0136
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/tools/SaveImage.java
@@ -0,0 +1,632 @@
+/*
+ * 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.filtershow.tools;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.filters.FiltersManager;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.CachingPipeline;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.filtershow.pipeline.ProcessingService;
+import com.android.gallery3d.util.UsageStatistics;
+import com.android.gallery3d.util.XmpUtilHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.sql.Date;
+import java.text.SimpleDateFormat;
+import java.util.TimeZone;
+
+/**
+ * Handles saving edited photo
+ */
+public class SaveImage {
+ private static final String LOGTAG = "SaveImage";
+
+ /**
+ * Callback for updates
+ */
+ public interface Callback {
+ void onProgress(int max, int current);
+ }
+
+ public interface ContentResolverQueryCallback {
+ void onCursorResult(Cursor cursor);
+ }
+
+ private static final String TIME_STAMP_NAME = "_yyyyMMdd_HHmmss";
+ private static final String PREFIX_PANO = "PANO";
+ private static final String PREFIX_IMG = "IMG";
+ private static final String POSTFIX_JPG = ".jpg";
+ private static final String AUX_DIR_NAME = ".aux";
+
+ private final Context mContext;
+ private final Uri mSourceUri;
+ private final Callback mCallback;
+ private final File mDestinationFile;
+ private final Uri mSelectedImageUri;
+
+ private int mCurrentProcessingStep = 1;
+
+ public static final int MAX_PROCESSING_STEPS = 6;
+ public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
+
+ // In order to support the new edit-save behavior such that user won't see
+ // the edited image together with the original image, we are adding a new
+ // auxiliary directory for the edited image. Basically, the original image
+ // will be hidden in that directory after edit and user will see the edited
+ // image only.
+ // Note that deletion on the edited image will also cause the deletion of
+ // the original image under auxiliary directory.
+ //
+ // There are several situations we need to consider:
+ // 1. User edit local image local01.jpg. A local02.jpg will be created in the
+ // same directory, and original image will be moved to auxiliary directory as
+ // ./.aux/local02.jpg.
+ // If user edit the local02.jpg, local03.jpg will be created in the local
+ // directory and ./.aux/local02.jpg will be renamed to ./.aux/local03.jpg
+ //
+ // 2. User edit remote image remote01.jpg from picassa or other server.
+ // remoteSavedLocal01.jpg will be saved under proper local directory.
+ // In remoteSavedLocal01.jpg, there will be a reference pointing to the
+ // remote01.jpg. There will be no local copy of remote01.jpg.
+ // If user edit remoteSavedLocal01.jpg, then a new remoteSavedLocal02.jpg
+ // will be generated and still pointing to the remote01.jpg
+ //
+ // 3. User delete any local image local.jpg.
+ // Since the filenames are kept consistent in auxiliary directory, every
+ // time a local.jpg get deleted, the files in auxiliary directory whose
+ // names starting with "local." will be deleted.
+ // This pattern will facilitate the multiple images deletion in the auxiliary
+ // directory.
+
+ /**
+ * @param context
+ * @param sourceUri The Uri for the original image, which can be the hidden
+ * image under the auxiliary directory or the same as selectedImageUri.
+ * @param selectedImageUri The Uri for the image selected by the user.
+ * In most cases, it is a content Uri for local image or remote image.
+ * @param destination Destinaton File, if this is null, a new file will be
+ * created under the same directory as selectedImageUri.
+ * @param callback Let the caller know the saving has completed.
+ * @return the newSourceUri
+ */
+ public SaveImage(Context context, Uri sourceUri, Uri selectedImageUri,
+ File destination, Callback callback) {
+ mContext = context;
+ mSourceUri = sourceUri;
+ mCallback = callback;
+ if (destination == null) {
+ mDestinationFile = getNewFile(context, selectedImageUri);
+ } else {
+ mDestinationFile = destination;
+ }
+
+ mSelectedImageUri = selectedImageUri;
+ }
+
+ public static File getFinalSaveDirectory(Context context, Uri sourceUri) {
+ File saveDirectory = SaveImage.getSaveDirectory(context, sourceUri);
+ if ((saveDirectory == null) || !saveDirectory.canWrite()) {
+ saveDirectory = new File(Environment.getExternalStorageDirectory(),
+ SaveImage.DEFAULT_SAVE_DIRECTORY);
+ }
+ // Create the directory if it doesn't exist
+ if (!saveDirectory.exists())
+ saveDirectory.mkdirs();
+ return saveDirectory;
+ }
+
+ public static File getNewFile(Context context, Uri sourceUri) {
+ File saveDirectory = getFinalSaveDirectory(context, sourceUri);
+ String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(
+ System.currentTimeMillis()));
+ if (hasPanoPrefix(context, sourceUri)) {
+ return new File(saveDirectory, PREFIX_PANO + filename + POSTFIX_JPG);
+ }
+ return new File(saveDirectory, PREFIX_IMG + filename + POSTFIX_JPG);
+ }
+
+ /**
+ * Remove the files in the auxiliary directory whose names are the same as
+ * the source image.
+ * @param contentResolver The application's contentResolver
+ * @param srcContentUri The content Uri for the source image.
+ */
+ public static void deleteAuxFiles(ContentResolver contentResolver,
+ Uri srcContentUri) {
+ final String[] fullPath = new String[1];
+ String[] queryProjection = new String[] { ImageColumns.DATA };
+ querySourceFromContentResolver(contentResolver,
+ srcContentUri, queryProjection,
+ new ContentResolverQueryCallback() {
+ @Override
+ public void onCursorResult(Cursor cursor) {
+ fullPath[0] = cursor.getString(0);
+ }
+ }
+ );
+ if (fullPath[0] != null) {
+ // Construct the auxiliary directory given the source file's path.
+ // Then select and delete all the files starting with the same name
+ // under the auxiliary directory.
+ File currentFile = new File(fullPath[0]);
+
+ String filename = currentFile.getName();
+ int firstDotPos = filename.indexOf(".");
+ final String filenameNoExt = (firstDotPos == -1) ? filename :
+ filename.substring(0, firstDotPos);
+ File auxDir = getLocalAuxDirectory(currentFile);
+ if (auxDir.exists()) {
+ FilenameFilter filter = new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ if (name.startsWith(filenameNoExt + ".")) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+
+ // Delete all auxiliary files whose name is matching the
+ // current local image.
+ File[] auxFiles = auxDir.listFiles(filter);
+ for (File file : auxFiles) {
+ file.delete();
+ }
+ }
+ }
+ }
+
+ public Object getPanoramaXMPData(Uri source, ImagePreset preset) {
+ Object xmp = null;
+ if (preset.isPanoramaSafe()) {
+ InputStream is = null;
+ try {
+ is = mContext.getContentResolver().openInputStream(source);
+ xmp = XmpUtilHelper.extractXMPMeta(is);
+ } catch (FileNotFoundException e) {
+ Log.w(LOGTAG, "Failed to get XMP data from image: ", e);
+ } finally {
+ Utils.closeSilently(is);
+ }
+ }
+ return xmp;
+ }
+
+ public boolean putPanoramaXMPData(File file, Object xmp) {
+ if (xmp != null) {
+ return XmpUtilHelper.writeXMPMeta(file.getAbsolutePath(), xmp);
+ }
+ return false;
+ }
+
+ public ExifInterface getExifData(Uri source) {
+ ExifInterface exif = new ExifInterface();
+ String mimeType = mContext.getContentResolver().getType(mSelectedImageUri);
+ if (mimeType == null) {
+ mimeType = ImageLoader.getMimeType(mSelectedImageUri);
+ }
+ if (mimeType.equals(ImageLoader.JPEG_MIME_TYPE)) {
+ InputStream inStream = null;
+ try {
+ inStream = mContext.getContentResolver().openInputStream(source);
+ exif.readExif(inStream);
+ } catch (FileNotFoundException e) {
+ Log.w(LOGTAG, "Cannot find file: " + source, e);
+ } catch (IOException e) {
+ Log.w(LOGTAG, "Cannot read exif for: " + source, e);
+ } finally {
+ Utils.closeSilently(inStream);
+ }
+ }
+ return exif;
+ }
+
+ public boolean putExifData(File file, ExifInterface exif, Bitmap image,
+ int jpegCompressQuality) {
+ boolean ret = false;
+ OutputStream s = null;
+ try {
+ s = exif.getExifWriterStream(file.getAbsolutePath());
+ image.compress(Bitmap.CompressFormat.JPEG,
+ (jpegCompressQuality > 0) ? jpegCompressQuality : 1, s);
+ s.flush();
+ s.close();
+ s = null;
+ ret = true;
+ } catch (FileNotFoundException e) {
+ Log.w(LOGTAG, "File not found: " + file.getAbsolutePath(), e);
+ } catch (IOException e) {
+ Log.w(LOGTAG, "Could not write exif: ", e);
+ } finally {
+ Utils.closeSilently(s);
+ }
+ return ret;
+ }
+
+ private Uri resetToOriginalImageIfNeeded(ImagePreset preset, boolean doAuxBackup) {
+ Uri uri = null;
+ if (!preset.hasModifications()) {
+ // This can happen only when preset has no modification but save
+ // button is enabled, it means the file is loaded with filters in
+ // the XMP, then all the filters are removed or restore to default.
+ // In this case, when mSourceUri exists, rename it to the
+ // destination file.
+ File srcFile = getLocalFileFromUri(mContext, mSourceUri);
+ // If the source is not a local file, then skip this renaming and
+ // create a local copy as usual.
+ if (srcFile != null) {
+ srcFile.renameTo(mDestinationFile);
+ uri = SaveImage.linkNewFileToUri(mContext, mSelectedImageUri,
+ mDestinationFile, System.currentTimeMillis(), doAuxBackup);
+ }
+ }
+ return uri;
+ }
+
+ private void resetProgress() {
+ mCurrentProcessingStep = 0;
+ }
+
+ private void updateProgress() {
+ if (mCallback != null) {
+ mCallback.onProgress(MAX_PROCESSING_STEPS, ++mCurrentProcessingStep);
+ }
+ }
+
+ public Uri processAndSaveImage(ImagePreset preset, boolean doAuxBackup, int quality) {
+
+ Uri uri = resetToOriginalImageIfNeeded(preset, doAuxBackup);
+ if (uri != null) {
+ return null;
+ }
+
+ resetProgress();
+
+ boolean noBitmap = true;
+ int num_tries = 0;
+ int sampleSize = 1;
+
+ // If necessary, move the source file into the auxiliary directory,
+ // newSourceUri is then pointing to the new location.
+ // If no file is moved, newSourceUri will be the same as mSourceUri.
+ Uri newSourceUri = mSourceUri;
+ if (doAuxBackup) {
+ newSourceUri = moveSrcToAuxIfNeeded(mSourceUri, mDestinationFile);
+ }
+
+ // Stopgap fix for low-memory devices.
+ while (noBitmap) {
+ try {
+ updateProgress();
+ // Try to do bitmap operations, downsample if low-memory
+ Bitmap bitmap = ImageLoader.loadOrientedBitmapWithBackouts(mContext, newSourceUri,
+ sampleSize);
+ if (bitmap == null) {
+ return null;
+ }
+ updateProgress();
+ CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(),
+ "Saving");
+
+ bitmap = pipeline.renderFinalImage(bitmap, preset);
+ updateProgress();
+
+ Object xmp = getPanoramaXMPData(newSourceUri, preset);
+ ExifInterface exif = getExifData(newSourceUri);
+
+ updateProgress();
+ // Set tags
+ long time = System.currentTimeMillis();
+ exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, time,
+ TimeZone.getDefault());
+ exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION,
+ ExifInterface.Orientation.TOP_LEFT));
+ // Remove old thumbnail
+ exif.removeCompressedThumbnail();
+
+ updateProgress();
+
+ // If we succeed in writing the bitmap as a jpeg, return a uri.
+ if (putExifData(mDestinationFile, exif, bitmap, quality)) {
+ putPanoramaXMPData(mDestinationFile, xmp);
+ // mDestinationFile will save the newSourceUri info in the XMP.
+ XmpPresets.writeFilterXMP(mContext, newSourceUri,
+ mDestinationFile, preset);
+
+ // After this call, mSelectedImageUri will be actually
+ // pointing at the new file mDestinationFile.
+ uri = SaveImage.linkNewFileToUri(mContext, mSelectedImageUri,
+ mDestinationFile, time, doAuxBackup);
+ }
+ updateProgress();
+
+ noBitmap = false;
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR,
+ "SaveComplete", null);
+ } catch (OutOfMemoryError e) {
+ // Try 5 times before failing for good.
+ if (++num_tries >= 5) {
+ throw e;
+ }
+ System.gc();
+ sampleSize *= 2;
+ resetProgress();
+ }
+ }
+ return uri;
+ }
+
+ /**
+ * Move the source file to auxiliary directory if needed and return the Uri
+ * pointing to this new source file.
+ * @param srcUri Uri to the source image.
+ * @param dstFile Providing the destination file info to help to build the
+ * auxiliary directory and new source file's name.
+ * @return the newSourceUri pointing to the new source image.
+ */
+ private Uri moveSrcToAuxIfNeeded(Uri srcUri, File dstFile) {
+ File srcFile = getLocalFileFromUri(mContext, srcUri);
+ if (srcFile == null) {
+ Log.d(LOGTAG, "Source file is not a local file, no update.");
+ return srcUri;
+ }
+
+ // Get the destination directory and create the auxilliary directory
+ // if necessary.
+ File auxDiretory = getLocalAuxDirectory(dstFile);
+ if (!auxDiretory.exists()) {
+ auxDiretory.mkdirs();
+ }
+
+ // Make sure there is a .nomedia file in the auxiliary directory, such
+ // that MediaScanner will not report those files under this directory.
+ File noMedia = new File(auxDiretory, ".nomedia");
+ if (!noMedia.exists()) {
+ try {
+ noMedia.createNewFile();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Can't create the nomedia");
+ return srcUri;
+ }
+ }
+ // We are using the destination file name such that photos sitting in
+ // the auxiliary directory are matching the parent directory.
+ File newSrcFile = new File(auxDiretory, dstFile.getName());
+
+ if (!newSrcFile.exists()) {
+ srcFile.renameTo(newSrcFile);
+ }
+
+ return Uri.fromFile(newSrcFile);
+
+ }
+
+ private static File getLocalAuxDirectory(File dstFile) {
+ File dstDirectory = dstFile.getParentFile();
+ File auxDiretory = new File(dstDirectory + "/" + AUX_DIR_NAME);
+ return auxDiretory;
+ }
+
+ public static Uri makeAndInsertUri(Context context, Uri sourceUri) {
+ long time = System.currentTimeMillis();
+ String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(time));
+ File saveDirectory = getFinalSaveDirectory(context, sourceUri);
+ File file = new File(saveDirectory, filename + ".JPG");
+ return linkNewFileToUri(context, sourceUri, file, time, false);
+ }
+
+ public static void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
+ File destination) {
+ Uri selectedImageUri = filterShowActivity.getSelectedImageUri();
+ Uri sourceImageUri = MasterImage.getImage().getUri();
+
+ Intent processIntent = ProcessingService.getSaveIntent(filterShowActivity, preset,
+ destination, selectedImageUri, sourceImageUri, false, 90);
+
+ filterShowActivity.startService(processIntent);
+
+ if (!filterShowActivity.isSimpleEditAction()) {
+ // terminate for now
+ filterShowActivity.completeSaveImage(selectedImageUri);
+ }
+ }
+
+ public static void querySource(Context context, Uri sourceUri, String[] projection,
+ ContentResolverQueryCallback callback) {
+ ContentResolver contentResolver = context.getContentResolver();
+ querySourceFromContentResolver(contentResolver, sourceUri, projection, callback);
+ }
+
+ private static void querySourceFromContentResolver(
+ ContentResolver contentResolver, Uri sourceUri, String[] projection,
+ ContentResolverQueryCallback callback) {
+ Cursor cursor = null;
+ try {
+ cursor = contentResolver.query(sourceUri, projection, null, null,
+ null);
+ if ((cursor != null) && cursor.moveToNext()) {
+ callback.onCursorResult(cursor);
+ }
+ } catch (Exception e) {
+ // Ignore error for lacking the data column from the source.
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ private static File getSaveDirectory(Context context, Uri sourceUri) {
+ File file = getLocalFileFromUri(context, sourceUri);
+ if (file != null) {
+ return file.getParentFile();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Construct a File object based on the srcUri.
+ * @return The file object. Return null if srcUri is invalid or not a local
+ * file.
+ */
+ private static File getLocalFileFromUri(Context context, Uri srcUri) {
+ if (srcUri == null) {
+ Log.e(LOGTAG, "srcUri is null.");
+ return null;
+ }
+
+ String scheme = srcUri.getScheme();
+ if (scheme == null) {
+ Log.e(LOGTAG, "scheme is null.");
+ return null;
+ }
+
+ final File[] file = new File[1];
+ // sourceUri can be a file path or a content Uri, it need to be handled
+ // differently.
+ if (scheme.equals(ContentResolver.SCHEME_CONTENT)) {
+ if (srcUri.getAuthority().equals(MediaStore.AUTHORITY)) {
+ querySource(context, srcUri, new String[] {
+ ImageColumns.DATA
+ },
+ new ContentResolverQueryCallback() {
+
+ @Override
+ public void onCursorResult(Cursor cursor) {
+ file[0] = new File(cursor.getString(0));
+ }
+ });
+ }
+ } else if (scheme.equals(ContentResolver.SCHEME_FILE)) {
+ file[0] = new File(srcUri.getPath());
+ }
+ return file[0];
+ }
+
+ /**
+ * Gets the actual filename for a Uri from Gallery's ContentProvider.
+ */
+ private static String getTrueFilename(Context context, Uri src) {
+ if (context == null || src == null) {
+ return null;
+ }
+ final String[] trueName = new String[1];
+ querySource(context, src, new String[] {
+ ImageColumns.DATA
+ }, new ContentResolverQueryCallback() {
+ @Override
+ public void onCursorResult(Cursor cursor) {
+ trueName[0] = new File(cursor.getString(0)).getName();
+ }
+ });
+ return trueName[0];
+ }
+
+ /**
+ * Checks whether the true filename has the panorama image prefix.
+ */
+ private static boolean hasPanoPrefix(Context context, Uri src) {
+ String name = getTrueFilename(context, src);
+ return name != null && name.startsWith(PREFIX_PANO);
+ }
+
+ /**
+ * If the <code>sourceUri</code> is a local content Uri, update the
+ * <code>sourceUri</code> to point to the <code>file</code>.
+ * At the same time, the old file <code>sourceUri</code> used to point to
+ * will be removed if it is local.
+ * If the <code>sourceUri</code> is not a local content Uri, then the
+ * <code>file</code> will be inserted as a new content Uri.
+ * @return the final Uri referring to the <code>file</code>.
+ */
+ public static Uri linkNewFileToUri(Context context, Uri sourceUri,
+ File file, long time, boolean deleteOriginal) {
+ File oldSelectedFile = getLocalFileFromUri(context, sourceUri);
+ final ContentValues values = new ContentValues();
+
+ time /= 1000;
+ values.put(Images.Media.TITLE, file.getName());
+ values.put(Images.Media.DISPLAY_NAME, file.getName());
+ values.put(Images.Media.MIME_TYPE, "image/jpeg");
+ values.put(Images.Media.DATE_TAKEN, time);
+ values.put(Images.Media.DATE_MODIFIED, time);
+ values.put(Images.Media.DATE_ADDED, time);
+ values.put(Images.Media.ORIENTATION, 0);
+ values.put(Images.Media.DATA, file.getAbsolutePath());
+ values.put(Images.Media.SIZE, file.length());
+
+ final String[] projection = new String[] {
+ ImageColumns.DATE_TAKEN,
+ ImageColumns.LATITUDE, ImageColumns.LONGITUDE,
+ };
+ SaveImage.querySource(context, sourceUri, projection,
+ new SaveImage.ContentResolverQueryCallback() {
+
+ @Override
+ public void onCursorResult(Cursor cursor) {
+ values.put(Images.Media.DATE_TAKEN, cursor.getLong(0));
+
+ double latitude = cursor.getDouble(1);
+ double longitude = cursor.getDouble(2);
+ // TODO: Change || to && after the default location
+ // issue is fixed.
+ if ((latitude != 0f) || (longitude != 0f)) {
+ values.put(Images.Media.LATITUDE, latitude);
+ values.put(Images.Media.LONGITUDE, longitude);
+ }
+ }
+ });
+
+ Uri result = sourceUri;
+ if (oldSelectedFile == null || !deleteOriginal) {
+ result = context.getContentResolver().insert(
+ Images.Media.EXTERNAL_CONTENT_URI, values);
+ } else {
+ context.getContentResolver().update(sourceUri, values, null, null);
+ if (oldSelectedFile.exists()) {
+ oldSelectedFile.delete();
+ }
+ }
+
+ return result;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/tools/XmpPresets.java b/src/com/android/gallery3d/filtershow/tools/XmpPresets.java
new file mode 100644
index 000000000..3995eeb85
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/tools/XmpPresets.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2013 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.filtershow.tools;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.XMPMeta;
+import com.adobe.xmp.XMPMetaFactory;
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.ImagePreset;
+import com.android.gallery3d.util.XmpUtilHelper;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+public class XmpPresets {
+ public static final String
+ XMP_GOOGLE_FILTER_NAMESPACE = "http://ns.google.com/photos/1.0/filter/";
+ public static final String XMP_GOOGLE_FILTER_PREFIX = "AFltr";
+ public static final String XMP_SRC_FILE_URI = "SourceFileUri";
+ public static final String XMP_FILTERSTACK = "filterstack";
+ private static final String LOGTAG = "XmpPresets";
+
+ public static class XMresults {
+ public String presetString;
+ public ImagePreset preset;
+ public Uri originalimage;
+ }
+
+ static {
+ try {
+ XMPMetaFactory.getSchemaRegistry().registerNamespace(
+ XMP_GOOGLE_FILTER_NAMESPACE, XMP_GOOGLE_FILTER_PREFIX);
+ } catch (XMPException e) {
+ Log.e(LOGTAG, "Register XMP name space failed", e);
+ }
+ }
+
+ public static void writeFilterXMP(
+ Context context, Uri srcUri, File dstFile, ImagePreset preset) {
+ InputStream is = null;
+ XMPMeta xmpMeta = null;
+ try {
+ is = context.getContentResolver().openInputStream(srcUri);
+ xmpMeta = XmpUtilHelper.extractXMPMeta(is);
+ } catch (FileNotFoundException e) {
+
+ } finally {
+ Utils.closeSilently(is);
+ }
+
+ if (xmpMeta == null) {
+ xmpMeta = XMPMetaFactory.create();
+ }
+ try {
+ xmpMeta.setProperty(XMP_GOOGLE_FILTER_NAMESPACE,
+ XMP_SRC_FILE_URI, srcUri.toString());
+ xmpMeta.setProperty(XMP_GOOGLE_FILTER_NAMESPACE,
+ XMP_FILTERSTACK, preset.getJsonString(context.getString(R.string.saved)));
+ } catch (XMPException e) {
+ Log.v(LOGTAG, "Write XMP meta to file failed:" + dstFile.getAbsolutePath());
+ return;
+ }
+
+ if (!XmpUtilHelper.writeXMPMeta(dstFile.getAbsolutePath(), xmpMeta)) {
+ Log.v(LOGTAG, "Write XMP meta to file failed:" + dstFile.getAbsolutePath());
+ }
+ }
+
+ public static XMresults extractXMPData(
+ Context context, MasterImage mMasterImage, Uri uriToEdit) {
+ XMresults ret = new XMresults();
+
+ InputStream is = null;
+ XMPMeta xmpMeta = null;
+ try {
+ is = context.getContentResolver().openInputStream(uriToEdit);
+ xmpMeta = XmpUtilHelper.extractXMPMeta(is);
+ } catch (FileNotFoundException e) {
+ } finally {
+ Utils.closeSilently(is);
+ }
+
+ if (xmpMeta == null) {
+ return null;
+ }
+
+ try {
+ String strSrcUri = xmpMeta.getPropertyString(XMP_GOOGLE_FILTER_NAMESPACE,
+ XMP_SRC_FILE_URI);
+
+ if (strSrcUri != null) {
+ String filterString = xmpMeta.getPropertyString(XMP_GOOGLE_FILTER_NAMESPACE,
+ XMP_FILTERSTACK);
+
+ Uri srcUri = Uri.parse(strSrcUri);
+ ret.originalimage = srcUri;
+
+ ret.preset = new ImagePreset(mMasterImage.getPreset());
+ ret.presetString = filterString;
+ boolean ok = ret.preset.readJsonFromString(filterString);
+ if (!ok) {
+ return null;
+ }
+ return ret;
+ }
+ } catch (XMPException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/ui/ExportDialog.java b/src/com/android/gallery3d/filtershow/ui/ExportDialog.java
new file mode 100644
index 000000000..4b30e7b18
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/ui/ExportDialog.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013 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.filtershow.ui;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.imageshow.MasterImage;
+import com.android.gallery3d.filtershow.pipeline.ProcessingService;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.File;
+
+public class ExportDialog extends DialogFragment implements View.OnClickListener, SeekBar.OnSeekBarChangeListener{
+ SeekBar mSeekBar;
+ TextView mSeekVal;
+ String mSliderLabel;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.filtershow_export_dialog, container);
+ mSeekBar = (SeekBar) view.findViewById(R.id.qualitySeekBar);
+ mSeekVal = (TextView) view.findViewById(R.id.qualityTextView);
+ mSliderLabel = getString(R.string.quality) + ": ";
+ mSeekVal.setText(mSliderLabel + mSeekBar.getProgress());
+ mSeekBar.setOnSeekBarChangeListener(this);
+ view.findViewById(R.id.cancel).setOnClickListener(this);
+ view.findViewById(R.id.done).setOnClickListener(this);
+ getDialog().setTitle(R.string.export_flattened);
+ return view;
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar arg0) {
+ // Do nothing
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar arg0) {
+ // Do nothing
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar arg0, int arg1, boolean arg2) {
+ mSeekVal.setText(mSliderLabel + arg1);
+ }
+
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.cancel:
+ dismiss();
+ break;
+ case R.id.done:
+ FilterShowActivity activity = (FilterShowActivity) getActivity();
+ Uri sourceUri = MasterImage.getImage().getUri();
+ File dest = SaveImage.getNewFile(activity, sourceUri);
+ Intent processIntent = ProcessingService.getSaveIntent(activity, MasterImage
+ .getImage().getPreset(), dest, activity.getSelectedImageUri(), sourceUri,
+ true, mSeekBar.getProgress());
+ activity.startService(processIntent);
+ dismiss();
+ break;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/ui/FramedTextButton.java b/src/com/android/gallery3d/filtershow/ui/FramedTextButton.java
new file mode 100644
index 000000000..c1e4109d2
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/ui/FramedTextButton.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2012 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.filtershow.ui;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.widget.ImageButton;
+
+import com.android.gallery3d.R;
+
+public class FramedTextButton extends ImageButton {
+ private static final String LOGTAG = "FramedTextButton";
+ private String mText = null;
+ private static int mTextSize = 24;
+ private static int mTextPadding = 20;
+ private static Paint gPaint = new Paint();
+ private static Path gPath = new Path();
+ private static int mTrianglePadding = 2;
+ private static int mTriangleSize = 30;
+
+ public static void setTextSize(int value) {
+ mTextSize = value;
+ }
+
+ public static void setTextPadding(int value) {
+ mTextPadding = value;
+ }
+
+ public static void setTrianglePadding(int value) {
+ mTrianglePadding = value;
+ }
+
+ public static void setTriangleSize(int value) {
+ mTriangleSize = value;
+ }
+
+ public void setText(String text) {
+ mText = text;
+ invalidate();
+ }
+
+ public void setTextFrom(int itemId) {
+ switch (itemId) {
+ case R.id.curve_menu_rgb: {
+ setText(getContext().getString(R.string.curves_channel_rgb));
+ break;
+ }
+ case R.id.curve_menu_red: {
+ setText(getContext().getString(R.string.curves_channel_red));
+ break;
+ }
+ case R.id.curve_menu_green: {
+ setText(getContext().getString(R.string.curves_channel_green));
+ break;
+ }
+ case R.id.curve_menu_blue: {
+ setText(getContext().getString(R.string.curves_channel_blue));
+ break;
+ }
+ }
+ invalidate();
+ }
+
+ public FramedTextButton(Context context) {
+ this(context, null);
+ }
+
+ public FramedTextButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ if (attrs == null) {
+ return;
+ }
+ TypedArray a = getContext().obtainStyledAttributes(
+ attrs, R.styleable.ImageButtonTitle);
+
+ mText = a.getString(R.styleable.ImageButtonTitle_android_text);
+ }
+
+ public String getText(){
+ return mText;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ gPaint.setARGB(96, 255, 255, 255);
+ gPaint.setStrokeWidth(2);
+ gPaint.setStyle(Paint.Style.STROKE);
+ int w = getWidth();
+ int h = getHeight();
+ canvas.drawRect(mTextPadding, mTextPadding, w - mTextPadding,
+ h - mTextPadding, gPaint);
+ gPath.reset();
+ gPath.moveTo(w - mTextPadding - mTrianglePadding - mTriangleSize,
+ h - mTextPadding - mTrianglePadding);
+ gPath.lineTo(w - mTextPadding - mTrianglePadding,
+ h - mTextPadding - mTrianglePadding - mTriangleSize);
+ gPath.lineTo(w - mTextPadding - mTrianglePadding,
+ h - mTextPadding - mTrianglePadding);
+ gPath.close();
+ gPaint.setARGB(128, 255, 255, 255);
+ gPaint.setStrokeWidth(1);
+ gPaint.setStyle(Paint.Style.FILL_AND_STROKE);
+ canvas.drawPath(gPath, gPaint);
+ if (mText != null) {
+ gPaint.reset();
+ gPaint.setARGB(255, 255, 255, 255);
+ gPaint.setTextSize(mTextSize);
+ float textWidth = gPaint.measureText(mText);
+ Rect bounds = new Rect();
+ gPaint.getTextBounds(mText, 0, mText.length(), bounds);
+ int x = (int) ((w - textWidth) / 2);
+ int y = (h + bounds.height()) / 2;
+
+ canvas.drawText(mText, x, y, gPaint);
+ }
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java b/src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java
new file mode 100644
index 000000000..ef40c5e44
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/ui/SelectionRenderer.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 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.filtershow.ui;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+
+public class SelectionRenderer {
+
+ public static void drawSelection(Canvas canvas, int left, int top, int right, int bottom,
+ int stroke, Paint paint) {
+ canvas.drawRect(left, top, right, top + stroke, paint);
+ canvas.drawRect(left, bottom - stroke, right, bottom, paint);
+ canvas.drawRect(left, top, left + stroke, bottom, paint);
+ canvas.drawRect(right - stroke, top, right, bottom, paint);
+ }
+
+ public static void drawSelection(Canvas canvas, int left, int top, int right, int bottom,
+ int stroke, Paint selectPaint, int border, Paint borderPaint) {
+ canvas.drawRect(left, top, right, top + stroke, selectPaint);
+ canvas.drawRect(left, bottom - stroke, right, bottom, selectPaint);
+ canvas.drawRect(left, top, left + stroke, bottom, selectPaint);
+ canvas.drawRect(right - stroke, top, right, bottom, selectPaint);
+ canvas.drawRect(left + stroke, top + stroke, right - stroke,
+ top + stroke + border, borderPaint);
+ canvas.drawRect(left + stroke, bottom - stroke - border, right - stroke,
+ bottom - stroke, borderPaint);
+ canvas.drawRect(left + stroke, top + stroke, left + stroke + border,
+ bottom - stroke, borderPaint);
+ canvas.drawRect(right - stroke - border, top + stroke, right - stroke,
+ bottom - stroke, borderPaint);
+ }
+
+}
diff --git a/src/com/android/gallery3d/gadget/LocalPhotoSource.java b/src/com/android/gallery3d/gadget/LocalPhotoSource.java
new file mode 100644
index 000000000..4e94e8d75
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/LocalPhotoSource.java
@@ -0,0 +1,205 @@
+/*
+ * 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.gadget;
+
+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 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Random;
+
+public class LocalPhotoSource implements WidgetSource {
+
+ @SuppressWarnings("unused")
+ 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<Long> mPhotos = new ArrayList<Long>();
+ 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);
+ }
+
+ @Override
+ 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<Integer> selected = new HashSet<Integer>(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();
+ }
+ }
+
+ @Override
+ 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/gadget/MediaSetSource.java b/src/com/android/gallery3d/gadget/MediaSetSource.java
new file mode 100644
index 000000000..458651c98
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/MediaSetSource.java
@@ -0,0 +1,233 @@
+/*
+ * 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.gadget;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Binder;
+
+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 java.util.ArrayList;
+import java.util.Arrays;
+
+public class MediaSetSource implements WidgetSource, ContentListener {
+ private static final String TAG = "MediaSetSource";
+
+ private DataManager mDataManager;
+ private Path mAlbumPath;
+
+ private WidgetSource mSource;
+
+ private MediaSet mRootSet;
+ private ContentListener mListener;
+
+ public MediaSetSource(DataManager manager, String albumPath) {
+ MediaSet mediaSet = (MediaSet) manager.getMediaObject(albumPath);
+ if (mediaSet != null) {
+ mSource = new CheckedMediaSetSource(mediaSet);
+ return;
+ }
+
+ // Initialize source to an empty source until the album path can be resolved
+ mDataManager = Utils.checkNotNull(manager);
+ mAlbumPath = Path.fromString(albumPath);
+ mSource = new EmptySource();
+ monitorRootPath();
+ }
+
+ @Override
+ public int size() {
+ return mSource.size();
+ }
+
+ @Override
+ public Bitmap getImage(int index) {
+ return mSource.getImage(index);
+ }
+
+ @Override
+ public Uri getContentUri(int index) {
+ return mSource.getContentUri(index);
+ }
+
+ @Override
+ public synchronized void setContentListener(ContentListener listener) {
+ if (mRootSet != null) {
+ mListener = listener;
+ } else {
+ mSource.setContentListener(listener);
+ }
+ }
+
+ @Override
+ public void reload() {
+ mSource.reload();
+ }
+
+ @Override
+ public void close() {
+ mSource.close();
+ }
+
+ @Override
+ public void onContentDirty() {
+ resolveAlbumPath();
+ }
+
+ private void monitorRootPath() {
+ String rootPath = mDataManager.getTopSetPath(DataManager.INCLUDE_ALL);
+ mRootSet = (MediaSet) mDataManager.getMediaObject(rootPath);
+ mRootSet.addContentListener(this);
+ }
+
+ private synchronized void resolveAlbumPath() {
+ if (mDataManager == null) return;
+ MediaSet mediaSet = (MediaSet) mDataManager.getMediaObject(mAlbumPath);
+ if (mediaSet != null) {
+ // Clear the reference instead of removing the listener
+ // to get around a concurrent modification exception.
+ mRootSet = null;
+
+ mSource = new CheckedMediaSetSource(mediaSet);
+ if (mListener != null) {
+ mListener.onContentDirty();
+ mSource.setContentListener(mListener);
+ mListener = null;
+ }
+ mDataManager = null;
+ mAlbumPath = null;
+ }
+ }
+
+ private static class CheckedMediaSetSource implements WidgetSource, ContentListener {
+ private static final int CACHE_SIZE = 32;
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "CheckedMediaSetSource";
+
+ 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 CheckedMediaSetSource(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<MediaItem> 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();
+ }
+ }
+
+ 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() {}
+ }
+}
diff --git a/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java b/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.java
new file mode 100644
index 000000000..58466bf01
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/PhotoAppWidgetProvider.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.gadget;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.gadget.WidgetDatabaseHelper.Entry;
+import com.android.gallery3d.onetimeinitializer.GalleryWidgetMigrator;
+
+public class PhotoAppWidgetProvider 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) {
+
+ if (ApiHelper.HAS_REMOTE_VIEWS_SERVICE) {
+ // migrate gallery widgets from pre-JB releases to JB due to bucket ID change
+ GalleryWidgetMigrator.migrateGalleryWidgets(context);
+ }
+
+ 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);
+ }
+
+ @SuppressWarnings("deprecation")
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ 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));
+
+ // We use the deprecated API for backward compatibility
+ // The new API is available in ICE_CREAM_SANDWICH (15)
+ views.setRemoteAdapter(widgetId, 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);
+ try {
+ byte[] data = entry.imageData;
+ Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
+ views.setImageViewBitmap(R.id.photo, bitmap);
+ } catch (Throwable t) {
+ Log.w(TAG, "cannot load widget image: " + appWidgetId, t);
+ }
+
+ if (entry.imageUri != null) {
+ try {
+ Uri uri = Uri.parse(entry.imageUri);
+ Intent clickIntent = new Intent(context, WidgetClickHandler.class)
+ .setData(uri);
+ PendingIntent pendingClickIntent = PendingIntent.getActivity(context, 0,
+ clickIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+ views.setOnClickPendingIntent(R.id.photo, pendingClickIntent);
+ } catch (Throwable t) {
+ Log.w(TAG, "cannot load widget uri: " + appWidgetId, t);
+ }
+ }
+ 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();
+ }
+}
diff --git a/src/com/android/gallery3d/gadget/WidgetClickHandler.java b/src/com/android/gallery3d/gadget/WidgetClickHandler.java
new file mode 100644
index 000000000..37ee1a651
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/WidgetClickHandler.java
@@ -0,0 +1,77 @@
+/*
+ * 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.gadget;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.Gallery;
+import com.android.gallery3d.app.PhotoPage;
+import com.android.gallery3d.common.ApiHelper;
+
+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
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ protected void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+ // The behavior is changed in JB, refer to b/6384492 for more details
+ boolean tediousBack = Build.VERSION.SDK_INT >= ApiHelper.VERSION_CODES.JELLY_BEAN;
+ Uri uri = getIntent().getData();
+ Intent intent;
+ if (isValidDataUri(uri)) {
+ intent = new Intent(Intent.ACTION_VIEW, uri);
+ if (tediousBack) {
+ intent.putExtra(PhotoPage.KEY_TREAT_BACK_AS_UP, true);
+ }
+ } else {
+ Toast.makeText(this,
+ R.string.no_such_item, Toast.LENGTH_LONG).show();
+ intent = new Intent(this, Gallery.class);
+ }
+ if (tediousBack) {
+ intent.setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK |
+ Intent.FLAG_ACTIVITY_CLEAR_TASK |
+ Intent.FLAG_ACTIVITY_TASK_ON_HOME);
+ }
+ startActivity(intent);
+ finish();
+ }
+}
diff --git a/src/com/android/gallery3d/gadget/WidgetConfigure.java b/src/com/android/gallery3d/gadget/WidgetConfigure.java
new file mode 100644
index 000000000..2a4c6cfe4
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/WidgetConfigure.java
@@ -0,0 +1,209 @@
+/*
+ * 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.gadget;
+
+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.util.Log;
+import android.widget.RemoteViews;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AlbumPicker;
+import com.android.gallery3d.app.DialogPicker;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.LocalAlbum;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+
+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 String KEY_PICKED_ITEM = "picked-item";
+
+ 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 static int MAX_WIDGET_SIDE = 360;
+
+ private int mAppWidgetId = -1;
+ private Uri mPickedItem;
+
+ @Override
+ protected void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+ mAppWidgetId = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
+
+ if (mAppWidgetId == -1) {
+ setResult(Activity.RESULT_CANCELED);
+ finish();
+ return;
+ }
+
+ if (savedState == null) {
+ if (ApiHelper.HAS_REMOTE_VIEWS_SERVICE) {
+ Intent intent = new Intent(this, WidgetTypeChooser.class);
+ startActivityForResult(intent, REQUEST_WIDGET_TYPE);
+ } else { // Choose the photo type widget
+ setWidgetType(new Intent()
+ .putExtra(KEY_WIDGET_TYPE, R.id.widget_type_photo));
+ }
+ } else {
+ mPickedItem = savedState.getParcelable(KEY_PICKED_ITEM);
+ }
+ }
+
+ protected void onSaveInstanceStates(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(KEY_PICKED_ITEM, mPickedItem);
+ }
+
+ private void updateWidgetAndFinish(WidgetDatabaseHelper.Entry entry) {
+ AppWidgetManager manager = AppWidgetManager.getInstance(this);
+ RemoteViews views = PhotoAppWidgetProvider.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();
+
+ float width = res.getDimension(R.dimen.appwidget_width);
+ float height = res.getDimension(R.dimen.appwidget_height);
+
+ // We try to crop a larger image (by scale factor), but there is still
+ // a bound on the binder limit.
+ float scale = Math.min(WIDGET_SCALE_FACTOR,
+ MAX_WIDGET_SIDE / Math.max(width, height));
+
+ int widgetWidth = Math.round(width * scale);
+ int widgetHeight = Math.round(height * scale);
+
+ mPickedItem = data.getData();
+ Intent request = new Intent(CropActivity.CROP_ACTION, mPickedItem)
+ .putExtra(CropExtras.KEY_OUTPUT_X, widgetWidth)
+ .putExtra(CropExtras.KEY_OUTPUT_Y, widgetHeight)
+ .putExtra(CropExtras.KEY_ASPECT_X, widgetWidth)
+ .putExtra(CropExtras.KEY_ASPECT_Y, widgetHeight)
+ .putExtra(CropExtras.KEY_SCALE_UP_IF_NEEDED, true)
+ .putExtra(CropExtras.KEY_SCALE, true)
+ .putExtra(CropExtras.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 {
+ String relativePath = null;
+ GalleryApp galleryApp = (GalleryApp) getApplicationContext();
+ DataManager manager = galleryApp.getDataManager();
+ Path path = Path.fromString(albumPath);
+ MediaSet mediaSet = (MediaSet) manager.getMediaObject(path);
+ if (mediaSet instanceof LocalAlbum) {
+ int bucketId = Integer.parseInt(path.getSuffix());
+ // If the chosen album is a local album, find relative path
+ // Otherwise, leave the relative path field empty
+ relativePath = LocalAlbum.getRelativePath(bucketId);
+ Log.i(TAG, "Setting widget, album path: " + albumPath
+ + ", relative path: " + relativePath);
+ }
+ helper.setWidget(mAppWidgetId,
+ WidgetDatabaseHelper.TYPE_ALBUM, albumPath, relativePath);
+ updateWidgetAndFinish(helper.getEntry(mAppWidgetId));
+ } finally {
+ helper.close();
+ }
+ }
+
+ private void setWidgetType(Intent data) {
+ int widgetType = data.getIntExtra(KEY_WIDGET_TYPE, R.id.widget_type_shuffle);
+ if (widgetType == R.id.widget_type_album) {
+ Intent intent = new Intent(this, AlbumPicker.class);
+ startActivityForResult(intent, REQUEST_CHOOSE_ALBUM);
+ } else if (widgetType == R.id.widget_type_shuffle) {
+ WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this);
+ try {
+ helper.setWidget(mAppWidgetId, WidgetDatabaseHelper.TYPE_SHUFFLE, null, 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/gadget/WidgetDatabaseHelper.java b/src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java
new file mode 100644
index 000000000..c0145843b
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/WidgetDatabaseHelper.java
@@ -0,0 +1,309 @@
+/*
+ * 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.gadget;
+
+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.net.Uri;
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+public class WidgetDatabaseHelper extends SQLiteOpenHelper {
+ private static final String TAG = "PhotoDatabaseHelper";
+ private static final String DATABASE_NAME = "launcher.db";
+
+ // Increment the database version to 5. In version 5, we
+ // add a column in widgets table to record relative paths.
+ private static final int DATABASE_VERSION = 5;
+
+ 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";
+ private static final String FIELD_RELATIVE_PATH = "relativePath";
+
+ 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,
+ FIELD_APPWIDGET_ID, FIELD_RELATIVE_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 int INDEX_APPWIDGET_ID = 4;
+ private static final int INDEX_RELATIVE_PATH = 5;
+ private static final String WHERE_APPWIDGET_ID = FIELD_APPWIDGET_ID + " = ?";
+ private static final String WHERE_WIDGET_TYPE = FIELD_WIDGET_TYPE + " = ?";
+
+ public static class Entry {
+ public int widgetId;
+ public int type;
+ public String imageUri;
+ public byte imageData[];
+ public String albumPath;
+ public String relativePath;
+
+ private Entry() {}
+
+ private Entry(int id, Cursor cursor) {
+ widgetId = id;
+ type = cursor.getInt(INDEX_WIDGET_TYPE);
+ if (type == TYPE_SINGLE_PHOTO) {
+ imageUri = cursor.getString(INDEX_IMAGE_URI);
+ imageData = cursor.getBlob(INDEX_PHOTO_BLOB);
+ } else if (type == TYPE_ALBUM) {
+ albumPath = cursor.getString(INDEX_ALBUM_PATH);
+ relativePath = cursor.getString(INDEX_RELATIVE_PATH);
+ }
+ }
+
+ private Entry(Cursor cursor) {
+ this(cursor.getInt(INDEX_APPWIDGET_ID), cursor);
+ }
+ }
+
+ 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, "
+ + FIELD_RELATIVE_PATH + " TEXT)");
+ }
+
+ private void saveData(SQLiteDatabase db, int oldVersion, ArrayList<Entry> data) {
+ if (oldVersion <= 2) {
+ Cursor cursor = db.query("photos",
+ new String[] {FIELD_APPWIDGET_ID, FIELD_PHOTO_BLOB},
+ null, null, null, null, null);
+ if (cursor == null) return;
+ try {
+ while (cursor.moveToNext()) {
+ Entry entry = new Entry();
+ entry.type = TYPE_SINGLE_PHOTO;
+ entry.widgetId = cursor.getInt(0);
+ entry.imageData = cursor.getBlob(1);
+ data.add(entry);
+ }
+ } finally {
+ cursor.close();
+ }
+ } else if (oldVersion == 3) {
+ Cursor cursor = db.query("photos",
+ new String[] {FIELD_APPWIDGET_ID, FIELD_PHOTO_BLOB, FIELD_IMAGE_URI},
+ null, null, null, null, null);
+ if (cursor == null) return;
+ try {
+ while (cursor.moveToNext()) {
+ Entry entry = new Entry();
+ entry.type = TYPE_SINGLE_PHOTO;
+ entry.widgetId = cursor.getInt(0);
+ entry.imageData = cursor.getBlob(1);
+ entry.imageUri = cursor.getString(2);
+ data.add(entry);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ private void restoreData(SQLiteDatabase db, ArrayList<Entry> data) {
+ db.beginTransaction();
+ try {
+ for (Entry entry : data) {
+ ContentValues values = new ContentValues();
+ values.put(FIELD_APPWIDGET_ID, entry.widgetId);
+ values.put(FIELD_WIDGET_TYPE, entry.type);
+ values.put(FIELD_IMAGE_URI, entry.imageUri);
+ values.put(FIELD_PHOTO_BLOB, entry.imageData);
+ values.put(FIELD_ALBUM_PATH, entry.albumPath);
+ db.insert(TABLE_WIDGETS, null, values);
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion < 4) {
+ // Table "photos" is renamed to "widget" in version 4
+ ArrayList<Entry> data = new ArrayList<Entry>();
+ saveData(db, oldVersion, data);
+
+ Log.w(TAG, "destroying all old data.");
+ db.execSQL("DROP TABLE IF EXISTS photos");
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_WIDGETS);
+ onCreate(db);
+
+ restoreData(db, data);
+ }
+ // Add a column for relative path
+ if (oldVersion < DATABASE_VERSION) {
+ try {
+ db.execSQL("ALTER TABLE widgets ADD COLUMN relativePath TEXT");
+ } catch (Throwable t) {
+ Log.e(TAG, "Failed to add the column for relative path.");
+ return;
+ }
+ }
+ }
+
+ /**
+ * 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, String relativePath) {
+ 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));
+ values.put(FIELD_RELATIVE_PATH, relativePath);
+ getWritableDatabase().replaceOrThrow(TABLE_WIDGETS, null, values);
+ return true;
+ } catch (Throwable e) {
+ Log.e(TAG, "set widget fail", e);
+ return false;
+ }
+ }
+
+ public Entry getEntry(int appWidgetId) {
+ Cursor cursor = null;
+ try {
+ SQLiteDatabase db = getReadableDatabase();
+ cursor = db.query(TABLE_WIDGETS, PROJECTION,
+ WHERE_APPWIDGET_ID, new String[] {String.valueOf(appWidgetId)},
+ null, null, null);
+ if (cursor == null || !cursor.moveToNext()) {
+ Log.e(TAG, "query fail: empty cursor: " + cursor + " appWidgetId: "
+ + appWidgetId);
+ 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);
+ }
+ }
+
+ public List<Entry> getEntries(int type) {
+ Cursor cursor = null;
+ try {
+ SQLiteDatabase db = getReadableDatabase();
+ cursor = db.query(TABLE_WIDGETS, PROJECTION,
+ WHERE_WIDGET_TYPE, new String[] {String.valueOf(type)},
+ null, null, null);
+ if (cursor == null) {
+ Log.e(TAG, "query fail: null cursor: " + cursor);
+ return null;
+ }
+ ArrayList<Entry> result = new ArrayList<Entry>(cursor.getCount());
+ while (cursor.moveToNext()) {
+ result.add(new Entry(cursor));
+ }
+ return result;
+ } catch (Throwable e) {
+ Log.e(TAG, "Could not load widget from database", e);
+ return null;
+ } finally {
+ Utils.closeSilently(cursor);
+ }
+ }
+
+ /**
+ * Updates the entry in the widget database.
+ */
+ public void updateEntry(Entry entry) {
+ deleteEntry(entry.widgetId);
+ try {
+ ContentValues values = new ContentValues();
+ values.put(FIELD_APPWIDGET_ID, entry.widgetId);
+ values.put(FIELD_WIDGET_TYPE, entry.type);
+ values.put(FIELD_ALBUM_PATH, entry.albumPath);
+ values.put(FIELD_IMAGE_URI, entry.imageUri);
+ values.put(FIELD_PHOTO_BLOB, entry.imageData);
+ values.put(FIELD_RELATIVE_PATH, entry.relativePath);
+ getWritableDatabase().insert(TABLE_WIDGETS, null, values);
+ } catch (Throwable e) {
+ Log.e(TAG, "set widget fail", e);
+ }
+ }
+
+ /**
+ * Remove any bitmap associated with the given appWidgetId.
+ */
+ public void deleteEntry(int appWidgetId) {
+ try {
+ SQLiteDatabase db = getWritableDatabase();
+ db.delete(TABLE_WIDGETS, WHERE_APPWIDGET_ID,
+ new String[] {String.valueOf(appWidgetId)});
+ } catch (SQLiteException e) {
+ Log.e(TAG, "Could not delete photo from database", e);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/gadget/WidgetService.java b/src/com/android/gallery3d/gadget/WidgetService.java
new file mode 100644
index 000000000..94dd16439
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/WidgetService.java
@@ -0,0 +1,143 @@
+/*
+ * 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.gadget;
+
+import android.annotation.TargetApi;
+import android.appwidget.AppWidgetManager;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.ContentListener;
+
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+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 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) {
+ mSource = new MediaSetSource(mApp.getDataManager(), mAlbumPath);
+ } 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;
+ }
+
+ @Override
+ public int getCount() {
+ return mSource.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ 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/gadget/WidgetSource.java b/src/com/android/gallery3d/gadget/WidgetSource.java
new file mode 100644
index 000000000..92874c740
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/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.gadget;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import com.android.gallery3d.data.ContentListener;
+
+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/gadget/WidgetTypeChooser.java b/src/com/android/gallery3d/gadget/WidgetTypeChooser.java
new file mode 100644
index 000000000..1694f1c04
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/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.gadget;
+
+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;
+
+import com.android.gallery3d.R;
+
+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/gadget/WidgetUtils.java b/src/com/android/gallery3d/gadget/WidgetUtils.java
new file mode 100644
index 000000000..c20c186df
--- /dev/null
+++ b/src/com/android/gallery3d/gadget/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.gadget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.Log;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.ThreadPool;
+
+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;
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/BasicTexture.java b/src/com/android/gallery3d/glrenderer/BasicTexture.java
new file mode 100644
index 000000000..2e77b903f
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/BasicTexture.java
@@ -0,0 +1,212 @@
+/*
+ * 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.glrenderer;
+
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+
+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.
+public 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;
+
+ // Log a warning if a texture is larger along a dimension
+ private static final int MAX_TEXTURE_SIZE = 4096;
+
+ protected int mId = -1;
+ protected int mState;
+
+ protected int mWidth = UNSPECIFIED;
+ protected int mHeight = UNSPECIFIED;
+
+ protected int mTextureWidth;
+ protected int mTextureHeight;
+
+ private boolean mHasBorder;
+
+ protected GLCanvas mCanvasRef = null;
+ private static WeakHashMap<BasicTexture, Object> sAllTextures
+ = new WeakHashMap<BasicTexture, Object>();
+ 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;
+ }
+
+ /**
+ * 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.
+ */
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ mTextureWidth = width > 0 ? Utils.nextPowerOf2(width) : 0;
+ mTextureHeight = height > 0 ? Utils.nextPowerOf2(height) : 0;
+ if (mTextureWidth > MAX_TEXTURE_SIZE || mTextureHeight > MAX_TEXTURE_SIZE) {
+ Log.w(TAG, String.format("texture is too large: %d x %d",
+ mTextureWidth, mTextureHeight), new Exception());
+ }
+ }
+
+ public boolean isFlippedVertically() {
+ return false;
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ 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;
+ }
+
+ // Returns true if the texture has one pixel transparent border around the
+ // actual content. This is used to avoid jigged edges.
+ //
+ // The jigged edges appear because we use GL_CLAMP_TO_EDGE for texture wrap
+ // mode (GL_CLAMP is not available in OpenGL ES), so a pixel partially
+ // covered by the texture will use the color of the edge texel. If we add
+ // the transparent border, the color of the edge texel will be mixed with
+ // appropriate amount of transparent.
+ //
+ // Currently our background is black, so we can draw the thumbnails without
+ // enabling blending.
+ public boolean hasBorder() {
+ return mHasBorder;
+ }
+
+ protected void setBorder(boolean hasBorder) {
+ mHasBorder = hasBorder;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y) {
+ canvas.drawTexture(this, x, y, getWidth(), getHeight());
+ }
+
+ @Override
+ 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);
+
+ // Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D).
+ abstract protected int getTarget();
+
+ public boolean isLoaded() {
+ return mState == STATE_LOADED;
+ }
+
+ // 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;
+ if (canvas != null && mId != -1) {
+ canvas.unloadTexture(this);
+ mId = -1; // Don't free it again.
+ }
+ mState = 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();
+ }
+ }
+ }
+
+ public static void invalidateAllTextures() {
+ synchronized (sAllTextures) {
+ for (BasicTexture t : sAllTextures.keySet()) {
+ t.mState = STATE_UNLOADED;
+ t.setAssociatedCanvas(null);
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/BitmapTexture.java b/src/com/android/gallery3d/glrenderer/BitmapTexture.java
new file mode 100644
index 000000000..100b0b3b9
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/BitmapTexture.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.glrenderer;
+
+import android.graphics.Bitmap;
+
+import junit.framework.Assert;
+
+// 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) {
+ this(bitmap, false);
+ }
+
+ public BitmapTexture(Bitmap bitmap, boolean hasBorder) {
+ super(hasBorder);
+ Assert.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/glrenderer/CanvasTexture.java b/src/com/android/gallery3d/glrenderer/CanvasTexture.java
new file mode 100644
index 000000000..bff9d4baa
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/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.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+
+// 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/glrenderer/ColorTexture.java b/src/com/android/gallery3d/glrenderer/ColorTexture.java
new file mode 100644
index 000000000..904c78e1b
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/ColorTexture.java
@@ -0,0 +1,63 @@
+/*
+ * 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.glrenderer;
+
+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;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y) {
+ draw(canvas, x, y, mWidth, mHeight);
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+ canvas.fillRect(x, y, w, h, mColor);
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return Utils.isOpaque(mColor);
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/ExtTexture.java b/src/com/android/gallery3d/glrenderer/ExtTexture.java
new file mode 100644
index 000000000..af76300b1
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/ExtTexture.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2012 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.glrenderer;
+
+// ExtTexture is a texture whose content comes from a external texture.
+// Before drawing, setSize() should be called.
+public class ExtTexture extends BasicTexture {
+
+ private int mTarget;
+
+ public ExtTexture(GLCanvas canvas, int target) {
+ GLId glId = canvas.getGLId();
+ mId = glId.generateTexture();
+ mTarget = target;
+ }
+
+ private void uploadToCanvas(GLCanvas canvas) {
+ canvas.setTextureParameters(this);
+ setAssociatedCanvas(canvas);
+ mState = STATE_LOADED;
+ }
+
+ @Override
+ protected boolean onBind(GLCanvas canvas) {
+ if (!isLoaded()) {
+ uploadToCanvas(canvas);
+ }
+
+ return true;
+ }
+
+ @Override
+ public int getTarget() {
+ return mTarget;
+ }
+
+ @Override
+ 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/glrenderer/FadeInTexture.java b/src/com/android/gallery3d/glrenderer/FadeInTexture.java
new file mode 100644
index 000000000..838d465f5
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/FadeInTexture.java
@@ -0,0 +1,43 @@
+/*
+ * 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.glrenderer;
+
+
+// FadeInTexture is a texture which begins with a color, then gradually animates
+// into a given texture.
+public class FadeInTexture extends FadeTexture implements Texture {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FadeInTexture";
+
+ private final int mColor;
+ private final TiledTexture mTexture;
+
+ public FadeInTexture(int color, TiledTexture texture) {
+ super(texture.getWidth(), texture.getHeight(), texture.isOpaque());
+ mColor = color;
+ mTexture = texture;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+ if (isAnimating()) {
+ mTexture.drawMixed(canvas, mColor, getRatio(), x, y, w, h);
+ } else {
+ mTexture.draw(canvas, x, y, w, h);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/FadeOutTexture.java b/src/com/android/gallery3d/glrenderer/FadeOutTexture.java
new file mode 100644
index 000000000..b05f3b631
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/FadeOutTexture.java
@@ -0,0 +1,42 @@
+/*
+ * 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.glrenderer;
+
+
+// FadeOutTexture is a texture which begins with a given texture, then gradually animates
+// into fading out totally.
+public class FadeOutTexture extends FadeTexture {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FadeOutTexture";
+
+ private final BasicTexture mTexture;
+
+ public FadeOutTexture(BasicTexture texture) {
+ super(texture.getWidth(), texture.getHeight(), texture.isOpaque());
+ mTexture = texture;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+ if (isAnimating()) {
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.setAlpha(getRatio());
+ mTexture.draw(canvas, x, y, w, h);
+ canvas.restore();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/FadeTexture.java b/src/com/android/gallery3d/glrenderer/FadeTexture.java
new file mode 100644
index 000000000..002c90f5c
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/FadeTexture.java
@@ -0,0 +1,81 @@
+/*
+ * 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.glrenderer;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.ui.AnimationTime;
+
+// FadeTexture is a texture which fades the given texture along the time.
+public abstract class FadeTexture implements Texture {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FadeTexture";
+
+ // The duration of the fading animation in milliseconds
+ public static final int DURATION = 180;
+
+ private final long mStartTime;
+ private final int mWidth;
+ private final int mHeight;
+ private final boolean mIsOpaque;
+ private boolean mIsAnimating;
+
+ public FadeTexture(int width, int height, boolean opaque) {
+ mWidth = width;
+ mHeight = height;
+ mIsOpaque = opaque;
+ mStartTime = now();
+ mIsAnimating = true;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y) {
+ draw(canvas, x, y, mWidth, mHeight);
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return mIsOpaque;
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public boolean isAnimating() {
+ if (mIsAnimating) {
+ if (now() - mStartTime >= DURATION) {
+ mIsAnimating = false;
+ }
+ }
+ return mIsAnimating;
+ }
+
+ protected float getRatio() {
+ float r = (float)(now() - mStartTime) / DURATION;
+ return Utils.clamp(1.0f - r, 0.0f, 1.0f);
+ }
+
+ private long now() {
+ return AnimationTime.get();
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLCanvas.java b/src/com/android/gallery3d/glrenderer/GLCanvas.java
new file mode 100644
index 000000000..305e90521
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLCanvas.java
@@ -0,0 +1,217 @@
+/*
+ * 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.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+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 {
+
+ public GLId getGLId();
+
+ // 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 abstract void setSize(int width, int height);
+
+ // Clear the drawing buffers. This should only be used by GLRoot.
+ public abstract void clearBuffer();
+
+ public abstract void clearBuffer(float[] argb);
+
+ // Sets and gets the current alpha, alpha must be in [0, 1].
+ public abstract void setAlpha(float alpha);
+
+ public abstract float getAlpha();
+
+ // (current alpha) = (current alpha) * alpha
+ public abstract void multiplyAlpha(float alpha);
+
+ // Change the current transform matrix.
+ public abstract void translate(float x, float y, float z);
+
+ public abstract void translate(float x, float y);
+
+ public abstract void scale(float sx, float sy, float sz);
+
+ public abstract void rotate(float angle, float x, float y, float z);
+
+ public abstract void multiplyMatrix(float[] mMatrix, int offset);
+
+ // Pushes the configuration state (matrix, and alpha) onto
+ // a private stack.
+ public abstract void save();
+
+ // Same as save(), but only save those specified in saveFlags.
+ public abstract void save(int saveFlags);
+
+ public static final int SAVE_FLAG_ALL = 0xFFFFFFFF;
+ public static final int SAVE_FLAG_ALPHA = 0x01;
+ public static final int SAVE_FLAG_MATRIX = 0x02;
+
+ // 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 abstract void restore();
+
+ // Draws a line using the specified paint from (x1, y1) to (x2, y2).
+ // (Both end points are included).
+ public abstract 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 abstract void drawRect(float x1, float y1, float x2, float y2, GLPaint paint);
+
+ // Fills the specified rectangle with the specified color.
+ public abstract void fillRect(float x, float y, float width, float height, int color);
+
+ // Draws a texture to the specified rectangle.
+ public abstract void drawTexture(
+ BasicTexture texture, int x, int y, int width, int height);
+
+ public abstract void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,
+ int uvBuffer, int indexBuffer, int indexCount);
+
+ // Draws the source rectangle part of the texture to the target rectangle.
+ public abstract void drawTexture(BasicTexture texture, RectF source, RectF target);
+
+ // Draw a texture with a specified texture transform.
+ public abstract void drawTexture(BasicTexture texture, float[] mTextureTransform,
+ int x, int y, int w, int h);
+
+ // 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 abstract void drawMixed(BasicTexture from, int toColor,
+ float ratio, int x, int y, int w, int h);
+
+ // Draw a region of a texture and a specified color to the specified
+ // rectangle. The actual color used is from * (1 - ratio) + to * ratio.
+ // The region of the texture is defined by parameter "src". The target
+ // rectangle is specified by parameter "target".
+ public abstract void drawMixed(BasicTexture from, int toColor,
+ float ratio, RectF src, RectF target);
+
+ // 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 abstract boolean unloadTexture(BasicTexture texture);
+
+ // Delete the specified buffer object, similar to unloadTexture.
+ public abstract void deleteBuffer(int bufferId);
+
+ // Delete the textures and buffers in GL side. This function should only be
+ // called in the GL thread.
+ public abstract void deleteRecycledResources();
+
+ // Dump statistics information and clear the counters. For debug only.
+ public abstract void dumpStatisticsAndClear();
+
+ public abstract void beginRenderTarget(RawTexture texture);
+
+ public abstract void endRenderTarget();
+
+ /**
+ * Sets texture parameters to use GL_CLAMP_TO_EDGE for both
+ * GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T. Sets texture parameters to be
+ * GL_LINEAR for GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER.
+ * bindTexture() must be called prior to this.
+ *
+ * @param texture The texture to set parameters on.
+ */
+ public abstract void setTextureParameters(BasicTexture texture);
+
+ /**
+ * Initializes the texture to a size by calling texImage2D on it.
+ *
+ * @param texture The texture to initialize the size.
+ * @param format The texture format (e.g. GL_RGBA)
+ * @param type The texture type (e.g. GL_UNSIGNED_BYTE)
+ */
+ public abstract void initializeTextureSize(BasicTexture texture, int format, int type);
+
+ /**
+ * Initializes the texture to a size by calling texImage2D on it.
+ *
+ * @param texture The texture to initialize the size.
+ * @param bitmap The bitmap to initialize the bitmap with.
+ */
+ public abstract void initializeTexture(BasicTexture texture, Bitmap bitmap);
+
+ /**
+ * Calls glTexSubImage2D to upload a bitmap to the texture.
+ *
+ * @param texture The target texture to write to.
+ * @param xOffset Specifies a texel offset in the x direction within the
+ * texture array.
+ * @param yOffset Specifies a texel offset in the y direction within the
+ * texture array.
+ * @param format The texture format (e.g. GL_RGBA)
+ * @param type The texture type (e.g. GL_UNSIGNED_BYTE)
+ */
+ public abstract void texSubImage2D(BasicTexture texture, int xOffset, int yOffset,
+ Bitmap bitmap,
+ int format, int type);
+
+ /**
+ * Generates buffers and uploads the buffer data.
+ *
+ * @param buffer The buffer to upload
+ * @return The buffer ID that was generated.
+ */
+ public abstract int uploadBuffer(java.nio.FloatBuffer buffer);
+
+ /**
+ * Generates buffers and uploads the element array buffer data.
+ *
+ * @param buffer The buffer to upload
+ * @return The buffer ID that was generated.
+ */
+ public abstract int uploadBuffer(java.nio.ByteBuffer buffer);
+
+ /**
+ * After LightCycle makes GL calls, this method is called to restore the GL
+ * configuration to the one expected by GLCanvas.
+ */
+ public abstract void recoverFromLightCycle();
+
+ /**
+ * Gets the bounds given by x, y, width, and height as well as the internal
+ * matrix state. There is no special handling for non-90-degree rotations.
+ * It only considers the lower-left and upper-right corners as the bounds.
+ *
+ * @param bounds The output bounds to write to.
+ * @param x The left side of the input rectangle.
+ * @param y The bottom of the input rectangle.
+ * @param width The width of the input rectangle.
+ * @param height The height of the input rectangle.
+ */
+ public abstract void getBounds(Rect bounds, int x, int y, int width, int height);
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLES11Canvas.java b/src/com/android/gallery3d/glrenderer/GLES11Canvas.java
new file mode 100644
index 000000000..7013c3d1f
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLES11Canvas.java
@@ -0,0 +1,997 @@
+/*
+ * 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.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.opengl.GLU;
+import android.opengl.GLUtils;
+import android.opengl.Matrix;
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.IntArray;
+
+import junit.framework.Assert;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+
+import javax.microedition.khronos.opengles.GL10;
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11Ext;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
+
+public class GLES11Canvas 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 GL11 mGL;
+
+ private final float mMatrixValues[] = new float[16];
+ private final float mTextureMatrixValues[] = new float[16];
+
+ // The results of mapPoints are stored in this buffer, and the order is
+ // x1, y1, x2, y2.
+ private final float mMapPointsBuffer[] = new float[4];
+
+ private final float mTextureColor[] = new float[4];
+
+ private int mBoxCoords;
+
+ private GLState mGLState;
+ private final ArrayList<RawTexture> mTargetStack = new ArrayList<RawTexture>();
+
+ private float mAlpha;
+ private final ArrayList<ConfigState> mRestoreStack = new ArrayList<ConfigState>();
+ 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 mScreenWidth;
+ private int mScreenHeight;
+ private boolean mBlendEnabled = true;
+ private int mFrameBuffer[] = new int[1];
+ private static float[] sCropRect = new float[4];
+
+ private RawTexture mTargetTexture;
+
+ // Drawing statistics
+ int mCountDrawLine;
+ int mCountFillRect;
+ int mCountDrawMesh;
+ int mCountTextureRect;
+ int mCountTextureOES;
+
+ private static GLId mGLId = new GLES11IdImpl();
+
+ public GLES11Canvas(GL11 gl) {
+ mGL = gl;
+ mGLState = new GLState(gl);
+ // 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];
+ mGLId.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 and mAlpha will be initialized in setSize()
+ }
+
+ @Override
+ public void setSize(int width, int height) {
+ Assert.assertTrue(width >= 0 && height >= 0);
+
+ if (mTargetTexture == null) {
+ mScreenWidth = width;
+ mScreenHeight = height;
+ }
+ mAlpha = 1.0f;
+
+ 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);
+ // to match the graphic coordinate system in android, we flip it vertically.
+ if (mTargetTexture == null) {
+ Matrix.translateM(matrix, 0, 0, height, 0);
+ Matrix.scaleM(matrix, 0, 1, -1, 1);
+ }
+ }
+
+ @Override
+ public void setAlpha(float alpha) {
+ Assert.assertTrue(alpha >= 0 && alpha <= 1);
+ mAlpha = alpha;
+ }
+
+ @Override
+ public float getAlpha() {
+ return mAlpha;
+ }
+
+ @Override
+ public void multiplyAlpha(float alpha) {
+ Assert.assertTrue(alpha >= 0 && alpha <= 1);
+ mAlpha *= alpha;
+ }
+
+ private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {
+ return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
+ }
+
+ @Override
+ 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());
+
+ saveTransform();
+ translate(x, y);
+ scale(width, height, 1);
+
+ gl.glLoadMatrixf(mMatrixValues, 0);
+ gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, 4);
+
+ restoreTransform();
+ mCountDrawLine++;
+ }
+
+ @Override
+ 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());
+
+ saveTransform();
+ translate(x1, y1);
+ scale(x2 - x1, y2 - y1, 1);
+
+ gl.glLoadMatrixf(mMatrixValues, 0);
+ gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, 2);
+
+ restoreTransform();
+ mCountDrawLine++;
+ }
+
+ @Override
+ public void fillRect(float x, float y, float width, float height, int color) {
+ mGLState.setColorMode(color, mAlpha);
+ GL11 gl = mGL;
+
+ saveTransform();
+ translate(x, y);
+ scale(width, height, 1);
+
+ gl.glLoadMatrixf(mMatrixValues, 0);
+ gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);
+
+ restoreTransform();
+ mCountFillRect++;
+ }
+
+ @Override
+ public void translate(float x, float y, float z) {
+ Matrix.translateM(mMatrixValues, 0, x, y, z);
+ }
+
+ // This is a faster version of translate(x, y, z) because
+ // (1) we knows z = 0, (2) we inline the Matrix.translateM call,
+ // (3) we unroll the loop
+ @Override
+ public void translate(float x, float y) {
+ float[] m = mMatrixValues;
+ m[12] += m[0] * x + m[4] * y;
+ m[13] += m[1] * x + m[5] * y;
+ m[14] += m[2] * x + m[6] * y;
+ m[15] += m[3] * x + m[7] * y;
+ }
+
+ @Override
+ public void scale(float sx, float sy, float sz) {
+ Matrix.scaleM(mMatrixValues, 0, sx, sy, sz);
+ }
+
+ @Override
+ public void rotate(float angle, float x, float y, float z) {
+ if (angle == 0) return;
+ 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);
+ }
+
+ @Override
+ public void multiplyMatrix(float matrix[], int offset) {
+ float[] temp = mTempMatrix;
+ Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset);
+ 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);
+ scale(width, height, 1);
+
+ gl.glLoadMatrixf(mMatrixValues, 0);
+ gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);
+
+ restoreTransform();
+ mCountTextureRect++;
+ }
+
+ @Override
+ 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);
+
+ 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++;
+ }
+
+ // Transforms two points by the given matrix m. The result
+ // {x1', y1', x2', y2'} are stored in mMapPointsBuffer and also returned.
+ private float[] mapPoints(float m[], int x1, int y1, int x2, int y2) {
+ float[] r = mMapPointsBuffer;
+
+ // Multiply m and (x1 y1 0 1) to produce (x3 y3 z3 w3). z3 is unused.
+ float x3 = m[0] * x1 + m[4] * y1 + m[12];
+ float y3 = m[1] * x1 + m[5] * y1 + m[13];
+ float w3 = m[3] * x1 + m[7] * y1 + m[15];
+ r[0] = x3 / w3;
+ r[1] = y3 / w3;
+
+ // Same for x2 y2.
+ float x4 = m[0] * x2 + m[4] * y2 + m[12];
+ float y4 = m[1] * x2 + m[5] * y2 + m[13];
+ float w4 = m[3] * x2 + m[7] * y2 + m[15];
+ r[2] = x4 / w4;
+ r[3] = y4 / w4;
+
+ return r;
+ }
+
+ 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)) {
+ if (texture.hasBorder()) {
+ setTextureCoords(
+ 1.0f / texture.getTextureWidth(),
+ 1.0f / texture.getTextureHeight(),
+ (texture.getWidth() - 1.0f) / texture.getTextureWidth(),
+ (texture.getHeight() - 1.0f) / texture.getTextureHeight());
+ } else {
+ 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 = (int) (points[0] + 0.5f);
+ y = (int) (points[1] + 0.5f);
+ width = (int) (points[2] + 0.5f) - x;
+ height = (int) (points[3] + 0.5f) - y;
+ if (width > 0 && height > 0) {
+ ((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height);
+ mCountTextureOES++;
+ }
+ }
+ }
+
+ @Override
+ public void drawTexture(
+ BasicTexture texture, int x, int y, int width, int height) {
+ drawTexture(texture, x, y, width, height, mAlpha);
+ }
+
+ private 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);
+ }
+
+ @Override
+ 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());
+ }
+
+ @Override
+ public void drawTexture(BasicTexture texture, float[] mTextureTransform,
+ int x, int y, int w, int h) {
+ mGLState.setBlendEnabled(mBlendEnabled
+ && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
+ if (!bindTexture(texture)) return;
+ setTextureCoords(mTextureTransform);
+ mGLState.setTextureAlpha(mAlpha);
+ textureRect(x, y, w, h);
+ }
+
+ // 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 static 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;
+ }
+ }
+
+ @Override
+ 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);
+ }
+
+ private boolean bindTexture(BasicTexture texture) {
+ if (!texture.onBind(this)) return false;
+ int target = texture.getTarget();
+ mGLState.setTextureTarget(target);
+ mGL.glBindTexture(target, 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 setMixedColor(int toColor, float ratio, float alpha) {
+ //
+ // The formula we want:
+ // alpha * ((1 - ratio) * from + ratio * to)
+ //
+ // The formula that GL supports is in the form of:
+ // combo * from + (1 - combo) * to * scale
+ //
+ // So, we have combo = alpha * (1 - ratio)
+ // and scale = alpha * ratio / (1 - combo)
+ //
+ float combo = alpha * (1 - ratio);
+ float scale = alpha * ratio / (1 - combo);
+
+ // 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 colorScale = scale * (toColor >>> 24) / (0xff * 0xff);
+ setTextureColor(((toColor >>> 16) & 0xff) * colorScale,
+ ((toColor >>> 8) & 0xff) * colorScale,
+ (toColor & 0xff) * colorScale, combo);
+ GL11 gl = mGL;
+ 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);
+
+ }
+
+ @Override
+ public void drawMixed(BasicTexture from, int toColor, float ratio,
+ RectF source, RectF target) {
+ if (target.width() <= 0 || target.height() <= 0) return;
+
+ if (ratio <= 0.01f) {
+ drawTexture(from, source, target);
+ return;
+ } else if (ratio >= 1) {
+ fillRect(target.left, target.top, target.width(), target.height(), toColor);
+ return;
+ }
+
+ float alpha = mAlpha;
+
+ // Copy the input to avoid changing it.
+ mDrawTextureSourceRect.set(source);
+ mDrawTextureTargetRect.set(target);
+ source = mDrawTextureSourceRect;
+ target = mDrawTextureTargetRect;
+
+ mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()
+ || !Utils.isOpaque(toColor) || alpha < OPAQUE_ALPHA));
+
+ if (!bindTexture(from)) return;
+
+ // Interpolate the RGB and alpha values between both textures.
+ mGLState.setTexEnvMode(GL11.GL_COMBINE);
+ setMixedColor(toColor, ratio, alpha);
+ convertCoordinate(source, target, from);
+ setTextureCoords(source);
+ textureRect(target.left, target.top, target.width(), target.height());
+ mGLState.setTexEnvMode(GL11.GL_REPLACE);
+ }
+
+ private void drawMixed(BasicTexture from, int toColor,
+ float ratio, int x, int y, int width, int height, float alpha) {
+ // change from 0 to 0.01f to prevent getting divided by zero below
+ if (ratio <= 0.01f) {
+ 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;
+
+ // Interpolate the RGB and alpha values between both textures.
+ mGLState.setTexEnvMode(GL11.GL_COMBINE);
+ setMixedColor(toColor, ratio, alpha);
+
+ drawBoundTexture(from, x, y, width, height);
+ mGLState.setTexEnvMode(GL11.GL_REPLACE);
+ }
+
+ // 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;
+ }
+
+ private static class GLState {
+
+ private final GL11 mGL;
+
+ private int mTexEnvMode = GL11.GL_REPLACE;
+ private float mTextureAlpha = 1.0f;
+ private int mTextureTarget = GL11.GL_TEXTURE_2D;
+ 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.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.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 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;
+
+ setTextureTarget(0);
+
+ 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));
+ }
+
+ // target is a value like GL_TEXTURE_2D. If target = 0, texturing is disabled.
+ public void setTextureTarget(int target) {
+ if (mTextureTarget == target) return;
+ if (mTextureTarget != 0) {
+ mGL.glDisable(mTextureTarget);
+ }
+ mTextureTarget = target;
+ if (mTextureTarget != 0) {
+ mGL.glEnable(mTextureTarget);
+ }
+ }
+
+ public void setBlendEnabled(boolean enabled) {
+ if (mBlendEnabled == enabled) return;
+ mBlendEnabled = enabled;
+ if (enabled) {
+ mGL.glEnable(GL11.GL_BLEND);
+ } else {
+ mGL.glDisable(GL11.GL_BLEND);
+ }
+ }
+ }
+
+ @Override
+ public void clearBuffer(float[] argb) {
+ if(argb != null && argb.length == 4) {
+ mGL.glClearColor(argb[1], argb[2], argb[3], argb[0]);
+ } else {
+ mGL.glClearColor(0, 0, 0, 1);
+ }
+ mGL.glClear(GL10.GL_COLOR_BUFFER_BIT);
+ }
+
+ @Override
+ public void clearBuffer() {
+ clearBuffer(null);
+ }
+
+ 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);
+ }
+
+ private void setTextureCoords(float[] mTextureTransform) {
+ mGL.glMatrixMode(GL11.GL_TEXTURE);
+ mGL.glLoadMatrixf(mTextureTransform, 0);
+ mGL.glMatrixMode(GL11.GL_MODELVIEW);
+ }
+
+ // unloadTexture and deleteBuffer can be called from the finalizer thread,
+ // so we synchronized on the mUnboundTextures object.
+ @Override
+ public boolean unloadTexture(BasicTexture t) {
+ synchronized (mUnboundTextures) {
+ if (!t.isLoaded()) return false;
+ mUnboundTextures.add(t.mId);
+ return true;
+ }
+ }
+
+ @Override
+ public void deleteBuffer(int bufferId) {
+ synchronized (mUnboundTextures) {
+ mDeleteBuffers.add(bufferId);
+ }
+ }
+
+ @Override
+ public void deleteRecycledResources() {
+ synchronized (mUnboundTextures) {
+ IntArray ids = mUnboundTextures;
+ if (ids.size() > 0) {
+ mGLId.glDeleteTextures(mGL, ids.size(), ids.getInternalArray(), 0);
+ ids.clear();
+ }
+
+ ids = mDeleteBuffers;
+ if (ids.size() > 0) {
+ mGLId.glDeleteBuffers(mGL, ids.size(), ids.getInternalArray(), 0);
+ ids.clear();
+ }
+ }
+ }
+
+ @Override
+ public void save() {
+ save(SAVE_FLAG_ALL);
+ }
+
+ @Override
+ public void save(int saveFlags) {
+ ConfigState config = obtainRestoreConfig();
+
+ if ((saveFlags & SAVE_FLAG_ALPHA) != 0) {
+ config.mAlpha = mAlpha;
+ } else {
+ config.mAlpha = -1;
+ }
+
+ if ((saveFlags & SAVE_FLAG_MATRIX) != 0) {
+ System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16);
+ } else {
+ config.mMatrix[0] = Float.NEGATIVE_INFINITY;
+ }
+
+ mRestoreStack.add(config);
+ }
+
+ @Override
+ public void restore() {
+ if (mRestoreStack.isEmpty()) throw new IllegalStateException();
+ ConfigState config = mRestoreStack.remove(mRestoreStack.size() - 1);
+ 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;
+ float mMatrix[] = new float[16];
+ ConfigState mNextFree;
+
+ public void restore(GLES11Canvas canvas) {
+ if (mAlpha >= 0) canvas.setAlpha(mAlpha);
+ if (mMatrix[0] != Float.NEGATIVE_INFINITY) {
+ System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16);
+ }
+ }
+ }
+
+ @Override
+ 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);
+ }
+
+ private void setRenderTarget(RawTexture texture) {
+ GL11ExtensionPack gl11ep = (GL11ExtensionPack) mGL;
+
+ if (mTargetTexture == null && texture != null) {
+ mGLId.glGenBuffers(1, mFrameBuffer, 0);
+ gl11ep.glBindFramebufferOES(
+ GL11ExtensionPack.GL_FRAMEBUFFER_OES, mFrameBuffer[0]);
+ }
+ if (mTargetTexture != null && texture == null) {
+ gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES, 0);
+ gl11ep.glDeleteFramebuffersOES(1, mFrameBuffer, 0);
+ }
+
+ mTargetTexture = texture;
+ if (texture == null) {
+ setSize(mScreenWidth, mScreenHeight);
+ } else {
+ setSize(texture.getWidth(), texture.getHeight());
+
+ if (!texture.isLoaded()) texture.prepare(this);
+
+ gl11ep.glFramebufferTexture2DOES(
+ GL11ExtensionPack.GL_FRAMEBUFFER_OES,
+ GL11ExtensionPack.GL_COLOR_ATTACHMENT0_OES,
+ GL11.GL_TEXTURE_2D, texture.getId(), 0);
+
+ checkFramebufferStatus(gl11ep);
+ }
+ }
+
+ @Override
+ public void endRenderTarget() {
+ RawTexture texture = mTargetStack.remove(mTargetStack.size() - 1);
+ setRenderTarget(texture);
+ restore(); // restore matrix and alpha
+ }
+
+ @Override
+ public void beginRenderTarget(RawTexture texture) {
+ save(); // save matrix and alpha
+ mTargetStack.add(mTargetTexture);
+ setRenderTarget(texture);
+ }
+
+ private static void checkFramebufferStatus(GL11ExtensionPack gl11ep) {
+ int status = gl11ep.glCheckFramebufferStatusOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES);
+ if (status != GL11ExtensionPack.GL_FRAMEBUFFER_COMPLETE_OES) {
+ String msg = "";
+ switch (status) {
+ case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES:
+ msg = "FRAMEBUFFER_FORMATS";
+ break;
+ case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES:
+ msg = "FRAMEBUFFER_ATTACHMENT";
+ break;
+ case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES:
+ msg = "FRAMEBUFFER_MISSING_ATTACHMENT";
+ break;
+ case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_OES:
+ msg = "FRAMEBUFFER_DRAW_BUFFER";
+ break;
+ case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_OES:
+ msg = "FRAMEBUFFER_READ_BUFFER";
+ break;
+ case GL11ExtensionPack.GL_FRAMEBUFFER_UNSUPPORTED_OES:
+ msg = "FRAMEBUFFER_UNSUPPORTED";
+ break;
+ case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES:
+ msg = "FRAMEBUFFER_INCOMPLETE_DIMENSIONS";
+ break;
+ }
+ throw new RuntimeException(msg + ":" + Integer.toHexString(status));
+ }
+ }
+
+ @Override
+ public void setTextureParameters(BasicTexture texture) {
+ int width = texture.getWidth();
+ int height = texture.getHeight();
+ // Define a vertically flipped crop rectangle for OES_draw_texture.
+ // The four values in sCropRect are: left, bottom, width, and
+ // height. Negative value of width or height means flip.
+ sCropRect[0] = 0;
+ sCropRect[1] = height;
+ sCropRect[2] = width;
+ sCropRect[3] = -height;
+
+ // Set texture parameters.
+ int target = texture.getTarget();
+ mGL.glBindTexture(target, texture.getId());
+ mGL.glTexParameterfv(target, GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0);
+ mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
+ mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
+ mGL.glTexParameterf(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
+ mGL.glTexParameterf(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
+ }
+
+ @Override
+ public void initializeTextureSize(BasicTexture texture, int format, int type) {
+ int target = texture.getTarget();
+ mGL.glBindTexture(target, texture.getId());
+ int width = texture.getTextureWidth();
+ int height = texture.getTextureHeight();
+ mGL.glTexImage2D(target, 0, format, width, height, 0, format, type, null);
+ }
+
+ @Override
+ public void initializeTexture(BasicTexture texture, Bitmap bitmap) {
+ int target = texture.getTarget();
+ mGL.glBindTexture(target, texture.getId());
+ GLUtils.texImage2D(target, 0, bitmap, 0);
+ }
+
+ @Override
+ public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap,
+ int format, int type) {
+ int target = texture.getTarget();
+ mGL.glBindTexture(target, texture.getId());
+ GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type);
+ }
+
+ @Override
+ public int uploadBuffer(FloatBuffer buf) {
+ return uploadBuffer(buf, Float.SIZE / Byte.SIZE);
+ }
+
+ @Override
+ public int uploadBuffer(ByteBuffer buf) {
+ return uploadBuffer(buf, 1);
+ }
+
+ private int uploadBuffer(Buffer buf, int elementSize) {
+ int[] bufferIds = new int[1];
+ mGLId.glGenBuffers(bufferIds.length, bufferIds, 0);
+ int bufferId = bufferIds[0];
+ mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, bufferId);
+ mGL.glBufferData(GL11.GL_ARRAY_BUFFER, buf.capacity() * elementSize, buf,
+ GL11.GL_STATIC_DRAW);
+ return bufferId;
+ }
+
+ @Override
+ public void recoverFromLightCycle() {
+ // This is only required for GLES20
+ }
+
+ @Override
+ public void getBounds(Rect bounds, int x, int y, int width, int height) {
+ // This is only required for GLES20
+ }
+
+ @Override
+ public GLId getGLId() {
+ return mGLId;
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLES11IdImpl.java b/src/com/android/gallery3d/glrenderer/GLES11IdImpl.java
new file mode 100644
index 000000000..e4793730f
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLES11IdImpl.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012 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.glrenderer;
+
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
+
+/**
+ * Open GL ES 1.1 implementation for generating and destroying texture IDs and
+ * buffer IDs
+ */
+public class GLES11IdImpl implements GLId {
+ private static int sNextId = 1;
+ // Mutex for sNextId
+ private static Object sLock = new Object();
+
+ @Override
+ public int generateTexture() {
+ synchronized (sLock) {
+ return sNextId++;
+ }
+ }
+
+ @Override
+ public void glGenBuffers(int n, int[] buffers, int offset) {
+ synchronized (sLock) {
+ while (n-- > 0) {
+ buffers[offset + n] = sNextId++;
+ }
+ }
+ }
+
+ @Override
+ public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) {
+ synchronized (sLock) {
+ gl.glDeleteTextures(n, textures, offset);
+ }
+ }
+
+ @Override
+ public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) {
+ synchronized (sLock) {
+ gl.glDeleteBuffers(n, buffers, offset);
+ }
+ }
+
+ @Override
+ public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) {
+ synchronized (sLock) {
+ gl11ep.glDeleteFramebuffersOES(n, buffers, offset);
+ }
+ }
+
+
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLES20Canvas.java b/src/com/android/gallery3d/glrenderer/GLES20Canvas.java
new file mode 100644
index 000000000..4ead1315e
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLES20Canvas.java
@@ -0,0 +1,1009 @@
+/*
+ * Copyright (C) 2012 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.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.opengl.GLES20;
+import android.opengl.GLUtils;
+import android.opengl.Matrix;
+import android.util.Log;
+
+import com.android.gallery3d.util.IntArray;
+
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class GLES20Canvas implements GLCanvas {
+ // ************** Constants **********************
+ private static final String TAG = GLES20Canvas.class.getSimpleName();
+ private static final int FLOAT_SIZE = Float.SIZE / Byte.SIZE;
+ private static final float OPAQUE_ALPHA = 0.95f;
+
+ private static final int COORDS_PER_VERTEX = 2;
+ private static final int VERTEX_STRIDE = COORDS_PER_VERTEX * FLOAT_SIZE;
+
+ private static final int COUNT_FILL_VERTEX = 4;
+ private static final int COUNT_LINE_VERTEX = 2;
+ private static final int COUNT_RECT_VERTEX = 4;
+ private static final int OFFSET_FILL_RECT = 0;
+ private static final int OFFSET_DRAW_LINE = OFFSET_FILL_RECT + COUNT_FILL_VERTEX;
+ private static final int OFFSET_DRAW_RECT = OFFSET_DRAW_LINE + COUNT_LINE_VERTEX;
+
+ private static final float[] BOX_COORDINATES = {
+ 0, 0, // Fill rectangle
+ 1, 0,
+ 0, 1,
+ 1, 1,
+ 0, 0, // Draw line
+ 1, 1,
+ 0, 0, // Draw rectangle outline
+ 0, 1,
+ 1, 1,
+ 1, 0,
+ };
+
+ private static final float[] BOUNDS_COORDINATES = {
+ 0, 0, 0, 1,
+ 1, 1, 0, 1,
+ };
+
+ private static final String POSITION_ATTRIBUTE = "aPosition";
+ private static final String COLOR_UNIFORM = "uColor";
+ private static final String MATRIX_UNIFORM = "uMatrix";
+ private static final String TEXTURE_MATRIX_UNIFORM = "uTextureMatrix";
+ private static final String TEXTURE_SAMPLER_UNIFORM = "uTextureSampler";
+ private static final String ALPHA_UNIFORM = "uAlpha";
+ private static final String TEXTURE_COORD_ATTRIBUTE = "aTextureCoordinate";
+
+ private static final String DRAW_VERTEX_SHADER = ""
+ + "uniform mat4 " + MATRIX_UNIFORM + ";\n"
+ + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n"
+ + "void main() {\n"
+ + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n"
+ + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n"
+ + "}\n";
+
+ private static final String DRAW_FRAGMENT_SHADER = ""
+ + "precision mediump float;\n"
+ + "uniform vec4 " + COLOR_UNIFORM + ";\n"
+ + "void main() {\n"
+ + " gl_FragColor = " + COLOR_UNIFORM + ";\n"
+ + "}\n";
+
+ private static final String TEXTURE_VERTEX_SHADER = ""
+ + "uniform mat4 " + MATRIX_UNIFORM + ";\n"
+ + "uniform mat4 " + TEXTURE_MATRIX_UNIFORM + ";\n"
+ + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n"
+ + "varying vec2 vTextureCoord;\n"
+ + "void main() {\n"
+ + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n"
+ + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n"
+ + " vTextureCoord = (" + TEXTURE_MATRIX_UNIFORM + " * pos).xy;\n"
+ + "}\n";
+
+ private static final String MESH_VERTEX_SHADER = ""
+ + "uniform mat4 " + MATRIX_UNIFORM + ";\n"
+ + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n"
+ + "attribute vec2 " + TEXTURE_COORD_ATTRIBUTE + ";\n"
+ + "varying vec2 vTextureCoord;\n"
+ + "void main() {\n"
+ + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n"
+ + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n"
+ + " vTextureCoord = " + TEXTURE_COORD_ATTRIBUTE + ";\n"
+ + "}\n";
+
+ private static final String TEXTURE_FRAGMENT_SHADER = ""
+ + "precision mediump float;\n"
+ + "varying vec2 vTextureCoord;\n"
+ + "uniform float " + ALPHA_UNIFORM + ";\n"
+ + "uniform sampler2D " + TEXTURE_SAMPLER_UNIFORM + ";\n"
+ + "void main() {\n"
+ + " gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n"
+ + " gl_FragColor *= " + ALPHA_UNIFORM + ";\n"
+ + "}\n";
+
+ private static final String OES_TEXTURE_FRAGMENT_SHADER = ""
+ + "#extension GL_OES_EGL_image_external : require\n"
+ + "precision mediump float;\n"
+ + "varying vec2 vTextureCoord;\n"
+ + "uniform float " + ALPHA_UNIFORM + ";\n"
+ + "uniform samplerExternalOES " + TEXTURE_SAMPLER_UNIFORM + ";\n"
+ + "void main() {\n"
+ + " gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n"
+ + " gl_FragColor *= " + ALPHA_UNIFORM + ";\n"
+ + "}\n";
+
+ private static final int INITIAL_RESTORE_STATE_SIZE = 8;
+ private static final int MATRIX_SIZE = 16;
+
+ // Keep track of restore state
+ private float[] mMatrices = new float[INITIAL_RESTORE_STATE_SIZE * MATRIX_SIZE];
+ private float[] mAlphas = new float[INITIAL_RESTORE_STATE_SIZE];
+ private IntArray mSaveFlags = new IntArray();
+
+ private int mCurrentAlphaIndex = 0;
+ private int mCurrentMatrixIndex = 0;
+
+ // Viewport size
+ private int mWidth;
+ private int mHeight;
+
+ // Projection matrix
+ private float[] mProjectionMatrix = new float[MATRIX_SIZE];
+
+ // Screen size for when we aren't bound to a texture
+ private int mScreenWidth;
+ private int mScreenHeight;
+
+ // GL programs
+ private int mDrawProgram;
+ private int mTextureProgram;
+ private int mOesTextureProgram;
+ private int mMeshProgram;
+
+ // GL buffer containing BOX_COORDINATES
+ private int mBoxCoordinates;
+
+ // Handle indices -- common
+ private static final int INDEX_POSITION = 0;
+ private static final int INDEX_MATRIX = 1;
+
+ // Handle indices -- draw
+ private static final int INDEX_COLOR = 2;
+
+ // Handle indices -- texture
+ private static final int INDEX_TEXTURE_MATRIX = 2;
+ private static final int INDEX_TEXTURE_SAMPLER = 3;
+ private static final int INDEX_ALPHA = 4;
+
+ // Handle indices -- mesh
+ private static final int INDEX_TEXTURE_COORD = 2;
+
+ private abstract static class ShaderParameter {
+ public int handle;
+ protected final String mName;
+
+ public ShaderParameter(String name) {
+ mName = name;
+ }
+
+ public abstract void loadHandle(int program);
+ }
+
+ private static class UniformShaderParameter extends ShaderParameter {
+ public UniformShaderParameter(String name) {
+ super(name);
+ }
+
+ @Override
+ public void loadHandle(int program) {
+ handle = GLES20.glGetUniformLocation(program, mName);
+ checkError();
+ }
+ }
+
+ private static class AttributeShaderParameter extends ShaderParameter {
+ public AttributeShaderParameter(String name) {
+ super(name);
+ }
+
+ @Override
+ public void loadHandle(int program) {
+ handle = GLES20.glGetAttribLocation(program, mName);
+ checkError();
+ }
+ }
+
+ ShaderParameter[] mDrawParameters = {
+ new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION
+ new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX
+ new UniformShaderParameter(COLOR_UNIFORM), // INDEX_COLOR
+ };
+ ShaderParameter[] mTextureParameters = {
+ new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION
+ new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX
+ new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX
+ new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER
+ new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA
+ };
+ ShaderParameter[] mOesTextureParameters = {
+ new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION
+ new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX
+ new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX
+ new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER
+ new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA
+ };
+ ShaderParameter[] mMeshParameters = {
+ new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION
+ new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX
+ new AttributeShaderParameter(TEXTURE_COORD_ATTRIBUTE), // INDEX_TEXTURE_COORD
+ new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER
+ new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA
+ };
+
+ private final IntArray mUnboundTextures = new IntArray();
+ private final IntArray mDeleteBuffers = new IntArray();
+
+ // Keep track of statistics for debugging
+ private int mCountDrawMesh = 0;
+ private int mCountTextureRect = 0;
+ private int mCountFillRect = 0;
+ private int mCountDrawLine = 0;
+
+ // Buffer for framebuffer IDs -- we keep track so we can switch the attached
+ // texture.
+ private int[] mFrameBuffer = new int[1];
+
+ // Bound textures.
+ private ArrayList<RawTexture> mTargetTextures = new ArrayList<RawTexture>();
+
+ // Temporary variables used within calculations
+ private final float[] mTempMatrix = new float[32];
+ private final float[] mTempColor = new float[4];
+ private final RectF mTempSourceRect = new RectF();
+ private final RectF mTempTargetRect = new RectF();
+ private final float[] mTempTextureMatrix = new float[MATRIX_SIZE];
+ private final int[] mTempIntArray = new int[1];
+
+ private static final GLId mGLId = new GLES20IdImpl();
+
+ public GLES20Canvas() {
+ Matrix.setIdentityM(mTempTextureMatrix, 0);
+ Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex);
+ mAlphas[mCurrentAlphaIndex] = 1f;
+ mTargetTextures.add(null);
+
+ FloatBuffer boxBuffer = createBuffer(BOX_COORDINATES);
+ mBoxCoordinates = uploadBuffer(boxBuffer);
+
+ int drawVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DRAW_VERTEX_SHADER);
+ int textureVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, TEXTURE_VERTEX_SHADER);
+ int meshVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, MESH_VERTEX_SHADER);
+ int drawFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DRAW_FRAGMENT_SHADER);
+ int textureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, TEXTURE_FRAGMENT_SHADER);
+ int oesTextureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,
+ OES_TEXTURE_FRAGMENT_SHADER);
+
+ mDrawProgram = assembleProgram(drawVertexShader, drawFragmentShader, mDrawParameters);
+ mTextureProgram = assembleProgram(textureVertexShader, textureFragmentShader,
+ mTextureParameters);
+ mOesTextureProgram = assembleProgram(textureVertexShader, oesTextureFragmentShader,
+ mOesTextureParameters);
+ mMeshProgram = assembleProgram(meshVertexShader, textureFragmentShader, mMeshParameters);
+ GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+ checkError();
+ }
+
+ private static FloatBuffer createBuffer(float[] values) {
+ // First create an nio buffer, then create a VBO from it.
+ int size = values.length * FLOAT_SIZE;
+ FloatBuffer buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder())
+ .asFloatBuffer();
+ buffer.put(values, 0, values.length).position(0);
+ return buffer;
+ }
+
+ private int assembleProgram(int vertexShader, int fragmentShader, ShaderParameter[] params) {
+ int program = GLES20.glCreateProgram();
+ checkError();
+ if (program == 0) {
+ throw new RuntimeException("Cannot create GL program: " + GLES20.glGetError());
+ }
+ GLES20.glAttachShader(program, vertexShader);
+ checkError();
+ GLES20.glAttachShader(program, fragmentShader);
+ checkError();
+ GLES20.glLinkProgram(program);
+ checkError();
+ int[] mLinkStatus = mTempIntArray;
+ GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, mLinkStatus, 0);
+ if (mLinkStatus[0] != GLES20.GL_TRUE) {
+ Log.e(TAG, "Could not link program: ");
+ Log.e(TAG, GLES20.glGetProgramInfoLog(program));
+ GLES20.glDeleteProgram(program);
+ program = 0;
+ }
+ for (int i = 0; i < params.length; i++) {
+ params[i].loadHandle(program);
+ }
+ return program;
+ }
+
+ private static int loadShader(int type, String shaderCode) {
+ // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
+ // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
+ int shader = GLES20.glCreateShader(type);
+
+ // add the source code to the shader and compile it
+ GLES20.glShaderSource(shader, shaderCode);
+ checkError();
+ GLES20.glCompileShader(shader);
+ checkError();
+
+ return shader;
+ }
+
+ @Override
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ GLES20.glViewport(0, 0, mWidth, mHeight);
+ checkError();
+ Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex);
+ Matrix.orthoM(mProjectionMatrix, 0, 0, width, 0, height, -1, 1);
+ if (getTargetTexture() == null) {
+ mScreenWidth = width;
+ mScreenHeight = height;
+ Matrix.translateM(mMatrices, mCurrentMatrixIndex, 0, height, 0);
+ Matrix.scaleM(mMatrices, mCurrentMatrixIndex, 1, -1, 1);
+ }
+ }
+
+ @Override
+ public void clearBuffer() {
+ GLES20.glClearColor(0f, 0f, 0f, 1f);
+ checkError();
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ checkError();
+ }
+
+ @Override
+ public void clearBuffer(float[] argb) {
+ GLES20.glClearColor(argb[1], argb[2], argb[3], argb[0]);
+ checkError();
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
+ checkError();
+ }
+
+ @Override
+ public float getAlpha() {
+ return mAlphas[mCurrentAlphaIndex];
+ }
+
+ @Override
+ public void setAlpha(float alpha) {
+ mAlphas[mCurrentAlphaIndex] = alpha;
+ }
+
+ @Override
+ public void multiplyAlpha(float alpha) {
+ setAlpha(getAlpha() * alpha);
+ }
+
+ @Override
+ public void translate(float x, float y, float z) {
+ Matrix.translateM(mMatrices, mCurrentMatrixIndex, x, y, z);
+ }
+
+ // This is a faster version of translate(x, y, z) because
+ // (1) we knows z = 0, (2) we inline the Matrix.translateM call,
+ // (3) we unroll the loop
+ @Override
+ public void translate(float x, float y) {
+ int index = mCurrentMatrixIndex;
+ float[] m = mMatrices;
+ m[index + 12] += m[index + 0] * x + m[index + 4] * y;
+ m[index + 13] += m[index + 1] * x + m[index + 5] * y;
+ m[index + 14] += m[index + 2] * x + m[index + 6] * y;
+ m[index + 15] += m[index + 3] * x + m[index + 7] * y;
+ }
+
+ @Override
+ public void scale(float sx, float sy, float sz) {
+ Matrix.scaleM(mMatrices, mCurrentMatrixIndex, sx, sy, sz);
+ }
+
+ @Override
+ public void rotate(float angle, float x, float y, float z) {
+ if (angle == 0f) {
+ return;
+ }
+ float[] temp = mTempMatrix;
+ Matrix.setRotateM(temp, 0, angle, x, y, z);
+ float[] matrix = mMatrices;
+ int index = mCurrentMatrixIndex;
+ Matrix.multiplyMM(temp, MATRIX_SIZE, matrix, index, temp, 0);
+ System.arraycopy(temp, MATRIX_SIZE, matrix, index, MATRIX_SIZE);
+ }
+
+ @Override
+ public void multiplyMatrix(float[] matrix, int offset) {
+ float[] temp = mTempMatrix;
+ float[] currentMatrix = mMatrices;
+ int index = mCurrentMatrixIndex;
+ Matrix.multiplyMM(temp, 0, currentMatrix, index, matrix, offset);
+ System.arraycopy(temp, 0, currentMatrix, index, 16);
+ }
+
+ @Override
+ public void save() {
+ save(SAVE_FLAG_ALL);
+ }
+
+ @Override
+ public void save(int saveFlags) {
+ boolean saveAlpha = (saveFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA;
+ if (saveAlpha) {
+ float currentAlpha = getAlpha();
+ mCurrentAlphaIndex++;
+ if (mAlphas.length <= mCurrentAlphaIndex) {
+ mAlphas = Arrays.copyOf(mAlphas, mAlphas.length * 2);
+ }
+ mAlphas[mCurrentAlphaIndex] = currentAlpha;
+ }
+ boolean saveMatrix = (saveFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX;
+ if (saveMatrix) {
+ int currentIndex = mCurrentMatrixIndex;
+ mCurrentMatrixIndex += MATRIX_SIZE;
+ if (mMatrices.length <= mCurrentMatrixIndex) {
+ mMatrices = Arrays.copyOf(mMatrices, mMatrices.length * 2);
+ }
+ System.arraycopy(mMatrices, currentIndex, mMatrices, mCurrentMatrixIndex, MATRIX_SIZE);
+ }
+ mSaveFlags.add(saveFlags);
+ }
+
+ @Override
+ public void restore() {
+ int restoreFlags = mSaveFlags.removeLast();
+ boolean restoreAlpha = (restoreFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA;
+ if (restoreAlpha) {
+ mCurrentAlphaIndex--;
+ }
+ boolean restoreMatrix = (restoreFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX;
+ if (restoreMatrix) {
+ mCurrentMatrixIndex -= MATRIX_SIZE;
+ }
+ }
+
+ @Override
+ public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) {
+ draw(GLES20.GL_LINE_STRIP, OFFSET_DRAW_LINE, COUNT_LINE_VERTEX, x1, y1, x2 - x1, y2 - y1,
+ paint);
+ mCountDrawLine++;
+ }
+
+ @Override
+ public void drawRect(float x, float y, float width, float height, GLPaint paint) {
+ draw(GLES20.GL_LINE_LOOP, OFFSET_DRAW_RECT, COUNT_RECT_VERTEX, x, y, width, height, paint);
+ mCountDrawLine++;
+ }
+
+ private void draw(int type, int offset, int count, float x, float y, float width, float height,
+ GLPaint paint) {
+ draw(type, offset, count, x, y, width, height, paint.getColor(), paint.getLineWidth());
+ }
+
+ private void draw(int type, int offset, int count, float x, float y, float width, float height,
+ int color, float lineWidth) {
+ prepareDraw(offset, color, lineWidth);
+ draw(mDrawParameters, type, count, x, y, width, height);
+ }
+
+ private void prepareDraw(int offset, int color, float lineWidth) {
+ GLES20.glUseProgram(mDrawProgram);
+ checkError();
+ if (lineWidth > 0) {
+ GLES20.glLineWidth(lineWidth);
+ checkError();
+ }
+ float[] colorArray = getColor(color);
+ boolean blendingEnabled = (colorArray[3] < 1f);
+ enableBlending(blendingEnabled);
+ if (blendingEnabled) {
+ GLES20.glBlendColor(colorArray[0], colorArray[1], colorArray[2], colorArray[3]);
+ checkError();
+ }
+
+ GLES20.glUniform4fv(mDrawParameters[INDEX_COLOR].handle, 1, colorArray, 0);
+ setPosition(mDrawParameters, offset);
+ checkError();
+ }
+
+ private float[] getColor(int color) {
+ float alpha = ((color >>> 24) & 0xFF) / 255f * getAlpha();
+ float red = ((color >>> 16) & 0xFF) / 255f * alpha;
+ float green = ((color >>> 8) & 0xFF) / 255f * alpha;
+ float blue = (color & 0xFF) / 255f * alpha;
+ mTempColor[0] = red;
+ mTempColor[1] = green;
+ mTempColor[2] = blue;
+ mTempColor[3] = alpha;
+ return mTempColor;
+ }
+
+ private void enableBlending(boolean enableBlending) {
+ if (enableBlending) {
+ GLES20.glEnable(GLES20.GL_BLEND);
+ checkError();
+ } else {
+ GLES20.glDisable(GLES20.GL_BLEND);
+ checkError();
+ }
+ }
+
+ private void setPosition(ShaderParameter[] params, int offset) {
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mBoxCoordinates);
+ checkError();
+ GLES20.glVertexAttribPointer(params[INDEX_POSITION].handle, COORDS_PER_VERTEX,
+ GLES20.GL_FLOAT, false, VERTEX_STRIDE, offset * VERTEX_STRIDE);
+ checkError();
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+ checkError();
+ }
+
+ private void draw(ShaderParameter[] params, int type, int count, float x, float y, float width,
+ float height) {
+ setMatrix(params, x, y, width, height);
+ int positionHandle = params[INDEX_POSITION].handle;
+ GLES20.glEnableVertexAttribArray(positionHandle);
+ checkError();
+ GLES20.glDrawArrays(type, 0, count);
+ checkError();
+ GLES20.glDisableVertexAttribArray(positionHandle);
+ checkError();
+ }
+
+ private void setMatrix(ShaderParameter[] params, float x, float y, float width, float height) {
+ Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f);
+ Matrix.scaleM(mTempMatrix, 0, width, height, 1f);
+ Matrix.multiplyMM(mTempMatrix, MATRIX_SIZE, mProjectionMatrix, 0, mTempMatrix, 0);
+ GLES20.glUniformMatrix4fv(params[INDEX_MATRIX].handle, 1, false, mTempMatrix, MATRIX_SIZE);
+ checkError();
+ }
+
+ @Override
+ public void fillRect(float x, float y, float width, float height, int color) {
+ draw(GLES20.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, COUNT_FILL_VERTEX, x, y, width, height,
+ color, 0f);
+ mCountFillRect++;
+ }
+
+ @Override
+ public void drawTexture(BasicTexture texture, int x, int y, int width, int height) {
+ if (width <= 0 || height <= 0) {
+ return;
+ }
+ copyTextureCoordinates(texture, mTempSourceRect);
+ mTempTargetRect.set(x, y, x + width, y + height);
+ convertCoordinate(mTempSourceRect, mTempTargetRect, texture);
+ drawTextureRect(texture, mTempSourceRect, mTempTargetRect);
+ }
+
+ private static void copyTextureCoordinates(BasicTexture texture, RectF outRect) {
+ int left = 0;
+ int top = 0;
+ int right = texture.getWidth();
+ int bottom = texture.getHeight();
+ if (texture.hasBorder()) {
+ left = 1;
+ top = 1;
+ right -= 1;
+ bottom -= 1;
+ }
+ outRect.set(left, top, right, bottom);
+ }
+
+ @Override
+ public void drawTexture(BasicTexture texture, RectF source, RectF target) {
+ if (target.width() <= 0 || target.height() <= 0) {
+ return;
+ }
+ mTempSourceRect.set(source);
+ mTempTargetRect.set(target);
+
+ convertCoordinate(mTempSourceRect, mTempTargetRect, texture);
+ drawTextureRect(texture, mTempSourceRect, mTempTargetRect);
+ }
+
+ @Override
+ public void drawTexture(BasicTexture texture, float[] textureTransform, int x, int y, int w,
+ int h) {
+ if (w <= 0 || h <= 0) {
+ return;
+ }
+ mTempTargetRect.set(x, y, x + w, y + h);
+ drawTextureRect(texture, textureTransform, mTempTargetRect);
+ }
+
+ private void drawTextureRect(BasicTexture texture, RectF source, RectF target) {
+ setTextureMatrix(source);
+ drawTextureRect(texture, mTempTextureMatrix, target);
+ }
+
+ private void setTextureMatrix(RectF source) {
+ mTempTextureMatrix[0] = source.width();
+ mTempTextureMatrix[5] = source.height();
+ mTempTextureMatrix[12] = source.left;
+ mTempTextureMatrix[13] = source.top;
+ }
+
+ // 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 static 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;
+ }
+ }
+
+ private void drawTextureRect(BasicTexture texture, float[] textureMatrix, RectF target) {
+ ShaderParameter[] params = prepareTexture(texture);
+ setPosition(params, OFFSET_FILL_RECT);
+ GLES20.glUniformMatrix4fv(params[INDEX_TEXTURE_MATRIX].handle, 1, false, textureMatrix, 0);
+ checkError();
+ if (texture.isFlippedVertically()) {
+ save(SAVE_FLAG_MATRIX);
+ translate(0, target.centerY());
+ scale(1, -1, 1);
+ translate(0, -target.centerY());
+ }
+ draw(params, GLES20.GL_TRIANGLE_STRIP, COUNT_FILL_VERTEX, target.left, target.top,
+ target.width(), target.height());
+ if (texture.isFlippedVertically()) {
+ restore();
+ }
+ mCountTextureRect++;
+ }
+
+ private ShaderParameter[] prepareTexture(BasicTexture texture) {
+ ShaderParameter[] params;
+ int program;
+ if (texture.getTarget() == GLES20.GL_TEXTURE_2D) {
+ params = mTextureParameters;
+ program = mTextureProgram;
+ } else {
+ params = mOesTextureParameters;
+ program = mOesTextureProgram;
+ }
+ prepareTexture(texture, program, params);
+ return params;
+ }
+
+ private void prepareTexture(BasicTexture texture, int program, ShaderParameter[] params) {
+ GLES20.glUseProgram(program);
+ checkError();
+ enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA);
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ checkError();
+ texture.onBind(this);
+ GLES20.glBindTexture(texture.getTarget(), texture.getId());
+ checkError();
+ GLES20.glUniform1i(params[INDEX_TEXTURE_SAMPLER].handle, 0);
+ checkError();
+ GLES20.glUniform1f(params[INDEX_ALPHA].handle, getAlpha());
+ checkError();
+ }
+
+ @Override
+ public void drawMesh(BasicTexture texture, int x, int y, int xyBuffer, int uvBuffer,
+ int indexBuffer, int indexCount) {
+ prepareTexture(texture, mMeshProgram, mMeshParameters);
+
+ GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
+ checkError();
+
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, xyBuffer);
+ checkError();
+ int positionHandle = mMeshParameters[INDEX_POSITION].handle;
+ GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false,
+ VERTEX_STRIDE, 0);
+ checkError();
+
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, uvBuffer);
+ checkError();
+ int texCoordHandle = mMeshParameters[INDEX_TEXTURE_COORD].handle;
+ GLES20.glVertexAttribPointer(texCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
+ false, VERTEX_STRIDE, 0);
+ checkError();
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+ checkError();
+
+ GLES20.glEnableVertexAttribArray(positionHandle);
+ checkError();
+ GLES20.glEnableVertexAttribArray(texCoordHandle);
+ checkError();
+
+ setMatrix(mMeshParameters, x, y, 1, 1);
+ GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, indexCount, GLES20.GL_UNSIGNED_BYTE, 0);
+ checkError();
+
+ GLES20.glDisableVertexAttribArray(positionHandle);
+ checkError();
+ GLES20.glDisableVertexAttribArray(texCoordHandle);
+ checkError();
+ GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
+ checkError();
+ mCountDrawMesh++;
+ }
+
+ @Override
+ public void drawMixed(BasicTexture texture, int toColor, float ratio, int x, int y, int w, int h) {
+ copyTextureCoordinates(texture, mTempSourceRect);
+ mTempTargetRect.set(x, y, x + w, y + h);
+ drawMixed(texture, toColor, ratio, mTempSourceRect, mTempTargetRect);
+ }
+
+ @Override
+ public void drawMixed(BasicTexture texture, int toColor, float ratio, RectF source, RectF target) {
+ if (target.width() <= 0 || target.height() <= 0) {
+ return;
+ }
+ save(SAVE_FLAG_ALPHA);
+
+ float currentAlpha = getAlpha();
+ float cappedRatio = Math.min(1f, Math.max(0f, ratio));
+
+ float textureAlpha = (1f - cappedRatio) * currentAlpha;
+ setAlpha(textureAlpha);
+ drawTexture(texture, source, target);
+
+ float colorAlpha = cappedRatio * currentAlpha;
+ setAlpha(colorAlpha);
+ fillRect(target.left, target.top, target.width(), target.height(), toColor);
+
+ restore();
+ }
+
+ @Override
+ public boolean unloadTexture(BasicTexture texture) {
+ boolean unload = texture.isLoaded();
+ if (unload) {
+ synchronized (mUnboundTextures) {
+ mUnboundTextures.add(texture.getId());
+ }
+ }
+ return unload;
+ }
+
+ @Override
+ public void deleteBuffer(int bufferId) {
+ synchronized (mUnboundTextures) {
+ mDeleteBuffers.add(bufferId);
+ }
+ }
+
+ @Override
+ public void deleteRecycledResources() {
+ synchronized (mUnboundTextures) {
+ IntArray ids = mUnboundTextures;
+ if (mUnboundTextures.size() > 0) {
+ mGLId.glDeleteTextures(null, ids.size(), ids.getInternalArray(), 0);
+ ids.clear();
+ }
+
+ ids = mDeleteBuffers;
+ if (ids.size() > 0) {
+ mGLId.glDeleteBuffers(null, ids.size(), ids.getInternalArray(), 0);
+ ids.clear();
+ }
+ }
+ }
+
+ @Override
+ public void dumpStatisticsAndClear() {
+ String line = String.format("MESH:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", mCountDrawMesh,
+ mCountTextureRect, mCountFillRect, mCountDrawLine);
+ mCountDrawMesh = 0;
+ mCountTextureRect = 0;
+ mCountFillRect = 0;
+ mCountDrawLine = 0;
+ Log.d(TAG, line);
+ }
+
+ @Override
+ public void endRenderTarget() {
+ RawTexture oldTexture = mTargetTextures.remove(mTargetTextures.size() - 1);
+ RawTexture texture = getTargetTexture();
+ setRenderTarget(oldTexture, texture);
+ restore(); // restore matrix and alpha
+ }
+
+ @Override
+ public void beginRenderTarget(RawTexture texture) {
+ save(); // save matrix and alpha and blending
+ RawTexture oldTexture = getTargetTexture();
+ mTargetTextures.add(texture);
+ setRenderTarget(oldTexture, texture);
+ }
+
+ private RawTexture getTargetTexture() {
+ return mTargetTextures.get(mTargetTextures.size() - 1);
+ }
+
+ private void setRenderTarget(BasicTexture oldTexture, RawTexture texture) {
+ if (oldTexture == null && texture != null) {
+ GLES20.glGenFramebuffers(1, mFrameBuffer, 0);
+ checkError();
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer[0]);
+ checkError();
+ } else if (oldTexture != null && texture == null) {
+ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
+ checkError();
+ GLES20.glDeleteFramebuffers(1, mFrameBuffer, 0);
+ checkError();
+ }
+
+ if (texture == null) {
+ setSize(mScreenWidth, mScreenHeight);
+ } else {
+ setSize(texture.getWidth(), texture.getHeight());
+
+ if (!texture.isLoaded()) {
+ texture.prepare(this);
+ }
+
+ GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
+ texture.getTarget(), texture.getId(), 0);
+ checkError();
+
+ checkFramebufferStatus();
+ }
+ }
+
+ private static void checkFramebufferStatus() {
+ int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
+ if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
+ String msg = "";
+ switch (status) {
+ case GLES20.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
+ msg = "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT";
+ break;
+ case GLES20.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
+ msg = "GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS";
+ break;
+ case GLES20.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
+ msg = "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT";
+ break;
+ case GLES20.GL_FRAMEBUFFER_UNSUPPORTED:
+ msg = "GL_FRAMEBUFFER_UNSUPPORTED";
+ break;
+ }
+ throw new RuntimeException(msg + ":" + Integer.toHexString(status));
+ }
+ }
+
+ @Override
+ public void setTextureParameters(BasicTexture texture) {
+ int target = texture.getTarget();
+ GLES20.glBindTexture(target, texture.getId());
+ checkError();
+ GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
+ GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
+ }
+
+ @Override
+ public void initializeTextureSize(BasicTexture texture, int format, int type) {
+ int target = texture.getTarget();
+ GLES20.glBindTexture(target, texture.getId());
+ checkError();
+ int width = texture.getTextureWidth();
+ int height = texture.getTextureHeight();
+ GLES20.glTexImage2D(target, 0, format, width, height, 0, format, type, null);
+ }
+
+ @Override
+ public void initializeTexture(BasicTexture texture, Bitmap bitmap) {
+ int target = texture.getTarget();
+ GLES20.glBindTexture(target, texture.getId());
+ checkError();
+ GLUtils.texImage2D(target, 0, bitmap, 0);
+ }
+
+ @Override
+ public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap,
+ int format, int type) {
+ int target = texture.getTarget();
+ GLES20.glBindTexture(target, texture.getId());
+ checkError();
+ GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type);
+ }
+
+ @Override
+ public int uploadBuffer(FloatBuffer buf) {
+ return uploadBuffer(buf, FLOAT_SIZE);
+ }
+
+ @Override
+ public int uploadBuffer(ByteBuffer buf) {
+ return uploadBuffer(buf, 1);
+ }
+
+ private int uploadBuffer(Buffer buffer, int elementSize) {
+ mGLId.glGenBuffers(1, mTempIntArray, 0);
+ checkError();
+ int bufferId = mTempIntArray[0];
+ GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferId);
+ checkError();
+ GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, buffer.capacity() * elementSize, buffer,
+ GLES20.GL_STATIC_DRAW);
+ checkError();
+ return bufferId;
+ }
+
+ public static void checkError() {
+ int error = GLES20.glGetError();
+ if (error != 0) {
+ Throwable t = new Throwable();
+ Log.e(TAG, "GL error: " + error, t);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static void printMatrix(String message, float[] m, int offset) {
+ StringBuilder b = new StringBuilder(message);
+ for (int i = 0; i < MATRIX_SIZE; i++) {
+ b.append(' ');
+ if (i % 4 == 0) {
+ b.append('\n');
+ }
+ b.append(m[offset + i]);
+ }
+ Log.v(TAG, b.toString());
+ }
+
+ @Override
+ public void recoverFromLightCycle() {
+ GLES20.glViewport(0, 0, mWidth, mHeight);
+ GLES20.glDisable(GLES20.GL_DEPTH_TEST);
+ GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+ checkError();
+ }
+
+ @Override
+ public void getBounds(Rect bounds, int x, int y, int width, int height) {
+ Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f);
+ Matrix.scaleM(mTempMatrix, 0, width, height, 1f);
+ Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE, mTempMatrix, 0, BOUNDS_COORDINATES, 0);
+ Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE + 4, mTempMatrix, 0, BOUNDS_COORDINATES, 4);
+ bounds.left = Math.round(mTempMatrix[MATRIX_SIZE]);
+ bounds.right = Math.round(mTempMatrix[MATRIX_SIZE + 4]);
+ bounds.top = Math.round(mTempMatrix[MATRIX_SIZE + 1]);
+ bounds.bottom = Math.round(mTempMatrix[MATRIX_SIZE + 5]);
+ bounds.sort();
+ }
+
+ @Override
+ public GLId getGLId() {
+ return mGLId;
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLES20IdImpl.java b/src/com/android/gallery3d/glrenderer/GLES20IdImpl.java
new file mode 100644
index 000000000..6cd7149cb
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLES20IdImpl.java
@@ -0,0 +1,42 @@
+package com.android.gallery3d.glrenderer;
+
+import android.opengl.GLES20;
+
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
+
+public class GLES20IdImpl implements GLId {
+ private final int[] mTempIntArray = new int[1];
+
+ @Override
+ public int generateTexture() {
+ GLES20.glGenTextures(1, mTempIntArray, 0);
+ GLES20Canvas.checkError();
+ return mTempIntArray[0];
+ }
+
+ @Override
+ public void glGenBuffers(int n, int[] buffers, int offset) {
+ GLES20.glGenBuffers(n, buffers, offset);
+ GLES20Canvas.checkError();
+ }
+
+ @Override
+ public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) {
+ GLES20.glDeleteTextures(n, textures, offset);
+ GLES20Canvas.checkError();
+ }
+
+
+ @Override
+ public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) {
+ GLES20.glDeleteBuffers(n, buffers, offset);
+ GLES20Canvas.checkError();
+ }
+
+ @Override
+ public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) {
+ GLES20.glDeleteFramebuffers(n, buffers, offset);
+ GLES20Canvas.checkError();
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLId.java b/src/com/android/gallery3d/glrenderer/GLId.java
new file mode 100644
index 000000000..3cec558f6
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLId.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2012 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.glrenderer;
+
+import javax.microedition.khronos.opengles.GL11;
+import javax.microedition.khronos.opengles.GL11ExtensionPack;
+
+// This mimics corresponding GL functions.
+public interface GLId {
+ public int generateTexture();
+
+ public void glGenBuffers(int n, int[] buffers, int offset);
+
+ public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset);
+
+ public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset);
+
+ public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset);
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLPaint.java b/src/com/android/gallery3d/glrenderer/GLPaint.java
new file mode 100644
index 000000000..16b220690
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLPaint.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.glrenderer;
+
+import junit.framework.Assert;
+
+public class GLPaint {
+ private float mLineWidth = 1f;
+ private int mColor = 0;
+
+ public void setColor(int color) {
+ mColor = color;
+ }
+
+ public int getColor() {
+ return mColor;
+ }
+
+ public void setLineWidth(float width) {
+ Assert.assertTrue(width >= 0);
+ mLineWidth = width;
+ }
+
+ public float getLineWidth() {
+ return mLineWidth;
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/MultiLineTexture.java b/src/com/android/gallery3d/glrenderer/MultiLineTexture.java
new file mode 100644
index 000000000..82839f107
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/MultiLineTexture.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.glrenderer;
+
+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,
+ Layout.Alignment alignment) {
+ TextPaint paint = StringTexture.getDefaultPaint(textSize, color);
+ Layout layout = new StaticLayout(text, 0, text.length(), paint,
+ maxWidth, alignment, 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/glrenderer/NinePatchChunk.java b/src/com/android/gallery3d/glrenderer/NinePatchChunk.java
new file mode 100644
index 000000000..9dc326622
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/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.glrenderer;
+
+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/glrenderer/NinePatchTexture.java b/src/com/android/gallery3d/glrenderer/NinePatchTexture.java
new file mode 100644
index 000000000..d0ddc46c3
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/NinePatchTexture.java
@@ -0,0 +1,424 @@
+/*
+ * 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.glrenderer;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+
+import com.android.gallery3d.common.Utils;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+// 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 SmallCache<NinePatchInstance> mInstanceCache
+ = new SmallCache<NinePatchInstance>();
+
+ 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;
+ }
+
+ // This is a simple cache for a small number of things. Linear search
+ // is used because the cache is small. It also tries to remove less used
+ // item when the cache is full by moving the often-used items to the front.
+ private static class SmallCache<V> {
+ private static final int CACHE_SIZE = 16;
+ private static final int CACHE_SIZE_START_MOVE = CACHE_SIZE / 2;
+ private int[] mKey = new int[CACHE_SIZE];
+ private V[] mValue = (V[]) new Object[CACHE_SIZE];
+ private int mCount; // number of items in this cache
+
+ // Puts a value into the cache. If the cache is full, also returns
+ // a less used item, otherwise returns null.
+ public V put(int key, V value) {
+ if (mCount == CACHE_SIZE) {
+ V old = mValue[CACHE_SIZE - 1]; // remove the last item
+ mKey[CACHE_SIZE - 1] = key;
+ mValue[CACHE_SIZE - 1] = value;
+ return old;
+ } else {
+ mKey[mCount] = key;
+ mValue[mCount] = value;
+ mCount++;
+ return null;
+ }
+ }
+
+ public V get(int key) {
+ for (int i = 0; i < mCount; i++) {
+ if (mKey[i] == key) {
+ // Move the accessed item one position to the front, so it
+ // will less likely to be removed when cache is full. Only
+ // do this if the cache is starting to get full.
+ if (mCount > CACHE_SIZE_START_MOVE && i > 0) {
+ int tmpKey = mKey[i];
+ mKey[i] = mKey[i - 1];
+ mKey[i - 1] = tmpKey;
+
+ V tmpValue = mValue[i];
+ mValue[i] = mValue[i - 1];
+ mValue[i - 1] = tmpValue;
+ }
+ return mValue[i];
+ }
+ }
+ return null;
+ }
+
+ public void clear() {
+ for (int i = 0; i < mCount; i++) {
+ mValue[i] = null; // make sure it's can be garbage-collected.
+ }
+ mCount = 0;
+ }
+
+ public int size() {
+ return mCount;
+ }
+
+ public V valueAt(int i) {
+ return mValue[i];
+ }
+ }
+
+ private NinePatchInstance findInstance(GLCanvas canvas, int w, int h) {
+ int key = w;
+ key = (key << 16) | h;
+ NinePatchInstance instance = mInstanceCache.get(key);
+
+ if (instance == null) {
+ instance = new NinePatchInstance(this, w, h);
+ NinePatchInstance removed = mInstanceCache.put(key, instance);
+ if (removed != null) {
+ removed.recycle(canvas);
+ }
+ }
+
+ return instance;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int w, int h) {
+ if (!isLoaded()) {
+ 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;
+ if (canvas == null) return;
+ int n = mInstanceCache.size();
+ for (int i = 0; i < n; i++) {
+ NinePatchInstance instance = mInstanceCache.valueAt(i);
+ 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 mXyBufferName = -1;
+ private int mUvBufferName;
+ private int mIndexBufferName;
+
+ 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.
+ *
+ * <pre>
+ * 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
+ * </pre>
+ *
+ * @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) {
+ mXyBufferName = canvas.uploadBuffer(mXyBuffer);
+ mUvBufferName = canvas.uploadBuffer(mUvBuffer);
+ mIndexBufferName = canvas.uploadBuffer(mIndexBuffer);
+
+ // These buffers are never used again.
+ mXyBuffer = null;
+ mUvBuffer = null;
+ mIndexBuffer = null;
+ }
+
+ public void draw(GLCanvas canvas, NinePatchTexture tex, int x, int y) {
+ if (mXyBufferName == -1) {
+ prepareBuffers(canvas);
+ }
+ canvas.drawMesh(tex, x, y, mXyBufferName, mUvBufferName, mIndexBufferName, mIdxCount);
+ }
+
+ public void recycle(GLCanvas canvas) {
+ if (mXyBuffer == null) {
+ canvas.deleteBuffer(mXyBufferName);
+ canvas.deleteBuffer(mUvBufferName);
+ canvas.deleteBuffer(mIndexBufferName);
+ mXyBufferName = -1;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/RawTexture.java b/src/com/android/gallery3d/glrenderer/RawTexture.java
new file mode 100644
index 000000000..93f0fdff9
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/RawTexture.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.glrenderer;
+
+import android.util.Log;
+
+import javax.microedition.khronos.opengles.GL11;
+
+public class RawTexture extends BasicTexture {
+ private static final String TAG = "RawTexture";
+
+ private final boolean mOpaque;
+ private boolean mIsFlipped;
+
+ public RawTexture(int width, int height, boolean opaque) {
+ mOpaque = opaque;
+ setSize(width, height);
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return mOpaque;
+ }
+
+ @Override
+ public boolean isFlippedVertically() {
+ return mIsFlipped;
+ }
+
+ public void setIsFlippedVertically(boolean isFlipped) {
+ mIsFlipped = isFlipped;
+ }
+
+ protected void prepare(GLCanvas canvas) {
+ GLId glId = canvas.getGLId();
+ mId = glId.generateTexture();
+ canvas.initializeTextureSize(this, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE);
+ canvas.setTextureParameters(this);
+ mState = STATE_LOADED;
+ setAssociatedCanvas(canvas);
+ }
+
+ @Override
+ protected boolean onBind(GLCanvas canvas) {
+ if (isLoaded()) return true;
+ Log.w(TAG, "lost the content due to context change");
+ return false;
+ }
+
+ @Override
+ public void yield() {
+ // we cannot free the texture because we have no backup.
+ }
+
+ @Override
+ protected int getTarget() {
+ return GL11.GL_TEXTURE_2D;
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/ResourceTexture.java b/src/com/android/gallery3d/glrenderer/ResourceTexture.java
new file mode 100644
index 000000000..eb8e8a517
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/ResourceTexture.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.glrenderer;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import junit.framework.Assert;
+
+// 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) {
+ Assert.assertNotNull(context);
+ mContext = 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/glrenderer/StringTexture.java b/src/com/android/gallery3d/glrenderer/StringTexture.java
new file mode 100644
index 000000000..56ca29753
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/StringTexture.java
@@ -0,0 +1,88 @@
+/*
+ * 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.glrenderer;
+
+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;
+import android.util.FloatMath;
+
+// 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.
+public 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, float textSize, int color,
+ float lengthLimit, boolean isBold) {
+ TextPaint paint = getDefaultPaint(textSize, color);
+ if (isBold) {
+ paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
+ }
+ if (lengthLimit > 0) {
+ 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) FloatMath.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/glrenderer/Texture.java b/src/com/android/gallery3d/glrenderer/Texture.java
new file mode 100644
index 000000000..3dcae4aec
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/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.glrenderer;
+
+
+// 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
+// -- FadeInTexture
+// -- BasicTexture
+// -- UploadedTexture
+// -- BitmapTexture
+// -- Tile
+// -- ResourceTexture
+// -- NinePatchTexture
+// -- CanvasTexture
+// -- 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/glrenderer/TextureUploader.java b/src/com/android/gallery3d/glrenderer/TextureUploader.java
new file mode 100644
index 000000000..f17ab845c
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/TextureUploader.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2012 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.glrenderer;
+
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
+
+import java.util.ArrayDeque;
+
+public class TextureUploader implements OnGLIdleListener {
+ private static final int INIT_CAPACITY = 64;
+ private static final int QUOTA_PER_FRAME = 1;
+
+ private final ArrayDeque<UploadedTexture> mFgTextures =
+ new ArrayDeque<UploadedTexture>(INIT_CAPACITY);
+ private final ArrayDeque<UploadedTexture> mBgTextures =
+ new ArrayDeque<UploadedTexture>(INIT_CAPACITY);
+ private final GLRoot mGLRoot;
+ private volatile boolean mIsQueued = false;
+
+ public TextureUploader(GLRoot root) {
+ mGLRoot = root;
+ }
+
+ public synchronized void clear() {
+ while (!mFgTextures.isEmpty()) {
+ mFgTextures.pop().setIsUploading(false);
+ }
+ while (!mBgTextures.isEmpty()) {
+ mBgTextures.pop().setIsUploading(false);
+ }
+ }
+
+ // caller should hold synchronized on "this"
+ private void queueSelfIfNeed() {
+ if (mIsQueued) return;
+ mIsQueued = true;
+ mGLRoot.addOnGLIdleListener(this);
+ }
+
+ public synchronized void addBgTexture(UploadedTexture t) {
+ if (t.isContentValid()) return;
+ mBgTextures.addLast(t);
+ t.setIsUploading(true);
+ queueSelfIfNeed();
+ }
+
+ public synchronized void addFgTexture(UploadedTexture t) {
+ if (t.isContentValid()) return;
+ mFgTextures.addLast(t);
+ t.setIsUploading(true);
+ queueSelfIfNeed();
+ }
+
+ private int upload(GLCanvas canvas, ArrayDeque<UploadedTexture> deque,
+ int uploadQuota, boolean isBackground) {
+ while (uploadQuota > 0) {
+ UploadedTexture t;
+ synchronized (this) {
+ if (deque.isEmpty()) break;
+ t = deque.removeFirst();
+ t.setIsUploading(false);
+ if (t.isContentValid()) continue;
+
+ // this has to be protected by the synchronized block
+ // to prevent the inner bitmap get recycled
+ t.updateContent(canvas);
+ }
+
+ // It will took some more time for a texture to be drawn for
+ // the first time.
+ // Thus, when scrolling, if a new column appears on screen,
+ // it may cause a UI jank even these textures are uploaded.
+ if (isBackground) t.draw(canvas, 0, 0);
+ --uploadQuota;
+ }
+ return uploadQuota;
+ }
+
+ @Override
+ public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+ int uploadQuota = QUOTA_PER_FRAME;
+ uploadQuota = upload(canvas, mFgTextures, uploadQuota, false);
+ if (uploadQuota < QUOTA_PER_FRAME) mGLRoot.requestRender();
+ upload(canvas, mBgTextures, uploadQuota, true);
+ synchronized (this) {
+ mIsQueued = !mFgTextures.isEmpty() || !mBgTextures.isEmpty();
+ return mIsQueued;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/TiledTexture.java b/src/com/android/gallery3d/glrenderer/TiledTexture.java
new file mode 100644
index 000000000..6ca1de088
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/TiledTexture.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2012 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.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RectF;
+import android.os.SystemClock;
+
+import com.android.gallery3d.ui.GLRoot;
+import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+
+// This class is similar to BitmapTexture, except the bitmap is
+// split into tiles. By doing so, we may increase the time required to
+// upload the whole bitmap but we reduce the time of uploading each tile
+// so it make the animation more smooth and prevents jank.
+public class TiledTexture implements Texture {
+ private static final int CONTENT_SIZE = 254;
+ private static final int BORDER_SIZE = 1;
+ private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE;
+ private static final int INIT_CAPACITY = 8;
+
+ // We are targeting at 60fps, so we have 16ms for each frame.
+ // In this 16ms, we use about 4~8 ms to upload tiles.
+ private static final long UPLOAD_TILE_LIMIT = 4; // ms
+
+ private static Tile sFreeTileHead = null;
+ private static final Object sFreeTileLock = new Object();
+
+ private static Bitmap sUploadBitmap;
+ private static Canvas sCanvas;
+ private static Paint sBitmapPaint;
+ private static Paint sPaint;
+
+ private int mUploadIndex = 0;
+
+ private final Tile[] mTiles; // Can be modified in different threads.
+ // Should be protected by "synchronized."
+ private final int mWidth;
+ private final int mHeight;
+ private final RectF mSrcRect = new RectF();
+ private final RectF mDestRect = new RectF();
+
+ public static class Uploader implements OnGLIdleListener {
+ private final ArrayDeque<TiledTexture> mTextures =
+ new ArrayDeque<TiledTexture>(INIT_CAPACITY);
+
+ private final GLRoot mGlRoot;
+ private boolean mIsQueued = false;
+
+ public Uploader(GLRoot glRoot) {
+ mGlRoot = glRoot;
+ }
+
+ public synchronized void clear() {
+ mTextures.clear();
+ }
+
+ public synchronized void addTexture(TiledTexture t) {
+ if (t.isReady()) return;
+ mTextures.addLast(t);
+
+ if (mIsQueued) return;
+ mIsQueued = true;
+ mGlRoot.addOnGLIdleListener(this);
+ }
+
+ @Override
+ public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+ ArrayDeque<TiledTexture> deque = mTextures;
+ synchronized (this) {
+ long now = SystemClock.uptimeMillis();
+ long dueTime = now + UPLOAD_TILE_LIMIT;
+ while (now < dueTime && !deque.isEmpty()) {
+ TiledTexture t = deque.peekFirst();
+ if (t.uploadNextTile(canvas)) {
+ deque.removeFirst();
+ mGlRoot.requestRender();
+ }
+ now = SystemClock.uptimeMillis();
+ }
+ mIsQueued = !mTextures.isEmpty();
+
+ // return true to keep this listener in the queue
+ return mIsQueued;
+ }
+ }
+ }
+
+ private static class Tile extends UploadedTexture {
+ public int offsetX;
+ public int offsetY;
+ public Bitmap bitmap;
+ public Tile nextFreeTile;
+ public int contentWidth;
+ public int contentHeight;
+
+ @Override
+ public void setSize(int width, int height) {
+ contentWidth = width;
+ contentHeight = height;
+ mWidth = width + 2 * BORDER_SIZE;
+ mHeight = height + 2 * BORDER_SIZE;
+ mTextureWidth = TILE_SIZE;
+ mTextureHeight = TILE_SIZE;
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ int x = BORDER_SIZE - offsetX;
+ int y = BORDER_SIZE - offsetY;
+ int r = bitmap.getWidth() + x;
+ int b = bitmap.getHeight() + y;
+ sCanvas.drawBitmap(bitmap, x, y, sBitmapPaint);
+ bitmap = null;
+
+ // draw borders if need
+ if (x > 0) sCanvas.drawLine(x - 1, 0, x - 1, TILE_SIZE, sPaint);
+ if (y > 0) sCanvas.drawLine(0, y - 1, TILE_SIZE, y - 1, sPaint);
+ if (r < CONTENT_SIZE) sCanvas.drawLine(r, 0, r, TILE_SIZE, sPaint);
+ if (b < CONTENT_SIZE) sCanvas.drawLine(0, b, TILE_SIZE, b, sPaint);
+
+ return sUploadBitmap;
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ // do nothing
+ }
+ }
+
+ private static void freeTile(Tile tile) {
+ tile.invalidateContent();
+ tile.bitmap = null;
+ synchronized (sFreeTileLock) {
+ tile.nextFreeTile = sFreeTileHead;
+ sFreeTileHead = tile;
+ }
+ }
+
+ private static Tile obtainTile() {
+ synchronized (sFreeTileLock) {
+ Tile result = sFreeTileHead;
+ if (result == null) return new Tile();
+ sFreeTileHead = result.nextFreeTile;
+ result.nextFreeTile = null;
+ return result;
+ }
+ }
+
+ private boolean uploadNextTile(GLCanvas canvas) {
+ if (mUploadIndex == mTiles.length) return true;
+
+ synchronized (mTiles) {
+ Tile next = mTiles[mUploadIndex++];
+
+ // Make sure tile has not already been recycled by the time
+ // this is called (race condition in onGLIdle)
+ if (next.bitmap != null) {
+ boolean hasBeenLoad = next.isLoaded();
+ next.updateContent(canvas);
+
+ // It will take some time for a texture to be drawn for the first
+ // time. When scrolling, we need to draw several tiles on the screen
+ // at the same time. It may cause a UI jank even these textures has
+ // been uploaded.
+ if (!hasBeenLoad) next.draw(canvas, 0, 0);
+ }
+ }
+ return mUploadIndex == mTiles.length;
+ }
+
+ public TiledTexture(Bitmap bitmap) {
+ mWidth = bitmap.getWidth();
+ mHeight = bitmap.getHeight();
+ ArrayList<Tile> list = new ArrayList<Tile>();
+
+ for (int x = 0, w = mWidth; x < w; x += CONTENT_SIZE) {
+ for (int y = 0, h = mHeight; y < h; y += CONTENT_SIZE) {
+ Tile tile = obtainTile();
+ tile.offsetX = x;
+ tile.offsetY = y;
+ tile.bitmap = bitmap;
+ tile.setSize(
+ Math.min(CONTENT_SIZE, mWidth - x),
+ Math.min(CONTENT_SIZE, mHeight - y));
+ list.add(tile);
+ }
+ }
+ mTiles = list.toArray(new Tile[list.size()]);
+ }
+
+ public boolean isReady() {
+ return mUploadIndex == mTiles.length;
+ }
+
+ // Can be called in UI thread.
+ public void recycle() {
+ synchronized (mTiles) {
+ for (int i = 0, n = mTiles.length; i < n; ++i) {
+ freeTile(mTiles[i]);
+ }
+ }
+ }
+
+ public static void freeResources() {
+ sUploadBitmap = null;
+ sCanvas = null;
+ sBitmapPaint = null;
+ sPaint = null;
+ }
+
+ public static void prepareResources() {
+ sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Config.ARGB_8888);
+ sCanvas = new Canvas(sUploadBitmap);
+ sBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+ sBitmapPaint.setXfermode(new PorterDuffXfermode(Mode.SRC));
+ sPaint = new Paint();
+ sPaint.setXfermode(new PorterDuffXfermode(Mode.SRC));
+ sPaint.setColor(Color.TRANSPARENT);
+ }
+
+ // We want to draw the "source" on the "target".
+ // This method is to find the "output" rectangle which is
+ // the corresponding area of the "src".
+ // (x,y) target
+ // (x0,y0) source +---------------+
+ // +----------+ | |
+ // | src | | output |
+ // | +--+ | linear map | +----+ |
+ // | +--+ | ----------> | | | |
+ // | | by (scaleX, scaleY) | +----+ |
+ // +----------+ | |
+ // Texture +---------------+
+ // Canvas
+ private static void mapRect(RectF output,
+ RectF src, float x0, float y0, float x, float y, float scaleX,
+ float scaleY) {
+ output.set(x + (src.left - x0) * scaleX,
+ y + (src.top - y0) * scaleY,
+ x + (src.right - x0) * scaleX,
+ y + (src.bottom - y0) * scaleY);
+ }
+
+ // Draws a mixed color of this texture and a specified color onto the
+ // a rectangle. The used color is: from * (1 - ratio) + to * ratio.
+ public void drawMixed(GLCanvas canvas, int color, float ratio,
+ int x, int y, int width, int height) {
+ RectF src = mSrcRect;
+ RectF dest = mDestRect;
+ float scaleX = (float) width / mWidth;
+ float scaleY = (float) height / mHeight;
+ synchronized (mTiles) {
+ for (int i = 0, n = mTiles.length; i < n; ++i) {
+ Tile t = mTiles[i];
+ src.set(0, 0, t.contentWidth, t.contentHeight);
+ src.offset(t.offsetX, t.offsetY);
+ mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
+ src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+ canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect);
+ }
+ }
+ }
+
+ // Draws the texture on to the specified rectangle.
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+ RectF src = mSrcRect;
+ RectF dest = mDestRect;
+ float scaleX = (float) width / mWidth;
+ float scaleY = (float) height / mHeight;
+ synchronized (mTiles) {
+ for (int i = 0, n = mTiles.length; i < n; ++i) {
+ Tile t = mTiles[i];
+ src.set(0, 0, t.contentWidth, t.contentHeight);
+ src.offset(t.offsetX, t.offsetY);
+ mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);
+ src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+ canvas.drawTexture(t, mSrcRect, mDestRect);
+ }
+ }
+ }
+
+ // Draws a sub region of this texture on to the specified rectangle.
+ public void draw(GLCanvas canvas, RectF source, RectF target) {
+ RectF src = mSrcRect;
+ RectF dest = mDestRect;
+ float x0 = source.left;
+ float y0 = source.top;
+ float x = target.left;
+ float y = target.top;
+ float scaleX = target.width() / source.width();
+ float scaleY = target.height() / source.height();
+
+ synchronized (mTiles) {
+ for (int i = 0, n = mTiles.length; i < n; ++i) {
+ Tile t = mTiles[i];
+ src.set(0, 0, t.contentWidth, t.contentHeight);
+ src.offset(t.offsetX, t.offsetY);
+ if (!src.intersect(source)) continue;
+ mapRect(dest, src, x0, y0, x, y, scaleX, scaleY);
+ src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);
+ canvas.drawTexture(t, src, dest);
+ }
+ }
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y) {
+ draw(canvas, x, y, mWidth, mHeight);
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/UploadedTexture.java b/src/com/android/gallery3d/glrenderer/UploadedTexture.java
new file mode 100644
index 000000000..f41a979b7
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/UploadedTexture.java
@@ -0,0 +1,298 @@
+/*
+ * 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.glrenderer;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.opengl.GLUtils;
+
+import junit.framework.Assert;
+
+import java.util.HashMap;
+
+import javax.microedition.khronos.opengles.GL11;
+
+// 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().
+public 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<BorderKey, Bitmap> sBorderLines =
+ new HashMap<BorderKey, Bitmap>();
+ private static BorderKey sBorderKey = new BorderKey();
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "Texture";
+ private boolean mContentValid = true;
+
+ // indicate this textures is being uploaded in background
+ private boolean mIsUploading = false;
+ private boolean mOpaque = true;
+ private boolean mThrottled = false;
+ private static int sUploadedCount;
+ private static final int UPLOAD_LIMIT = 100;
+
+ protected Bitmap mBitmap;
+ private int mBorder;
+
+ protected UploadedTexture() {
+ this(false);
+ }
+
+ protected UploadedTexture(boolean hasBorder) {
+ super(null, 0, STATE_UNLOADED);
+ if (hasBorder) {
+ setBorder(true);
+ mBorder = 1;
+ }
+ }
+
+ protected void setIsUploading(boolean uploading) {
+ mIsUploading = uploading;
+ }
+
+ public boolean isUploading() {
+ return mIsUploading;
+ }
+
+ 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();
+ int w = mBitmap.getWidth() + mBorder * 2;
+ int h = mBitmap.getHeight() + mBorder * 2;
+ if (mWidth == UNSPECIFIED) {
+ setSize(w, h);
+ }
+ }
+ return mBitmap;
+ }
+
+ private void freeBitmap() {
+ Assert.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;
+ mWidth = UNSPECIFIED;
+ mHeight = UNSPECIFIED;
+ }
+
+ /**
+ * Whether the content on GPU is valid.
+ */
+ public boolean isContentValid() {
+ return isLoaded() && mContentValid;
+ }
+
+ /**
+ * Updates the content on GPU's memory.
+ * @param canvas
+ */
+ public void updateContent(GLCanvas canvas) {
+ if (!isLoaded()) {
+ 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.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
+ freeBitmap();
+ mContentValid = true;
+ }
+ }
+
+ public static void resetUploadLimit() {
+ sUploadedCount = 0;
+ }
+
+ public static boolean uploadLimitReached() {
+ return sUploadedCount > UPLOAD_LIMIT;
+ }
+
+ private void uploadToCanvas(GLCanvas canvas) {
+
+ Bitmap bitmap = getBitmap();
+ if (bitmap != null) {
+ try {
+ int bWidth = bitmap.getWidth();
+ int bHeight = bitmap.getHeight();
+ int width = bWidth + mBorder * 2;
+ int height = bHeight + mBorder * 2;
+ int texWidth = getTextureWidth();
+ int texHeight = getTextureHeight();
+
+ Assert.assertTrue(bWidth <= texWidth && bHeight <= texHeight);
+
+ // Upload the bitmap to a new texture.
+ mId = canvas.getGLId().generateTexture();
+ canvas.setTextureParameters(this);
+
+ if (bWidth == texWidth && bHeight == texHeight) {
+ canvas.initializeTexture(this, bitmap);
+ } else {
+ int format = GLUtils.getInternalFormat(bitmap);
+ int type = GLUtils.getType(bitmap);
+ Config config = bitmap.getConfig();
+
+ canvas.initializeTextureSize(this, format, type);
+ canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);
+
+ if (mBorder > 0) {
+ // Left border
+ Bitmap line = getBorderLine(true, config, texHeight);
+ canvas.texSubImage2D(this, 0, 0, line, format, type);
+
+ // Top border
+ line = getBorderLine(false, config, texWidth);
+ canvas.texSubImage2D(this, 0, 0, line, format, type);
+ }
+
+ // Right border
+ if (mBorder + bWidth < texWidth) {
+ Bitmap line = getBorderLine(true, config, texHeight);
+ canvas.texSubImage2D(this, mBorder + bWidth, 0, line, format, type);
+ }
+
+ // Bottom border
+ if (mBorder + bHeight < texHeight) {
+ Bitmap line = getBorderLine(false, config, texWidth);
+ canvas.texSubImage2D(this, 0, mBorder + bHeight, line, format, type);
+ }
+ }
+ } finally {
+ freeBitmap();
+ }
+ // Update texture state.
+ setAssociatedCanvas(canvas);
+ mState = 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();
+ }
+
+ @Override
+ protected int getTarget() {
+ return GL11.GL_TEXTURE_2D;
+ }
+
+ public void setOpaque(boolean isOpaque) {
+ mOpaque = isOpaque;
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return mOpaque;
+ }
+
+ @Override
+ public void recycle() {
+ super.recycle();
+ if (mBitmap != null) freeBitmap();
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/ImportTask.java b/src/com/android/gallery3d/ingest/ImportTask.java
new file mode 100644
index 000000000..7d2d641a5
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ImportTask.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2013 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.ingest;
+
+import android.content.Context;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.os.Environment;
+import android.os.PowerManager;
+
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+public class ImportTask implements Runnable {
+
+ public interface Listener {
+ void onImportProgress(int visitedCount, int totalCount, String pathIfSuccessful);
+
+ void onImportFinish(Collection<MtpObjectInfo> objectsNotImported, int visitedCount);
+ }
+
+ static private final String WAKELOCK_LABEL = "MTP Import Task";
+
+ private Listener mListener;
+ private String mDestAlbumName;
+ private Collection<MtpObjectInfo> mObjectsToImport;
+ private MtpDevice mDevice;
+ private PowerManager.WakeLock mWakeLock;
+
+ public ImportTask(MtpDevice device, Collection<MtpObjectInfo> objectsToImport,
+ String destAlbumName, Context context) {
+ mDestAlbumName = destAlbumName;
+ mObjectsToImport = objectsToImport;
+ mDevice = device;
+ PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, WAKELOCK_LABEL);
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void run() {
+ mWakeLock.acquire();
+ try {
+ List<MtpObjectInfo> objectsNotImported = new LinkedList<MtpObjectInfo>();
+ int visited = 0;
+ int total = mObjectsToImport.size();
+ mListener.onImportProgress(visited, total, null);
+ File dest = new File(Environment.getExternalStorageDirectory(), mDestAlbumName);
+ dest.mkdirs();
+ for (MtpObjectInfo object : mObjectsToImport) {
+ visited++;
+ String importedPath = null;
+ if (GalleryUtils.hasSpaceForSize(object.getCompressedSize())) {
+ importedPath = new File(dest, object.getName()).getAbsolutePath();
+ if (!mDevice.importFile(object.getObjectHandle(), importedPath)) {
+ importedPath = null;
+ }
+ }
+ if (importedPath == null) {
+ objectsNotImported.add(object);
+ }
+ if (mListener != null) {
+ mListener.onImportProgress(visited, total, importedPath);
+ }
+ }
+ if (mListener != null) {
+ mListener.onImportFinish(objectsNotImported, visited);
+ }
+ } finally {
+ mListener = null;
+ mWakeLock.release();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/IngestActivity.java b/src/com/android/gallery3d/ingest/IngestActivity.java
new file mode 100644
index 000000000..687e9fd44
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/IngestActivity.java
@@ -0,0 +1,570 @@
+/*
+ * Copyright (C) 2013 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.ingest;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.Configuration;
+import android.database.DataSetObserver;
+import android.mtp.MtpObjectInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.support.v4.view.ViewPager;
+import android.util.SparseBooleanArray;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AbsListView.MultiChoiceModeListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.adapter.CheckBroker;
+import com.android.gallery3d.ingest.adapter.MtpAdapter;
+import com.android.gallery3d.ingest.adapter.MtpPagerAdapter;
+import com.android.gallery3d.ingest.data.MtpBitmapFetch;
+import com.android.gallery3d.ingest.ui.DateTileView;
+import com.android.gallery3d.ingest.ui.IngestGridView;
+import com.android.gallery3d.ingest.ui.IngestGridView.OnClearChoicesListener;
+
+import java.lang.ref.WeakReference;
+import java.util.Collection;
+
+public class IngestActivity extends Activity implements
+ MtpDeviceIndex.ProgressListener, ImportTask.Listener {
+
+ private IngestService mHelperService;
+ private boolean mActive = false;
+ private IngestGridView mGridView;
+ private MtpAdapter mAdapter;
+ private Handler mHandler;
+ private ProgressDialog mProgressDialog;
+ private ActionMode mActiveActionMode;
+
+ private View mWarningView;
+ private TextView mWarningText;
+ private int mLastCheckedPosition = 0;
+
+ private ViewPager mFullscreenPager;
+ private MtpPagerAdapter mPagerAdapter;
+ private boolean mFullscreenPagerVisible = false;
+
+ private MenuItem mMenuSwitcherItem;
+ private MenuItem mActionMenuSwitcherItem;
+
+ // The MTP framework components don't give us fine-grained file copy
+ // progress updates, so for large photos and videos, we will be stuck
+ // with a dialog not updating for a long time. To give the user feedback,
+ // we switch to the animated indeterminate progress bar after the timeout
+ // specified by INDETERMINATE_SWITCH_TIMEOUT_MS. On the next update from
+ // the framework, we switch back to the normal progress bar.
+ private static final int INDETERMINATE_SWITCH_TIMEOUT_MS = 3000;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ doBindHelperService();
+
+ setContentView(R.layout.ingest_activity_item_list);
+ mGridView = (IngestGridView) findViewById(R.id.ingest_gridview);
+ mAdapter = new MtpAdapter(this);
+ mAdapter.registerDataSetObserver(mMasterObserver);
+ mGridView.setAdapter(mAdapter);
+ mGridView.setMultiChoiceModeListener(mMultiChoiceModeListener);
+ mGridView.setOnItemClickListener(mOnItemClickListener);
+ mGridView.setOnClearChoicesListener(mPositionMappingCheckBroker);
+
+ mFullscreenPager = (ViewPager) findViewById(R.id.ingest_view_pager);
+
+ mHandler = new ItemListHandler(this);
+
+ MtpBitmapFetch.configureForContext(this);
+ }
+
+ private OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View itemView, int position, long arg3) {
+ mLastCheckedPosition = position;
+ mGridView.setItemChecked(position, !mGridView.getCheckedItemPositions().get(position));
+ }
+ };
+
+ private MultiChoiceModeListener mMultiChoiceModeListener = new MultiChoiceModeListener() {
+ private boolean mIgnoreItemCheckedStateChanges = false;
+
+ private void updateSelectedTitle(ActionMode mode) {
+ int count = mGridView.getCheckedItemCount();
+ mode.setTitle(getResources().getQuantityString(
+ R.plurals.number_of_items_selected, count, count));
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
+ boolean checked) {
+ if (mIgnoreItemCheckedStateChanges) return;
+ if (mAdapter.itemAtPositionIsBucket(position)) {
+ SparseBooleanArray checkedItems = mGridView.getCheckedItemPositions();
+ mIgnoreItemCheckedStateChanges = true;
+ mGridView.setItemChecked(position, false);
+
+ // Takes advantage of the fact that SectionIndexer imposes the
+ // need to clamp to the valid range
+ int nextSectionStart = mAdapter.getPositionForSection(
+ mAdapter.getSectionForPosition(position) + 1);
+ if (nextSectionStart == position)
+ nextSectionStart = mAdapter.getCount();
+
+ boolean rangeValue = false; // Value we want to set all of the bucket items to
+
+ // Determine if all the items in the bucket are currently checked, so that we
+ // can uncheck them, otherwise we will check all items in the bucket.
+ for (int i = position + 1; i < nextSectionStart; i++) {
+ if (checkedItems.get(i) == false) {
+ rangeValue = true;
+ break;
+ }
+ }
+
+ // Set all items in the bucket to the desired state
+ for (int i = position + 1; i < nextSectionStart; i++) {
+ if (checkedItems.get(i) != rangeValue)
+ mGridView.setItemChecked(i, rangeValue);
+ }
+
+ mPositionMappingCheckBroker.onBulkCheckedChange();
+ mIgnoreItemCheckedStateChanges = false;
+ } else {
+ mPositionMappingCheckBroker.onCheckedChange(position, checked);
+ }
+ mLastCheckedPosition = position;
+ updateSelectedTitle(mode);
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ MenuInflater inflater = mode.getMenuInflater();
+ inflater.inflate(R.menu.ingest_menu_item_list_selection, menu);
+ updateSelectedTitle(mode);
+ mActiveActionMode = mode;
+ mActionMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view);
+ setSwitcherMenuState(mActionMenuSwitcherItem, mFullscreenPagerVisible);
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mActiveActionMode = null;
+ mActionMenuSwitcherItem = null;
+ mHandler.sendEmptyMessage(ItemListHandler.MSG_BULK_CHECKED_CHANGE);
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ updateSelectedTitle(mode);
+ return false;
+ }
+ };
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.import_items:
+ if (mActiveActionMode != null) {
+ mHelperService.importSelectedItems(
+ mGridView.getCheckedItemPositions(),
+ mAdapter);
+ mActiveActionMode.finish();
+ }
+ return true;
+ case R.id.ingest_switch_view:
+ setFullscreenPagerVisibility(!mFullscreenPagerVisible);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.ingest_menu_item_list_selection, menu);
+ mMenuSwitcherItem = menu.findItem(R.id.ingest_switch_view);
+ menu.findItem(R.id.import_items).setVisible(false);
+ setSwitcherMenuState(mMenuSwitcherItem, mFullscreenPagerVisible);
+ return true;
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ doUnbindHelperService();
+ }
+
+ @Override
+ protected void onResume() {
+ DateTileView.refreshLocale();
+ mActive = true;
+ if (mHelperService != null) mHelperService.setClientActivity(this);
+ updateWarningView();
+ super.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ if (mHelperService != null) mHelperService.setClientActivity(null);
+ mActive = false;
+ cleanupProgressDialog();
+ super.onPause();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ MtpBitmapFetch.configureForContext(this);
+ }
+
+ private void showWarningView(int textResId) {
+ if (mWarningView == null) {
+ mWarningView = findViewById(R.id.ingest_warning_view);
+ mWarningText =
+ (TextView)mWarningView.findViewById(R.id.ingest_warning_view_text);
+ }
+ mWarningText.setText(textResId);
+ mWarningView.setVisibility(View.VISIBLE);
+ setFullscreenPagerVisibility(false);
+ mGridView.setVisibility(View.GONE);
+ }
+
+ private void hideWarningView() {
+ if (mWarningView != null) {
+ mWarningView.setVisibility(View.GONE);
+ setFullscreenPagerVisibility(false);
+ }
+ }
+
+ private PositionMappingCheckBroker mPositionMappingCheckBroker = new PositionMappingCheckBroker();
+
+ private class PositionMappingCheckBroker extends CheckBroker
+ implements OnClearChoicesListener {
+ private int mLastMappingPager = -1;
+ private int mLastMappingGrid = -1;
+
+ private int mapPagerToGridPosition(int position) {
+ if (position != mLastMappingPager) {
+ mLastMappingPager = position;
+ mLastMappingGrid = mAdapter.translatePositionWithoutLabels(position);
+ }
+ return mLastMappingGrid;
+ }
+
+ private int mapGridToPagerPosition(int position) {
+ if (position != mLastMappingGrid) {
+ mLastMappingGrid = position;
+ mLastMappingPager = mPagerAdapter.translatePositionWithLabels(position);
+ }
+ return mLastMappingPager;
+ }
+
+ @Override
+ public void setItemChecked(int position, boolean checked) {
+ mGridView.setItemChecked(mapPagerToGridPosition(position), checked);
+ }
+
+ @Override
+ public void onCheckedChange(int position, boolean checked) {
+ if (mPagerAdapter != null) {
+ super.onCheckedChange(mapGridToPagerPosition(position), checked);
+ }
+ }
+
+ @Override
+ public boolean isItemChecked(int position) {
+ return mGridView.getCheckedItemPositions().get(mapPagerToGridPosition(position));
+ }
+
+ @Override
+ public void onClearChoices() {
+ onBulkCheckedChange();
+ }
+ };
+
+ private DataSetObserver mMasterObserver = new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ if (mPagerAdapter != null) mPagerAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ if (mPagerAdapter != null) mPagerAdapter.notifyDataSetChanged();
+ }
+ };
+
+ private int pickFullscreenStartingPosition() {
+ int firstVisiblePosition = mGridView.getFirstVisiblePosition();
+ if (mLastCheckedPosition <= firstVisiblePosition
+ || mLastCheckedPosition > mGridView.getLastVisiblePosition()) {
+ return firstVisiblePosition;
+ } else {
+ return mLastCheckedPosition;
+ }
+ }
+
+ private void setSwitcherMenuState(MenuItem menuItem, boolean inFullscreenMode) {
+ if (menuItem == null) return;
+ if (!inFullscreenMode) {
+ menuItem.setIcon(android.R.drawable.ic_menu_zoom);
+ menuItem.setTitle(R.string.switch_photo_fullscreen);
+ } else {
+ menuItem.setIcon(android.R.drawable.ic_dialog_dialer);
+ menuItem.setTitle(R.string.switch_photo_grid);
+ }
+ }
+
+ private void setFullscreenPagerVisibility(boolean visible) {
+ mFullscreenPagerVisible = visible;
+ if (visible) {
+ if (mPagerAdapter == null) {
+ mPagerAdapter = new MtpPagerAdapter(this, mPositionMappingCheckBroker);
+ mPagerAdapter.setMtpDeviceIndex(mAdapter.getMtpDeviceIndex());
+ }
+ mFullscreenPager.setAdapter(mPagerAdapter);
+ mFullscreenPager.setCurrentItem(mPagerAdapter.translatePositionWithLabels(
+ pickFullscreenStartingPosition()), false);
+ } else if (mPagerAdapter != null) {
+ mGridView.setSelection(mAdapter.translatePositionWithoutLabels(
+ mFullscreenPager.getCurrentItem()));
+ mFullscreenPager.setAdapter(null);
+ }
+ mGridView.setVisibility(visible ? View.INVISIBLE : View.VISIBLE);
+ mFullscreenPager.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+ if (mActionMenuSwitcherItem != null) {
+ setSwitcherMenuState(mActionMenuSwitcherItem, visible);
+ }
+ setSwitcherMenuState(mMenuSwitcherItem, visible);
+ }
+
+ private void updateWarningView() {
+ if (!mAdapter.deviceConnected()) {
+ showWarningView(R.string.ingest_no_device);
+ } else if (mAdapter.indexReady() && mAdapter.getCount() == 0) {
+ showWarningView(R.string.ingest_empty_device);
+ } else {
+ hideWarningView();
+ }
+ }
+
+ private void UiThreadNotifyIndexChanged() {
+ mAdapter.notifyDataSetChanged();
+ if (mActiveActionMode != null) {
+ mActiveActionMode.finish();
+ mActiveActionMode = null;
+ }
+ updateWarningView();
+ }
+
+ protected void notifyIndexChanged() {
+ mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED);
+ }
+
+ private static class ProgressState {
+ String message;
+ String title;
+ int current;
+ int max;
+
+ public void reset() {
+ title = null;
+ message = null;
+ current = 0;
+ max = 0;
+ }
+ }
+
+ private ProgressState mProgressState = new ProgressState();
+
+ @Override
+ public void onObjectIndexed(MtpObjectInfo object, int numVisited) {
+ // Not guaranteed to be called on the UI thread
+ mProgressState.reset();
+ mProgressState.max = 0;
+ mProgressState.message = getResources().getQuantityString(
+ R.plurals.ingest_number_of_items_scanned, numVisited, numVisited);
+ mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
+ }
+
+ @Override
+ public void onSorting() {
+ // Not guaranteed to be called on the UI thread
+ mProgressState.reset();
+ mProgressState.max = 0;
+ mProgressState.message = getResources().getString(R.string.ingest_sorting);
+ mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
+ }
+
+ @Override
+ public void onIndexFinish() {
+ // Not guaranteed to be called on the UI thread
+ mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE);
+ mHandler.sendEmptyMessage(ItemListHandler.MSG_NOTIFY_CHANGED);
+ }
+
+ @Override
+ public void onImportProgress(final int visitedCount, final int totalCount,
+ String pathIfSuccessful) {
+ // Not guaranteed to be called on the UI thread
+ mProgressState.reset();
+ mProgressState.max = totalCount;
+ mProgressState.current = visitedCount;
+ mProgressState.title = getResources().getString(R.string.ingest_importing);
+ mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_UPDATE);
+ mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE);
+ mHandler.sendEmptyMessageDelayed(ItemListHandler.MSG_PROGRESS_INDETERMINATE,
+ INDETERMINATE_SWITCH_TIMEOUT_MS);
+ }
+
+ @Override
+ public void onImportFinish(Collection<MtpObjectInfo> objectsNotImported,
+ int numVisited) {
+ // Not guaranteed to be called on the UI thread
+ mHandler.sendEmptyMessage(ItemListHandler.MSG_PROGRESS_HIDE);
+ mHandler.removeMessages(ItemListHandler.MSG_PROGRESS_INDETERMINATE);
+ // TODO: maybe show an extra dialog listing the ones that failed
+ // importing, if any?
+ }
+
+ private ProgressDialog getProgressDialog() {
+ if (mProgressDialog == null || !mProgressDialog.isShowing()) {
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setCancelable(false);
+ }
+ return mProgressDialog;
+ }
+
+ private void updateProgressDialog() {
+ ProgressDialog dialog = getProgressDialog();
+ boolean indeterminate = (mProgressState.max == 0);
+ dialog.setIndeterminate(indeterminate);
+ dialog.setProgressStyle(indeterminate ? ProgressDialog.STYLE_SPINNER
+ : ProgressDialog.STYLE_HORIZONTAL);
+ if (mProgressState.title != null) {
+ dialog.setTitle(mProgressState.title);
+ }
+ if (mProgressState.message != null) {
+ dialog.setMessage(mProgressState.message);
+ }
+ if (!indeterminate) {
+ dialog.setProgress(mProgressState.current);
+ dialog.setMax(mProgressState.max);
+ }
+ if (!dialog.isShowing()) {
+ dialog.show();
+ }
+ }
+
+ private void makeProgressDialogIndeterminate() {
+ ProgressDialog dialog = getProgressDialog();
+ dialog.setIndeterminate(true);
+ }
+
+ private void cleanupProgressDialog() {
+ if (mProgressDialog != null) {
+ mProgressDialog.hide();
+ mProgressDialog = null;
+ }
+ }
+
+ // This is static and uses a WeakReference in order to avoid leaking the Activity
+ private static class ItemListHandler extends Handler {
+ public static final int MSG_PROGRESS_UPDATE = 0;
+ public static final int MSG_PROGRESS_HIDE = 1;
+ public static final int MSG_NOTIFY_CHANGED = 2;
+ public static final int MSG_BULK_CHECKED_CHANGE = 3;
+ public static final int MSG_PROGRESS_INDETERMINATE = 4;
+
+ WeakReference<IngestActivity> mParentReference;
+
+ public ItemListHandler(IngestActivity parent) {
+ super();
+ mParentReference = new WeakReference<IngestActivity>(parent);
+ }
+
+ public void handleMessage(Message message) {
+ IngestActivity parent = mParentReference.get();
+ if (parent == null || !parent.mActive)
+ return;
+ switch (message.what) {
+ case MSG_PROGRESS_HIDE:
+ parent.cleanupProgressDialog();
+ break;
+ case MSG_PROGRESS_UPDATE:
+ parent.updateProgressDialog();
+ break;
+ case MSG_NOTIFY_CHANGED:
+ parent.UiThreadNotifyIndexChanged();
+ break;
+ case MSG_BULK_CHECKED_CHANGE:
+ parent.mPositionMappingCheckBroker.onBulkCheckedChange();
+ break;
+ case MSG_PROGRESS_INDETERMINATE:
+ parent.makeProgressDialogIndeterminate();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private ServiceConnection mHelperServiceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mHelperService = ((IngestService.LocalBinder) service).getService();
+ mHelperService.setClientActivity(IngestActivity.this);
+ MtpDeviceIndex index = mHelperService.getIndex();
+ mAdapter.setMtpDeviceIndex(index);
+ if (mPagerAdapter != null) mPagerAdapter.setMtpDeviceIndex(index);
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ mHelperService = null;
+ }
+ };
+
+ private void doBindHelperService() {
+ bindService(new Intent(getApplicationContext(), IngestService.class),
+ mHelperServiceConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ private void doUnbindHelperService() {
+ if (mHelperService != null) {
+ mHelperService.setClientActivity(null);
+ unbindService(mHelperServiceConnection);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/IngestService.java b/src/com/android/gallery3d/ingest/IngestService.java
new file mode 100644
index 000000000..0ce3ab6a9
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/IngestService.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2013 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.ingest;
+
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaScannerConnection;
+import android.media.MediaScannerConnection.MediaScannerConnectionClient;
+import android.mtp.MtpDevice;
+import android.mtp.MtpDeviceInfo;
+import android.mtp.MtpObjectInfo;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.support.v4.app.NotificationCompat;
+import android.util.SparseBooleanArray;
+import android.widget.Adapter;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.NotificationIds;
+import com.android.gallery3d.data.MtpClient;
+import com.android.gallery3d.util.BucketNames;
+import com.android.gallery3d.util.UsageStatistics;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class IngestService extends Service implements ImportTask.Listener,
+ MtpDeviceIndex.ProgressListener, MtpClient.Listener {
+
+ public class LocalBinder extends Binder {
+ IngestService getService() {
+ return IngestService.this;
+ }
+ }
+
+ private static final int PROGRESS_UPDATE_INTERVAL_MS = 180;
+
+ private static MtpClient sClient;
+
+ private final IBinder mBinder = new LocalBinder();
+ private ScannerClient mScannerClient;
+ private MtpDevice mDevice;
+ private String mDevicePrettyName;
+ private MtpDeviceIndex mIndex;
+ private IngestActivity mClientActivity;
+ private boolean mRedeliverImportFinish = false;
+ private int mRedeliverImportFinishCount = 0;
+ private Collection<MtpObjectInfo> mRedeliverObjectsNotImported;
+ private boolean mRedeliverNotifyIndexChanged = false;
+ private boolean mRedeliverIndexFinish = false;
+ private NotificationManager mNotificationManager;
+ private NotificationCompat.Builder mNotificationBuilder;
+ private long mLastProgressIndexTime = 0;
+ private boolean mNeedRelaunchNotification = false;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mScannerClient = new ScannerClient(this);
+ mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ mNotificationBuilder = new NotificationCompat.Builder(this);
+ mNotificationBuilder.setSmallIcon(android.R.drawable.stat_notify_sync) // TODO drawable
+ .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, IngestActivity.class), 0));
+ mIndex = MtpDeviceIndex.getInstance();
+ mIndex.setProgressListener(this);
+
+ if (sClient == null) {
+ sClient = new MtpClient(getApplicationContext());
+ }
+ List<MtpDevice> devices = sClient.getDeviceList();
+ if (devices.size() > 0) {
+ setDevice(devices.get(0));
+ }
+ sClient.addListener(this);
+ }
+
+ @Override
+ public void onDestroy() {
+ sClient.removeListener(this);
+ mIndex.unsetProgressListener(this);
+ super.onDestroy();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ private void setDevice(MtpDevice device) {
+ if (mDevice == device) return;
+ mRedeliverImportFinish = false;
+ mRedeliverObjectsNotImported = null;
+ mRedeliverNotifyIndexChanged = false;
+ mRedeliverIndexFinish = false;
+ mDevice = device;
+ mIndex.setDevice(mDevice);
+ if (mDevice != null) {
+ MtpDeviceInfo deviceInfo = mDevice.getDeviceInfo();
+ if (deviceInfo == null) {
+ setDevice(null);
+ return;
+ } else {
+ mDevicePrettyName = deviceInfo.getModel();
+ mNotificationBuilder.setContentTitle(mDevicePrettyName);
+ new Thread(mIndex.getIndexRunnable()).start();
+ }
+ } else {
+ mDevicePrettyName = null;
+ }
+ if (mClientActivity != null) {
+ mClientActivity.notifyIndexChanged();
+ } else {
+ mRedeliverNotifyIndexChanged = true;
+ }
+ }
+
+ protected MtpDeviceIndex getIndex() {
+ return mIndex;
+ }
+
+ protected void setClientActivity(IngestActivity activity) {
+ if (mClientActivity == activity) return;
+ mClientActivity = activity;
+ if (mClientActivity == null) {
+ if (mNeedRelaunchNotification) {
+ mNotificationBuilder.setProgress(0, 0, false)
+ .setContentText(getResources().getText(R.string.ingest_scanning_done));
+ mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING,
+ mNotificationBuilder.build());
+ }
+ return;
+ }
+ mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_IMPORTING);
+ mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_SCANNING);
+ if (mRedeliverImportFinish) {
+ mClientActivity.onImportFinish(mRedeliverObjectsNotImported,
+ mRedeliverImportFinishCount);
+ mRedeliverImportFinish = false;
+ mRedeliverObjectsNotImported = null;
+ }
+ if (mRedeliverNotifyIndexChanged) {
+ mClientActivity.notifyIndexChanged();
+ mRedeliverNotifyIndexChanged = false;
+ }
+ if (mRedeliverIndexFinish) {
+ mClientActivity.onIndexFinish();
+ mRedeliverIndexFinish = false;
+ }
+ }
+
+ protected void importSelectedItems(SparseBooleanArray selected, Adapter adapter) {
+ List<MtpObjectInfo> importHandles = new ArrayList<MtpObjectInfo>();
+ for (int i = 0; i < selected.size(); i++) {
+ if (selected.valueAt(i)) {
+ Object item = adapter.getItem(selected.keyAt(i));
+ if (item instanceof MtpObjectInfo) {
+ importHandles.add(((MtpObjectInfo) item));
+ }
+ }
+ }
+ ImportTask task = new ImportTask(mDevice, importHandles, BucketNames.IMPORTED, this);
+ task.setListener(this);
+ mNotificationBuilder.setProgress(0, 0, true)
+ .setContentText(getResources().getText(R.string.ingest_importing));
+ startForeground(NotificationIds.INGEST_NOTIFICATION_IMPORTING,
+ mNotificationBuilder.build());
+ new Thread(task).start();
+ }
+
+ @Override
+ public void deviceAdded(MtpDevice device) {
+ if (mDevice == null) {
+ setDevice(device);
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_IMPORTER,
+ "DeviceConnected", null);
+ }
+ }
+
+ @Override
+ public void deviceRemoved(MtpDevice device) {
+ if (device == mDevice) {
+ setDevice(null);
+ mNeedRelaunchNotification = false;
+ mNotificationManager.cancel(NotificationIds.INGEST_NOTIFICATION_SCANNING);
+ }
+ }
+
+ @Override
+ public void onImportProgress(int visitedCount, int totalCount,
+ String pathIfSuccessful) {
+ if (pathIfSuccessful != null) {
+ mScannerClient.scanPath(pathIfSuccessful);
+ }
+ mNeedRelaunchNotification = false;
+ if (mClientActivity != null) {
+ mClientActivity.onImportProgress(visitedCount, totalCount, pathIfSuccessful);
+ }
+ mNotificationBuilder.setProgress(totalCount, visitedCount, false)
+ .setContentText(getResources().getText(R.string.ingest_importing));
+ mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_IMPORTING,
+ mNotificationBuilder.build());
+ }
+
+ @Override
+ public void onImportFinish(Collection<MtpObjectInfo> objectsNotImported,
+ int visitedCount) {
+ stopForeground(true);
+ mNeedRelaunchNotification = true;
+ if (mClientActivity != null) {
+ mClientActivity.onImportFinish(objectsNotImported, visitedCount);
+ } else {
+ mRedeliverImportFinish = true;
+ mRedeliverObjectsNotImported = objectsNotImported;
+ mRedeliverImportFinishCount = visitedCount;
+ mNotificationBuilder.setProgress(0, 0, false)
+ .setContentText(getResources().getText(R.string.import_complete));
+ mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_IMPORTING,
+ mNotificationBuilder.build());
+ }
+ UsageStatistics.onEvent(UsageStatistics.COMPONENT_IMPORTER,
+ "ImportFinished", null, visitedCount);
+ }
+
+ @Override
+ public void onObjectIndexed(MtpObjectInfo object, int numVisited) {
+ mNeedRelaunchNotification = false;
+ if (mClientActivity != null) {
+ mClientActivity.onObjectIndexed(object, numVisited);
+ } else {
+ // Throttle the updates to one every PROGRESS_UPDATE_INTERVAL_MS milliseconds
+ long currentTime = SystemClock.uptimeMillis();
+ if (currentTime > mLastProgressIndexTime + PROGRESS_UPDATE_INTERVAL_MS) {
+ mLastProgressIndexTime = currentTime;
+ mNotificationBuilder.setProgress(0, numVisited, true)
+ .setContentText(getResources().getText(R.string.ingest_scanning));
+ mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING,
+ mNotificationBuilder.build());
+ }
+ }
+ }
+
+ @Override
+ public void onSorting() {
+ if (mClientActivity != null) mClientActivity.onSorting();
+ }
+
+ @Override
+ public void onIndexFinish() {
+ mNeedRelaunchNotification = true;
+ if (mClientActivity != null) {
+ mClientActivity.onIndexFinish();
+ } else {
+ mNotificationBuilder.setProgress(0, 0, false)
+ .setContentText(getResources().getText(R.string.ingest_scanning_done));
+ mNotificationManager.notify(NotificationIds.INGEST_NOTIFICATION_SCANNING,
+ mNotificationBuilder.build());
+ mRedeliverIndexFinish = true;
+ }
+ }
+
+ // Copied from old Gallery3d code
+ private static final class ScannerClient implements MediaScannerConnectionClient {
+ ArrayList<String> mPaths = new ArrayList<String>();
+ 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) {
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/MtpDeviceIndex.java b/src/com/android/gallery3d/ingest/MtpDeviceIndex.java
new file mode 100644
index 000000000..d30f94a87
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/MtpDeviceIndex.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright (C) 2013 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.ingest;
+
+import android.mtp.MtpConstants;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * MTP objects in the index are organized into "buckets," or groupings.
+ * At present, these buckets are based on the date an item was created.
+ *
+ * When the index is created, the buckets are sorted in their natural
+ * order, and the items within the buckets sorted by the date they are taken.
+ *
+ * The index enables the access of items and bucket labels as one unified list.
+ * For example, let's say we have the following data in the index:
+ * [Bucket A]: [photo 1], [photo 2]
+ * [Bucket B]: [photo 3]
+ *
+ * Then the items can be thought of as being organized as a 5 element list:
+ * [Bucket A], [photo 1], [photo 2], [Bucket B], [photo 3]
+ *
+ * The data can also be accessed in descending order, in which case the list
+ * would be a bit different from simply reversing the ascending list, since the
+ * bucket labels need to always be at the beginning:
+ * [Bucket B], [photo 3], [Bucket A], [photo 2], [photo 1]
+ *
+ * The index enables all the following operations in constant time, both for
+ * ascending and descending views of the data:
+ * - get/getAscending/getDescending: get an item at a specified list position
+ * - size: get the total number of items (bucket labels and MTP objects)
+ * - getFirstPositionForBucketNumber
+ * - getBucketNumberForPosition
+ * - isFirstInBucket
+ *
+ * See the comments in buildLookupIndex for implementation notes.
+ */
+public class MtpDeviceIndex {
+
+ public static final int FORMAT_MOV = 0x300D; // For some reason this is not in MtpConstants
+
+ public static final Set<Integer> SUPPORTED_IMAGE_FORMATS;
+ public static final Set<Integer> SUPPORTED_VIDEO_FORMATS;
+
+ static {
+ SUPPORTED_IMAGE_FORMATS = new HashSet<Integer>();
+ SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_JFIF);
+ SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_EXIF_JPEG);
+ SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_PNG);
+ SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_GIF);
+ SUPPORTED_IMAGE_FORMATS.add(MtpConstants.FORMAT_BMP);
+
+ SUPPORTED_VIDEO_FORMATS = new HashSet<Integer>();
+ SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_3GP_CONTAINER);
+ SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_AVI);
+ SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_MP4_CONTAINER);
+ SUPPORTED_VIDEO_FORMATS.add(MtpConstants.FORMAT_MPEG);
+ // TODO: add FORMAT_MOV once Media Scanner supports .mov files
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((mDevice == null) ? 0 : mDevice.getDeviceId());
+ result = prime * result + mGeneration;
+ return result;
+ }
+
+ public interface ProgressListener {
+ public void onObjectIndexed(MtpObjectInfo object, int numVisited);
+
+ public void onSorting();
+
+ public void onIndexFinish();
+ }
+
+ public enum SortOrder {
+ Ascending, Descending
+ }
+
+ private MtpDevice mDevice;
+ private int[] mUnifiedLookupIndex;
+ private MtpObjectInfo[] mMtpObjects;
+ private DateBucket[] mBuckets;
+ private int mGeneration = 0;
+
+ public enum Progress {
+ Uninitialized, Initialized, Pending, Started, Sorting, Finished
+ }
+
+ private Progress mProgress = Progress.Uninitialized;
+ private ProgressListener mProgressListener;
+
+ private static final MtpDeviceIndex sInstance = new MtpDeviceIndex();
+ private static final MtpObjectTimestampComparator sMtpObjectComparator =
+ new MtpObjectTimestampComparator();
+
+ public static MtpDeviceIndex getInstance() {
+ return sInstance;
+ }
+
+ private MtpDeviceIndex() {
+ }
+
+ synchronized public MtpDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Sets the MtpDevice that should be indexed and initializes state, but does
+ * not kick off the actual indexing task, which is instead done by using
+ * {@link #getIndexRunnable()}
+ *
+ * @param device The MtpDevice that should be indexed
+ */
+ synchronized public void setDevice(MtpDevice device) {
+ if (device == mDevice) return;
+ mDevice = device;
+ resetState();
+ }
+
+ /**
+ * Provides a Runnable for the indexing task assuming the state has already
+ * been correctly initialized (by calling {@link #setDevice(MtpDevice)}) and
+ * has not already been run.
+ *
+ * @return Runnable for the main indexing task
+ */
+ synchronized public Runnable getIndexRunnable() {
+ if (mProgress != Progress.Initialized) return null;
+ mProgress = Progress.Pending;
+ return new IndexRunnable(mDevice);
+ }
+
+ synchronized public boolean indexReady() {
+ return mProgress == Progress.Finished;
+ }
+
+ synchronized public Progress getProgress() {
+ return mProgress;
+ }
+
+ /**
+ * @param listener Listener to change to
+ * @return Progress at the time the listener was added (useful for
+ * configuring initial UI state)
+ */
+ synchronized public Progress setProgressListener(ProgressListener listener) {
+ mProgressListener = listener;
+ return mProgress;
+ }
+
+ /**
+ * Make the listener null if it matches the argument
+ *
+ * @param listener Listener to unset, if currently registered
+ */
+ synchronized public void unsetProgressListener(ProgressListener listener) {
+ if (mProgressListener == listener)
+ mProgressListener = null;
+ }
+
+ /**
+ * @return The total number of elements in the index (labels and items)
+ */
+ public int size() {
+ return mProgress == Progress.Finished ? mUnifiedLookupIndex.length : 0;
+ }
+
+ /**
+ * @param position Index of item to fetch, where 0 is the first item in the
+ * specified order
+ * @param order
+ * @return the bucket label or MtpObjectInfo at the specified position and
+ * order
+ */
+ public Object get(int position, SortOrder order) {
+ if (mProgress != Progress.Finished) return null;
+ if(order == SortOrder.Ascending) {
+ DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
+ if (bucket.unifiedStartIndex == position) {
+ return bucket.bucket;
+ } else {
+ return mMtpObjects[bucket.itemsStartIndex + position - 1
+ - bucket.unifiedStartIndex];
+ }
+ } else {
+ int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
+ DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
+ if (bucket.unifiedEndIndex == zeroIndex) {
+ return bucket.bucket;
+ } else {
+ return mMtpObjects[bucket.itemsStartIndex + zeroIndex
+ - bucket.unifiedStartIndex];
+ }
+ }
+ }
+
+ /**
+ * @param position Index of item to fetch from a view of the data that doesn't
+ * include labels and is in the specified order
+ * @return position-th item in specified order, when not including labels
+ */
+ public MtpObjectInfo getWithoutLabels(int position, SortOrder order) {
+ if (mProgress != Progress.Finished) return null;
+ if (order == SortOrder.Ascending) {
+ return mMtpObjects[position];
+ } else {
+ return mMtpObjects[mMtpObjects.length - 1 - position];
+ }
+ }
+
+ /**
+ * Although this is O(log(number of buckets)), and thus should not be used
+ * in hotspots, even if the attached device has items for every day for
+ * a five-year timeframe, it would still only take 11 iterations at most,
+ * so shouldn't be a huge issue.
+ * @param position Index of item to map from a view of the data that doesn't
+ * include labels and is in the specified order
+ * @param order
+ * @return position in a view of the data that does include labels
+ */
+ public int getPositionFromPositionWithoutLabels(int position, SortOrder order) {
+ if (mProgress != Progress.Finished) return -1;
+ if (order == SortOrder.Descending) {
+ position = mMtpObjects.length - 1 - position;
+ }
+ int bucketNumber = 0;
+ int iMin = 0;
+ int iMax = mBuckets.length - 1;
+ while (iMax >= iMin) {
+ int iMid = (iMax + iMin) / 2;
+ if (mBuckets[iMid].itemsStartIndex + mBuckets[iMid].numItems <= position) {
+ iMin = iMid + 1;
+ } else if (mBuckets[iMid].itemsStartIndex > position) {
+ iMax = iMid - 1;
+ } else {
+ bucketNumber = iMid;
+ break;
+ }
+ }
+ int mappedPos = mBuckets[bucketNumber].unifiedStartIndex
+ + position - mBuckets[bucketNumber].itemsStartIndex;
+ if (order == SortOrder.Descending) {
+ mappedPos = mUnifiedLookupIndex.length - 1 - mappedPos;
+ }
+ return mappedPos;
+ }
+
+ public int getPositionWithoutLabelsFromPosition(int position, SortOrder order) {
+ if (mProgress != Progress.Finished) return -1;
+ if(order == SortOrder.Ascending) {
+ DateBucket bucket = mBuckets[mUnifiedLookupIndex[position]];
+ if (bucket.unifiedStartIndex == position) position++;
+ return bucket.itemsStartIndex + position - 1 - bucket.unifiedStartIndex;
+ } else {
+ int zeroIndex = mUnifiedLookupIndex.length - 1 - position;
+ DateBucket bucket = mBuckets[mUnifiedLookupIndex[zeroIndex]];
+ if (bucket.unifiedEndIndex == zeroIndex) zeroIndex--;
+ return mMtpObjects.length - 1 - bucket.itemsStartIndex
+ - zeroIndex + bucket.unifiedStartIndex;
+ }
+ }
+
+ /**
+ * @return The number of MTP items in the index (without labels)
+ */
+ public int sizeWithoutLabels() {
+ return mProgress == Progress.Finished ? mMtpObjects.length : 0;
+ }
+
+ public int getFirstPositionForBucketNumber(int bucketNumber, SortOrder order) {
+ if (order == SortOrder.Ascending) {
+ return mBuckets[bucketNumber].unifiedStartIndex;
+ } else {
+ return mUnifiedLookupIndex.length - mBuckets[mBuckets.length - 1 - bucketNumber].unifiedEndIndex - 1;
+ }
+ }
+
+ public int getBucketNumberForPosition(int position, SortOrder order) {
+ if (order == SortOrder.Ascending) {
+ return mUnifiedLookupIndex[position];
+ } else {
+ return mBuckets.length - 1 - mUnifiedLookupIndex[mUnifiedLookupIndex.length - 1 - position];
+ }
+ }
+
+ public boolean isFirstInBucket(int position, SortOrder order) {
+ if (order == SortOrder.Ascending) {
+ return mBuckets[mUnifiedLookupIndex[position]].unifiedStartIndex == position;
+ } else {
+ position = mUnifiedLookupIndex.length - 1 - position;
+ return mBuckets[mUnifiedLookupIndex[position]].unifiedEndIndex == position;
+ }
+ }
+
+ private Object[] mCachedReverseBuckets;
+
+ public Object[] getBuckets(SortOrder order) {
+ if (mBuckets == null) return null;
+ if (order == SortOrder.Ascending) {
+ return mBuckets;
+ } else {
+ if (mCachedReverseBuckets == null) {
+ computeReversedBuckets();
+ }
+ return mCachedReverseBuckets;
+ }
+ }
+
+ /*
+ * See the comments for buildLookupIndex for notes on the specific fields of
+ * this class.
+ */
+ private class DateBucket implements Comparable<DateBucket> {
+ SimpleDate bucket;
+ List<MtpObjectInfo> tempElementsList = new ArrayList<MtpObjectInfo>();
+ int unifiedStartIndex;
+ int unifiedEndIndex;
+ int itemsStartIndex;
+ int numItems;
+
+ public DateBucket(SimpleDate bucket) {
+ this.bucket = bucket;
+ }
+
+ public DateBucket(SimpleDate bucket, MtpObjectInfo firstElement) {
+ this(bucket);
+ tempElementsList.add(firstElement);
+ }
+
+ void sortElements(Comparator<MtpObjectInfo> comparator) {
+ Collections.sort(tempElementsList, comparator);
+ }
+
+ @Override
+ public String toString() {
+ return bucket.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return bucket.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (!(obj instanceof DateBucket)) return false;
+ DateBucket other = (DateBucket) obj;
+ if (bucket == null) {
+ if (other.bucket != null) return false;
+ } else if (!bucket.equals(other.bucket)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int compareTo(DateBucket another) {
+ return this.bucket.compareTo(another.bucket);
+ }
+ }
+
+ /**
+ * Comparator to sort MtpObjectInfo objects by date created.
+ */
+ private static class MtpObjectTimestampComparator implements Comparator<MtpObjectInfo> {
+ @Override
+ public int compare(MtpObjectInfo o1, MtpObjectInfo o2) {
+ long diff = o1.getDateCreated() - o2.getDateCreated();
+ if (diff < 0) {
+ return -1;
+ } else if (diff == 0) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+ }
+
+ private void resetState() {
+ mGeneration++;
+ mUnifiedLookupIndex = null;
+ mMtpObjects = null;
+ mBuckets = null;
+ mCachedReverseBuckets = null;
+ mProgress = (mDevice == null) ? Progress.Uninitialized : Progress.Initialized;
+ }
+
+
+ private class IndexRunnable implements Runnable {
+ private int[] mUnifiedLookupIndex;
+ private MtpObjectInfo[] mMtpObjects;
+ private DateBucket[] mBuckets;
+ private Map<SimpleDate, DateBucket> mBucketsTemp;
+ private MtpDevice mDevice;
+ private int mNumObjects = 0;
+
+ private class IndexingException extends Exception {};
+
+ public IndexRunnable(MtpDevice device) {
+ mDevice = device;
+ }
+
+ /*
+ * Implementation note: this is the way the index supports a lot of its operations in
+ * constant time and respecting the need to have bucket names always come before items
+ * in that bucket when accessing the list sequentially, both in ascending and descending
+ * orders.
+ *
+ * Let's say the data we have in the index is the following:
+ * [Bucket A]: [photo 1], [photo 2]
+ * [Bucket B]: [photo 3]
+ *
+ * In this case, the lookup index array would be
+ * [0, 0, 0, 1, 1]
+ *
+ * Now, whether we access the list in ascending or descending order, we know which bucket
+ * to look in (0 corresponds to A and 1 to B), and can return the bucket label as the first
+ * item in a bucket as needed. The individual IndexBUckets have a startIndex and endIndex
+ * that correspond to indices in this lookup index array, allowing us to calculate the
+ * offset of the specific item we want from within a specific bucket.
+ */
+ private void buildLookupIndex() {
+ int numBuckets = mBuckets.length;
+ mUnifiedLookupIndex = new int[mNumObjects + numBuckets];
+ int currentUnifiedIndexEntry = 0;
+ int nextUnifiedEntry;
+
+ mMtpObjects = new MtpObjectInfo[mNumObjects];
+ int currentItemsEntry = 0;
+ for (int i = 0; i < numBuckets; i++) {
+ DateBucket bucket = mBuckets[i];
+ nextUnifiedEntry = currentUnifiedIndexEntry + bucket.tempElementsList.size() + 1;
+ Arrays.fill(mUnifiedLookupIndex, currentUnifiedIndexEntry, nextUnifiedEntry, i);
+ bucket.unifiedStartIndex = currentUnifiedIndexEntry;
+ bucket.unifiedEndIndex = nextUnifiedEntry - 1;
+ currentUnifiedIndexEntry = nextUnifiedEntry;
+
+ bucket.itemsStartIndex = currentItemsEntry;
+ bucket.numItems = bucket.tempElementsList.size();
+ for (int j = 0; j < bucket.numItems; j++) {
+ mMtpObjects[currentItemsEntry] = bucket.tempElementsList.get(j);
+ currentItemsEntry++;
+ }
+ bucket.tempElementsList = null;
+ }
+ }
+
+ private void copyResults() {
+ MtpDeviceIndex.this.mUnifiedLookupIndex = mUnifiedLookupIndex;
+ MtpDeviceIndex.this.mMtpObjects = mMtpObjects;
+ MtpDeviceIndex.this.mBuckets = mBuckets;
+ mUnifiedLookupIndex = null;
+ mMtpObjects = null;
+ mBuckets = null;
+ }
+
+ @Override
+ public void run() {
+ try {
+ indexDevice();
+ } catch (IndexingException e) {
+ synchronized (MtpDeviceIndex.this) {
+ resetState();
+ if (mProgressListener != null) {
+ mProgressListener.onIndexFinish();
+ }
+ }
+ }
+ }
+
+ private void indexDevice() throws IndexingException {
+ synchronized (MtpDeviceIndex.this) {
+ mProgress = Progress.Started;
+ }
+ mBucketsTemp = new HashMap<SimpleDate, DateBucket>();
+ for (int storageId : mDevice.getStorageIds()) {
+ if (mDevice != getDevice()) throw new IndexingException();
+ Stack<Integer> pendingDirectories = new Stack<Integer>();
+ pendingDirectories.add(0xFFFFFFFF); // start at the root of the device
+ while (!pendingDirectories.isEmpty()) {
+ if (mDevice != getDevice()) throw new IndexingException();
+ int dirHandle = pendingDirectories.pop();
+ for (int objectHandle : mDevice.getObjectHandles(storageId, 0, dirHandle)) {
+ MtpObjectInfo objectInfo = mDevice.getObjectInfo(objectHandle);
+ if (objectInfo == null) throw new IndexingException();
+ int format = objectInfo.getFormat();
+ if (format == MtpConstants.FORMAT_ASSOCIATION) {
+ pendingDirectories.add(objectHandle);
+ } else if (SUPPORTED_IMAGE_FORMATS.contains(format)
+ || SUPPORTED_VIDEO_FORMATS.contains(format)) {
+ addObject(objectInfo);
+ }
+ }
+ }
+ }
+ Collection<DateBucket> values = mBucketsTemp.values();
+ mBucketsTemp = null;
+ mBuckets = values.toArray(new DateBucket[values.size()]);
+ values = null;
+ synchronized (MtpDeviceIndex.this) {
+ mProgress = Progress.Sorting;
+ if (mProgressListener != null) {
+ mProgressListener.onSorting();
+ }
+ }
+ sortAll();
+ buildLookupIndex();
+ synchronized (MtpDeviceIndex.this) {
+ if (mDevice != getDevice()) throw new IndexingException();
+ copyResults();
+
+ /*
+ * In order for getBuckets to operate in constant time for descending
+ * order, we must precompute a reversed array of the buckets, mainly
+ * because the android.widget.SectionIndexer interface which adapters
+ * that call getBuckets implement depends on section numbers to be
+ * ascending relative to the scroll position, so we must have this for
+ * descending order or the scrollbar goes crazy.
+ */
+ computeReversedBuckets();
+
+ mProgress = Progress.Finished;
+ if (mProgressListener != null) {
+ mProgressListener.onIndexFinish();
+ }
+ }
+ }
+
+ private SimpleDate mDateInstance = new SimpleDate();
+
+ private void addObject(MtpObjectInfo objectInfo) {
+ mNumObjects++;
+ mDateInstance.setTimestamp(objectInfo.getDateCreated());
+ DateBucket bucket = mBucketsTemp.get(mDateInstance);
+ if (bucket == null) {
+ bucket = new DateBucket(mDateInstance, objectInfo);
+ mBucketsTemp.put(mDateInstance, bucket);
+ mDateInstance = new SimpleDate(); // only create new date
+ // objects when they are used
+ return;
+ } else {
+ bucket.tempElementsList.add(objectInfo);
+ }
+ if (mProgressListener != null) {
+ mProgressListener.onObjectIndexed(objectInfo, mNumObjects);
+ }
+ }
+
+ private void sortAll() {
+ Arrays.sort(mBuckets);
+ for (DateBucket bucket : mBuckets) {
+ bucket.sortElements(sMtpObjectComparator);
+ }
+ }
+
+ }
+
+ private void computeReversedBuckets() {
+ mCachedReverseBuckets = new Object[mBuckets.length];
+ for (int i = 0; i < mCachedReverseBuckets.length; i++) {
+ mCachedReverseBuckets[i] = mBuckets[mBuckets.length - 1 - i];
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/SimpleDate.java b/src/com/android/gallery3d/ingest/SimpleDate.java
new file mode 100644
index 000000000..05db2cde2
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/SimpleDate.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2013 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.ingest;
+
+import java.text.DateFormat;
+import java.util.Calendar;
+
+/**
+ * Represents a date (year, month, day)
+ */
+public class SimpleDate implements Comparable<SimpleDate> {
+ public int month; // MM
+ public int day; // DD
+ public int year; // YYYY
+ private long timestamp;
+ private String mCachedStringRepresentation;
+
+ public SimpleDate() {
+ }
+
+ public SimpleDate(long timestamp) {
+ setTimestamp(timestamp);
+ }
+
+ private static Calendar sCalendarInstance = Calendar.getInstance();
+
+ public void setTimestamp(long timestamp) {
+ synchronized (sCalendarInstance) {
+ // TODO find a more efficient way to convert a timestamp to a date?
+ sCalendarInstance.setTimeInMillis(timestamp);
+ this.day = sCalendarInstance.get(Calendar.DATE);
+ this.month = sCalendarInstance.get(Calendar.MONTH);
+ this.year = sCalendarInstance.get(Calendar.YEAR);
+ this.timestamp = timestamp;
+ mCachedStringRepresentation = DateFormat.getDateInstance(DateFormat.SHORT).format(timestamp);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + day;
+ result = prime * result + month;
+ result = prime * result + year;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (!(obj instanceof SimpleDate))
+ return false;
+ SimpleDate other = (SimpleDate) obj;
+ if (year != other.year)
+ return false;
+ if (month != other.month)
+ return false;
+ if (day != other.day)
+ return false;
+ return true;
+ }
+
+ @Override
+ public int compareTo(SimpleDate other) {
+ int yearDiff = this.year - other.getYear();
+ if (yearDiff != 0)
+ return yearDiff;
+ else {
+ int monthDiff = this.month - other.getMonth();
+ if (monthDiff != 0)
+ return monthDiff;
+ else
+ return this.day - other.getDay();
+ }
+ }
+
+ public int getDay() {
+ return day;
+ }
+
+ public int getMonth() {
+ return month;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ @Override
+ public String toString() {
+ if (mCachedStringRepresentation == null) {
+ mCachedStringRepresentation = DateFormat.getDateInstance(DateFormat.SHORT).format(timestamp);
+ }
+ return mCachedStringRepresentation;
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/adapter/CheckBroker.java b/src/com/android/gallery3d/ingest/adapter/CheckBroker.java
new file mode 100644
index 000000000..6783f23c5
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/adapter/CheckBroker.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013 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.ingest.adapter;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public abstract class CheckBroker {
+ private Collection<OnCheckedChangedListener> mListeners =
+ new ArrayList<OnCheckedChangedListener>();
+
+ public interface OnCheckedChangedListener {
+ public void onCheckedChanged(int position, boolean isChecked);
+ public void onBulkCheckedChanged();
+ }
+
+ public abstract void setItemChecked(int position, boolean checked);
+
+ public void onCheckedChange(int position, boolean checked) {
+ if (isItemChecked(position) != checked) {
+ for (OnCheckedChangedListener l : mListeners) {
+ l.onCheckedChanged(position, checked);
+ }
+ }
+ }
+
+ public void onBulkCheckedChange() {
+ for (OnCheckedChangedListener l : mListeners) {
+ l.onBulkCheckedChanged();
+ }
+ }
+
+ public abstract boolean isItemChecked(int position);
+
+ public void registerOnCheckedChangeListener(OnCheckedChangedListener l) {
+ mListeners.add(l);
+ }
+
+ public void unregisterOnCheckedChangeListener(OnCheckedChangedListener l) {
+ mListeners.remove(l);
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java b/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java
new file mode 100644
index 000000000..e8dd69f8c
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/adapter/MtpAdapter.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2013 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.ingest.adapter;
+
+import android.app.Activity;
+import android.content.Context;
+import android.mtp.MtpObjectInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.SectionIndexer;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.MtpDeviceIndex;
+import com.android.gallery3d.ingest.MtpDeviceIndex.SortOrder;
+import com.android.gallery3d.ingest.SimpleDate;
+import com.android.gallery3d.ingest.ui.DateTileView;
+import com.android.gallery3d.ingest.ui.MtpThumbnailTileView;
+
+public class MtpAdapter extends BaseAdapter implements SectionIndexer {
+ public static final int ITEM_TYPE_MEDIA = 0;
+ public static final int ITEM_TYPE_BUCKET = 1;
+
+ private Context mContext;
+ private MtpDeviceIndex mModel;
+ private SortOrder mSortOrder = SortOrder.Descending;
+ private LayoutInflater mInflater;
+ private int mGeneration = 0;
+
+ public MtpAdapter(Activity context) {
+ super();
+ mContext = context;
+ mInflater = LayoutInflater.from(context);
+ }
+
+ public void setMtpDeviceIndex(MtpDeviceIndex index) {
+ mModel = index;
+ notifyDataSetChanged();
+ }
+
+ public MtpDeviceIndex getMtpDeviceIndex() {
+ return mModel;
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ mGeneration++;
+ super.notifyDataSetChanged();
+ }
+
+ @Override
+ public void notifyDataSetInvalidated() {
+ mGeneration++;
+ super.notifyDataSetInvalidated();
+ }
+
+ public boolean deviceConnected() {
+ return (mModel != null) && (mModel.getDevice() != null);
+ }
+
+ public boolean indexReady() {
+ return (mModel != null) && mModel.indexReady();
+ }
+
+ @Override
+ public int getCount() {
+ return mModel != null ? mModel.size() : 0;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mModel.get(position, mSortOrder);
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 2;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ // If the position is the first in its section, then it corresponds to
+ // a title tile, if not it's a media tile
+ if (position == getPositionForSection(getSectionForPosition(position))) {
+ return ITEM_TYPE_BUCKET;
+ } else {
+ return ITEM_TYPE_MEDIA;
+ }
+ }
+
+ public boolean itemAtPositionIsBucket(int position) {
+ return getItemViewType(position) == ITEM_TYPE_BUCKET;
+ }
+
+ public boolean itemAtPositionIsMedia(int position) {
+ return getItemViewType(position) == ITEM_TYPE_MEDIA;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ int type = getItemViewType(position);
+ if (type == ITEM_TYPE_MEDIA) {
+ MtpThumbnailTileView imageView;
+ if (convertView == null) {
+ imageView = (MtpThumbnailTileView) mInflater.inflate(
+ R.layout.ingest_thumbnail, parent, false);
+ } else {
+ imageView = (MtpThumbnailTileView) convertView;
+ }
+ imageView.setMtpDeviceAndObjectInfo(mModel.getDevice(), (MtpObjectInfo)getItem(position), mGeneration);
+ return imageView;
+ } else {
+ DateTileView dateTile;
+ if (convertView == null) {
+ dateTile = (DateTileView) mInflater.inflate(
+ R.layout.ingest_date_tile, parent, false);
+ } else {
+ dateTile = (DateTileView) convertView;
+ }
+ dateTile.setDate((SimpleDate)getItem(position));
+ return dateTile;
+ }
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ if (getCount() == 0) {
+ return 0;
+ }
+ int numSections = getSections().length;
+ if (section >= numSections) {
+ section = numSections - 1;
+ }
+ return mModel.getFirstPositionForBucketNumber(section, mSortOrder);
+ }
+
+ @Override
+ public int getSectionForPosition(int position) {
+ int count = getCount();
+ if (count == 0) {
+ return 0;
+ }
+ if (position >= count) {
+ position = count - 1;
+ }
+ return mModel.getBucketNumberForPosition(position, mSortOrder);
+ }
+
+ @Override
+ public Object[] getSections() {
+ return getCount() > 0 ? mModel.getBuckets(mSortOrder) : null;
+ }
+
+ public SortOrder getSortOrder() {
+ return mSortOrder;
+ }
+
+ public int translatePositionWithoutLabels(int position) {
+ if (mModel == null) return -1;
+ return mModel.getPositionFromPositionWithoutLabels(position, mSortOrder);
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java b/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java
new file mode 100644
index 000000000..9e7abc01d
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/adapter/MtpPagerAdapter.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2013 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.ingest.adapter;
+
+import android.content.Context;
+import android.mtp.MtpObjectInfo;
+import android.support.v4.view.PagerAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.MtpDeviceIndex;
+import com.android.gallery3d.ingest.MtpDeviceIndex.SortOrder;
+import com.android.gallery3d.ingest.ui.MtpFullscreenView;
+
+public class MtpPagerAdapter extends PagerAdapter {
+
+ private LayoutInflater mInflater;
+ private int mGeneration = 0;
+ private CheckBroker mBroker;
+ private MtpDeviceIndex mModel;
+ private SortOrder mSortOrder = SortOrder.Descending;
+
+ private MtpFullscreenView mReusableView = null;
+
+ public MtpPagerAdapter(Context context, CheckBroker broker) {
+ super();
+ mInflater = LayoutInflater.from(context);
+ mBroker = broker;
+ }
+
+ public void setMtpDeviceIndex(MtpDeviceIndex index) {
+ mModel = index;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return mModel != null ? mModel.sizeWithoutLabels() : 0;
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ mGeneration++;
+ super.notifyDataSetChanged();
+ }
+
+ public int translatePositionWithLabels(int position) {
+ if (mModel == null) return -1;
+ return mModel.getPositionWithoutLabelsFromPosition(position, mSortOrder);
+ }
+
+ @Override
+ public void finishUpdate(ViewGroup container) {
+ mReusableView = null;
+ super.finishUpdate(container);
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return view == object;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ MtpFullscreenView v = (MtpFullscreenView)object;
+ container.removeView(v);
+ mBroker.unregisterOnCheckedChangeListener(v);
+ mReusableView = v;
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ MtpFullscreenView v;
+ if (mReusableView != null) {
+ v = mReusableView;
+ mReusableView = null;
+ } else {
+ v = (MtpFullscreenView) mInflater.inflate(R.layout.ingest_fullsize, container, false);
+ }
+ MtpObjectInfo i = mModel.getWithoutLabels(position, mSortOrder);
+ v.getImageView().setMtpDeviceAndObjectInfo(mModel.getDevice(), i, mGeneration);
+ v.setPositionAndBroker(position, mBroker);
+ container.addView(v);
+ return v;
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java b/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java
new file mode 100644
index 000000000..bbc90f670
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/BitmapWithMetadata.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2013 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.ingest.data;
+
+import android.graphics.Bitmap;
+
+public class BitmapWithMetadata {
+ public Bitmap bitmap;
+ public int rotationDegrees;
+
+ public BitmapWithMetadata(Bitmap bitmap, int rotationDegrees) {
+ this.bitmap = bitmap;
+ this.rotationDegrees = rotationDegrees;
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
new file mode 100644
index 000000000..30868c22b
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/data/MtpBitmapFetch.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013 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.ingest.data;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+import com.android.camera.Exif;
+import com.android.photos.data.GalleryBitmapPool;
+
+public class MtpBitmapFetch {
+ private static int sMaxSize = 0;
+
+ public static void recycleThumbnail(Bitmap b) {
+ if (b != null) {
+ GalleryBitmapPool.getInstance().put(b);
+ }
+ }
+
+ public static Bitmap getThumbnail(MtpDevice device, MtpObjectInfo info) {
+ byte[] imageBytes = device.getThumbnail(info.getObjectHandle());
+ if (imageBytes == null) {
+ return null;
+ }
+ BitmapFactory.Options o = new BitmapFactory.Options();
+ o.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+ if (o.outWidth == 0 || o.outHeight == 0) {
+ return null;
+ }
+ o.inBitmap = GalleryBitmapPool.getInstance().get(o.outWidth, o.outHeight);
+ o.inMutable = true;
+ o.inJustDecodeBounds = false;
+ o.inSampleSize = 1;
+ try {
+ return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+ } catch (IllegalArgumentException e) {
+ // BitmapFactory throws an exception rather than returning null
+ // when image decoding fails and an existing bitmap was supplied
+ // for recycling, even if the failure was not caused by the use
+ // of that bitmap.
+ return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
+ }
+ }
+
+ public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info) {
+ return getFullsize(device, info, sMaxSize);
+ }
+
+ public static BitmapWithMetadata getFullsize(MtpDevice device, MtpObjectInfo info, int maxSide) {
+ byte[] imageBytes = device.getObject(info.getObjectHandle(), info.getCompressedSize());
+ if (imageBytes == null) {
+ return null;
+ }
+ Bitmap created;
+ if (maxSide > 0) {
+ BitmapFactory.Options o = new BitmapFactory.Options();
+ o.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+ int w = o.outWidth;
+ int h = o.outHeight;
+ int comp = Math.max(h, w);
+ int sampleSize = 1;
+ while ((comp >> 1) >= maxSide) {
+ comp = comp >> 1;
+ sampleSize++;
+ }
+ o.inSampleSize = sampleSize;
+ o.inJustDecodeBounds = false;
+ created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, o);
+ } else {
+ created = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
+ }
+ if (created == null) {
+ return null;
+ }
+
+ return new BitmapWithMetadata(created, Exif.getOrientation(imageBytes));
+ }
+
+ public static void configureForContext(Context context) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metrics);
+ sMaxSize = Math.max(metrics.heightPixels, metrics.widthPixels);
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/DateTileView.java b/src/com/android/gallery3d/ingest/ui/DateTileView.java
new file mode 100644
index 000000000..52fe9b85b
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/DateTileView.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2013 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.ingest.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.SimpleDate;
+
+import java.text.DateFormatSymbols;
+import java.util.Locale;
+
+public class DateTileView extends FrameLayout {
+ private static String[] sMonthNames = DateFormatSymbols.getInstance().getShortMonths();
+ private static Locale sLocale;
+
+ static {
+ refreshLocale();
+ }
+
+ public static boolean refreshLocale() {
+ Locale currentLocale = Locale.getDefault();
+ if (!currentLocale.equals(sLocale)) {
+ sLocale = currentLocale;
+ sMonthNames = DateFormatSymbols.getInstance(sLocale).getShortMonths();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private TextView mDateTextView;
+ private TextView mMonthTextView;
+ private TextView mYearTextView;
+ private int mMonth = -1;
+ private int mYear = -1;
+ private int mDate = -1;
+ private String[] mMonthNames = sMonthNames;
+
+ public DateTileView(Context context) {
+ super(context);
+ }
+
+ public DateTileView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DateTileView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Force this to be square
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mDateTextView = (TextView) findViewById(R.id.date_tile_day);
+ mMonthTextView = (TextView) findViewById(R.id.date_tile_month);
+ mYearTextView = (TextView) findViewById(R.id.date_tile_year);
+ }
+
+ public void setDate(SimpleDate date) {
+ setDate(date.getDay(), date.getMonth(), date.getYear());
+ }
+
+ public void setDate(int date, int month, int year) {
+ if (date != mDate) {
+ mDate = date;
+ mDateTextView.setText(mDate > 9 ? Integer.toString(mDate) : "0" + mDate);
+ }
+ if (mMonthNames != sMonthNames) {
+ mMonthNames = sMonthNames;
+ if (month == mMonth) {
+ mMonthTextView.setText(mMonthNames[mMonth]);
+ }
+ }
+ if (month != mMonth) {
+ mMonth = month;
+ mMonthTextView.setText(mMonthNames[mMonth]);
+ }
+ if (year != mYear) {
+ mYear = year;
+ mYearTextView.setText(Integer.toString(mYear));
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/IngestGridView.java b/src/com/android/gallery3d/ingest/ui/IngestGridView.java
new file mode 100644
index 000000000..c821259fe
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/IngestGridView.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2013 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.ingest.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.GridView;
+
+/**
+ * This just extends GridView with the ability to listen for calls
+ * to clearChoices()
+ */
+public class IngestGridView extends GridView {
+
+ public interface OnClearChoicesListener {
+ public void onClearChoices();
+ }
+
+ private OnClearChoicesListener mOnClearChoicesListener = null;
+
+ public IngestGridView(Context context) {
+ super(context);
+ }
+
+ public IngestGridView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public IngestGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setOnClearChoicesListener(OnClearChoicesListener l) {
+ mOnClearChoicesListener = l;
+ }
+
+ @Override
+ public void clearChoices() {
+ super.clearChoices();
+ if (mOnClearChoicesListener != null) {
+ mOnClearChoicesListener.onClearChoices();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java b/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java
new file mode 100644
index 000000000..8d3884dc6
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/MtpFullscreenView.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2013 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.ingest.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.CheckBox;
+import android.widget.Checkable;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.RelativeLayout;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.adapter.CheckBroker;
+
+public class MtpFullscreenView extends RelativeLayout implements Checkable,
+ CompoundButton.OnCheckedChangeListener, CheckBroker.OnCheckedChangedListener {
+
+ private MtpImageView mImageView;
+ private CheckBox mCheckbox;
+ private int mPosition = -1;
+ private CheckBroker mBroker;
+
+ public MtpFullscreenView(Context context) {
+ super(context);
+ }
+
+ public MtpFullscreenView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MtpFullscreenView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mImageView = (MtpImageView) findViewById(R.id.ingest_fullsize_image);
+ mCheckbox = (CheckBox) findViewById(R.id.ingest_fullsize_image_checkbox);
+ mCheckbox.setOnCheckedChangeListener(this);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mCheckbox.isChecked();
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ mCheckbox.setChecked(checked);
+ }
+
+ @Override
+ public void toggle() {
+ mCheckbox.toggle();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ setPositionAndBroker(-1, null);
+ super.onDetachedFromWindow();
+ }
+
+ public MtpImageView getImageView() {
+ return mImageView;
+ }
+
+ public int getPosition() {
+ return mPosition;
+ }
+
+ public void setPositionAndBroker(int position, CheckBroker b) {
+ if (mBroker != null) {
+ mBroker.unregisterOnCheckedChangeListener(this);
+ }
+ mPosition = position;
+ mBroker = b;
+ if (mBroker != null) {
+ setChecked(mBroker.isItemChecked(position));
+ mBroker.registerOnCheckedChangeListener(this);
+ }
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton arg0, boolean isChecked) {
+ if (mBroker != null) mBroker.setItemChecked(mPosition, isChecked);
+ }
+
+ @Override
+ public void onCheckedChanged(int position, boolean isChecked) {
+ if (position == mPosition) {
+ setChecked(isChecked);
+ }
+ }
+
+ @Override
+ public void onBulkCheckedChanged() {
+ if(mBroker != null) setChecked(mBroker.isItemChecked(mPosition));
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/MtpImageView.java b/src/com/android/gallery3d/ingest/ui/MtpImageView.java
new file mode 100644
index 000000000..80c105126
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/MtpImageView.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2013 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.ingest.ui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.MtpDeviceIndex;
+import com.android.gallery3d.ingest.data.BitmapWithMetadata;
+import com.android.gallery3d.ingest.data.MtpBitmapFetch;
+
+import java.lang.ref.WeakReference;
+
+public class MtpImageView extends ImageView {
+ // We will use the thumbnail for images larger than this threshold
+ private static final int MAX_FULLSIZE_PREVIEW_SIZE = 8388608; // 8 megabytes
+
+ private int mObjectHandle;
+ private int mGeneration;
+
+ private WeakReference<MtpImageView> mWeakReference = new WeakReference<MtpImageView>(this);
+ private Object mFetchLock = new Object();
+ private boolean mFetchPending = false;
+ private MtpObjectInfo mFetchObjectInfo;
+ private MtpDevice mFetchDevice;
+ private Object mFetchResult;
+ private Drawable mOverlayIcon;
+ private boolean mShowOverlayIcon;
+
+ private static final FetchImageHandler sFetchHandler = FetchImageHandler.createOnNewThread();
+ private static final ShowImageHandler sFetchCompleteHandler = new ShowImageHandler();
+
+ private void init() {
+ showPlaceholder();
+ }
+
+ public MtpImageView(Context context) {
+ super(context);
+ init();
+ }
+
+ public MtpImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public MtpImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void showPlaceholder() {
+ setImageResource(android.R.color.transparent);
+ }
+
+ public void setMtpDeviceAndObjectInfo(MtpDevice device, MtpObjectInfo object, int gen) {
+ int handle = object.getObjectHandle();
+ if (handle == mObjectHandle && gen == mGeneration) {
+ return;
+ }
+ cancelLoadingAndClear();
+ showPlaceholder();
+ mGeneration = gen;
+ mObjectHandle = handle;
+ mShowOverlayIcon = MtpDeviceIndex.SUPPORTED_VIDEO_FORMATS.contains(object.getFormat());
+ if (mShowOverlayIcon && mOverlayIcon == null) {
+ mOverlayIcon = getResources().getDrawable(R.drawable.ic_control_play);
+ updateOverlayIconBounds();
+ }
+ synchronized (mFetchLock) {
+ mFetchObjectInfo = object;
+ mFetchDevice = device;
+ if (mFetchPending) return;
+ mFetchPending = true;
+ sFetchHandler.sendMessage(
+ sFetchHandler.obtainMessage(0, mWeakReference));
+ }
+ }
+
+ protected Object fetchMtpImageDataFromDevice(MtpDevice device, MtpObjectInfo info) {
+ if (info.getCompressedSize() <= MAX_FULLSIZE_PREVIEW_SIZE
+ && MtpDeviceIndex.SUPPORTED_IMAGE_FORMATS.contains(info.getFormat())) {
+ return MtpBitmapFetch.getFullsize(device, info);
+ } else {
+ return new BitmapWithMetadata(MtpBitmapFetch.getThumbnail(device, info), 0);
+ }
+ }
+
+ private float mLastBitmapWidth;
+ private float mLastBitmapHeight;
+ private int mLastRotationDegrees;
+ private Matrix mDrawMatrix = new Matrix();
+
+ private void updateDrawMatrix() {
+ mDrawMatrix.reset();
+ float dwidth;
+ float dheight;
+ float vheight = getHeight();
+ float vwidth = getWidth();
+ float scale;
+ boolean rotated90 = (mLastRotationDegrees % 180 != 0);
+ if (rotated90) {
+ dwidth = mLastBitmapHeight;
+ dheight = mLastBitmapWidth;
+ } else {
+ dwidth = mLastBitmapWidth;
+ dheight = mLastBitmapHeight;
+ }
+ if (dwidth <= vwidth && dheight <= vheight) {
+ scale = 1.0f;
+ } else {
+ scale = Math.min(vwidth / dwidth, vheight / dheight);
+ }
+ mDrawMatrix.setScale(scale, scale);
+ if (rotated90) {
+ mDrawMatrix.postTranslate(-dheight * scale * 0.5f,
+ -dwidth * scale * 0.5f);
+ mDrawMatrix.postRotate(mLastRotationDegrees);
+ mDrawMatrix.postTranslate(dwidth * scale * 0.5f,
+ dheight * scale * 0.5f);
+ }
+ mDrawMatrix.postTranslate((vwidth - dwidth * scale) * 0.5f,
+ (vheight - dheight * scale) * 0.5f);
+ if (!rotated90 && mLastRotationDegrees > 0) {
+ // rotated by a multiple of 180
+ mDrawMatrix.postRotate(mLastRotationDegrees, vwidth / 2, vheight / 2);
+ }
+ setImageMatrix(mDrawMatrix);
+ }
+
+ private static final int OVERLAY_ICON_SIZE_DENOMINATOR = 4;
+
+ private void updateOverlayIconBounds() {
+ int iheight = mOverlayIcon.getIntrinsicHeight();
+ int iwidth = mOverlayIcon.getIntrinsicWidth();
+ int vheight = getHeight();
+ int vwidth = getWidth();
+ float scale_height = ((float) vheight) / (iheight * OVERLAY_ICON_SIZE_DENOMINATOR);
+ float scale_width = ((float) vwidth) / (iwidth * OVERLAY_ICON_SIZE_DENOMINATOR);
+ if (scale_height >= 1f && scale_width >= 1f) {
+ mOverlayIcon.setBounds((vwidth - iwidth) / 2,
+ (vheight - iheight) / 2,
+ (vwidth + iwidth) / 2,
+ (vheight + iheight) / 2);
+ } else {
+ float scale = Math.min(scale_height, scale_width);
+ mOverlayIcon.setBounds((int) (vwidth - scale * iwidth) / 2,
+ (int) (vheight - scale * iheight) / 2,
+ (int) (vwidth + scale * iwidth) / 2,
+ (int) (vheight + scale * iheight) / 2);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (changed && getScaleType() == ScaleType.MATRIX) {
+ updateDrawMatrix();
+ }
+ if (mShowOverlayIcon && changed && mOverlayIcon != null) {
+ updateOverlayIconBounds();
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mShowOverlayIcon && mOverlayIcon != null) {
+ mOverlayIcon.draw(canvas);
+ }
+ }
+
+ protected void onMtpImageDataFetchedFromDevice(Object result) {
+ BitmapWithMetadata bitmapWithMetadata = (BitmapWithMetadata)result;
+ if (getScaleType() == ScaleType.MATRIX) {
+ mLastBitmapHeight = bitmapWithMetadata.bitmap.getHeight();
+ mLastBitmapWidth = bitmapWithMetadata.bitmap.getWidth();
+ mLastRotationDegrees = bitmapWithMetadata.rotationDegrees;
+ updateDrawMatrix();
+ } else {
+ setRotation(bitmapWithMetadata.rotationDegrees);
+ }
+ setAlpha(0f);
+ setImageBitmap(bitmapWithMetadata.bitmap);
+ animate().alpha(1f);
+ }
+
+ protected void cancelLoadingAndClear() {
+ synchronized (mFetchLock) {
+ mFetchDevice = null;
+ mFetchObjectInfo = null;
+ mFetchResult = null;
+ }
+ animate().cancel();
+ setImageResource(android.R.color.transparent);
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ cancelLoadingAndClear();
+ super.onDetachedFromWindow();
+ }
+
+ private static class FetchImageHandler extends Handler {
+ public FetchImageHandler(Looper l) {
+ super(l);
+ }
+
+ public static FetchImageHandler createOnNewThread() {
+ HandlerThread t = new HandlerThread("MtpImageView Fetch");
+ t.start();
+ return new FetchImageHandler(t.getLooper());
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ @SuppressWarnings("unchecked")
+ MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get();
+ if (parent == null) return;
+ MtpObjectInfo objectInfo;
+ MtpDevice device;
+ synchronized (parent.mFetchLock) {
+ parent.mFetchPending = false;
+ device = parent.mFetchDevice;
+ objectInfo = parent.mFetchObjectInfo;
+ }
+ if (device == null) return;
+ Object result = parent.fetchMtpImageDataFromDevice(device, objectInfo);
+ if (result == null) return;
+ synchronized (parent.mFetchLock) {
+ if (parent.mFetchObjectInfo != objectInfo) return;
+ parent.mFetchResult = result;
+ parent.mFetchDevice = null;
+ parent.mFetchObjectInfo = null;
+ sFetchCompleteHandler.sendMessage(
+ sFetchCompleteHandler.obtainMessage(0, parent.mWeakReference));
+ }
+ }
+ }
+
+ private static class ShowImageHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ @SuppressWarnings("unchecked")
+ MtpImageView parent = ((WeakReference<MtpImageView>) msg.obj).get();
+ if (parent == null) return;
+ Object result;
+ synchronized (parent.mFetchLock) {
+ result = parent.mFetchResult;
+ }
+ if (result == null) return;
+ parent.onMtpImageDataFetchedFromDevice(result);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java b/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java
new file mode 100644
index 000000000..3307e78aa
--- /dev/null
+++ b/src/com/android/gallery3d/ingest/ui/MtpThumbnailTileView.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013 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.ingest.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.mtp.MtpDevice;
+import android.mtp.MtpObjectInfo;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ingest.data.MtpBitmapFetch;
+
+
+public class MtpThumbnailTileView extends MtpImageView implements Checkable {
+
+ private Paint mForegroundPaint;
+ private boolean mIsChecked;
+ private Bitmap mBitmap;
+
+ private void init() {
+ mForegroundPaint = new Paint();
+ mForegroundPaint.setColor(getResources().getColor(R.color.ingest_highlight_semitransparent));
+ }
+
+ public MtpThumbnailTileView(Context context) {
+ super(context);
+ init();
+ }
+
+ public MtpThumbnailTileView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public MtpThumbnailTileView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ @Override
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Force this to be square
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+ }
+
+ @Override
+ protected Object fetchMtpImageDataFromDevice(MtpDevice device, MtpObjectInfo info) {
+ return MtpBitmapFetch.getThumbnail(device, info);
+ }
+
+ @Override
+ protected void onMtpImageDataFetchedFromDevice(Object result) {
+ mBitmap = (Bitmap)result;
+ setImageBitmap(mBitmap);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (isChecked()) {
+ canvas.drawRect(canvas.getClipBounds(), mForegroundPaint);
+ }
+ }
+
+ @Override
+ public boolean isChecked() {
+ return mIsChecked;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ mIsChecked = checked;
+ }
+
+ @Override
+ public void toggle() {
+ setChecked(!mIsChecked);
+ }
+
+ @Override
+ protected void cancelLoadingAndClear() {
+ super.cancelLoadingAndClear();
+ if (mBitmap != null) {
+ MtpBitmapFetch.recycleThumbnail(mBitmap);
+ mBitmap = null;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/onetimeinitializer/GalleryWidgetMigrator.java b/src/com/android/gallery3d/onetimeinitializer/GalleryWidgetMigrator.java
new file mode 100644
index 000000000..ef26b1b97
--- /dev/null
+++ b/src/com/android/gallery3d/onetimeinitializer/GalleryWidgetMigrator.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2012 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.onetimeinitializer;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Environment;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.LocalAlbum;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.gadget.WidgetDatabaseHelper;
+import com.android.gallery3d.gadget.WidgetDatabaseHelper.Entry;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * This one-timer migrates local-album gallery app widgets from old paths from prior releases
+ * to updated paths in the current build version. This migration is needed because of
+ * bucket ID (i.e., directory hash) change in JB and JB MR1 (The external storage path has changed
+ * from /mnt/sdcard in pre-JB releases, to /storage/sdcard0 in JB, then again
+ * to /external/storage/sdcard/0 in JB MR1).
+ */
+public class GalleryWidgetMigrator {
+ private static final String TAG = "GalleryWidgetMigrator";
+ private static final String PRE_JB_EXT_PATH = "/mnt/sdcard";
+ private static final String JB_EXT_PATH = "/storage/sdcard0";
+ private static final String NEW_EXT_PATH =
+ Environment.getExternalStorageDirectory().getAbsolutePath();
+ private static final int RELATIVE_PATH_START = NEW_EXT_PATH.length();
+ private static final String KEY_EXT_PATH = "external_storage_path";
+
+ /**
+ * Migrates local-album gallery widgets from prior releases to current release
+ * due to bucket ID (i.e., directory hash) change.
+ */
+ public static void migrateGalleryWidgets(Context context) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ // Migration is only needed when external storage path has changed
+ String extPath = prefs.getString(KEY_EXT_PATH, null);
+ boolean isDone = NEW_EXT_PATH.equals(extPath);
+ if (isDone) return;
+
+ try {
+ migrateGalleryWidgetsInternal(context);
+ prefs.edit().putString(KEY_EXT_PATH, NEW_EXT_PATH).commit();
+ } catch (Throwable t) {
+ // exception may be thrown if external storage is not available(?)
+ Log.w(TAG, "migrateGalleryWidgets", t);
+ }
+ }
+
+ private static void migrateGalleryWidgetsInternal(Context context) {
+ GalleryApp galleryApp = (GalleryApp) context.getApplicationContext();
+ DataManager manager = galleryApp.getDataManager();
+ WidgetDatabaseHelper dbHelper = new WidgetDatabaseHelper(context);
+
+ // only need to migrate local-album entries of type TYPE_ALBUM
+ List<Entry> entries = dbHelper.getEntries(WidgetDatabaseHelper.TYPE_ALBUM);
+ if (entries == null) return;
+
+ // Check each entry's relativePath. If exists, update bucket id using relative
+ // path combined with external storage path. Otherwise, iterate through old external
+ // storage paths to find the relative path that matches the old bucket id, and then update
+ // bucket id and relative path
+ HashMap<Integer, Entry> localEntries = new HashMap<Integer, Entry>(entries.size());
+ for (Entry entry : entries) {
+ Path path = Path.fromString(entry.albumPath);
+ MediaSet mediaSet = (MediaSet) manager.getMediaObject(path);
+ if (mediaSet instanceof LocalAlbum) {
+ if (entry.relativePath != null && entry.relativePath.length() > 0) {
+ // update entry using relative path + external storage path
+ updateEntryUsingRelativePath(entry, dbHelper);
+ } else {
+ int bucketId = Integer.parseInt(path.getSuffix());
+ localEntries.put(bucketId, entry);
+ }
+ }
+ }
+ if (!localEntries.isEmpty()) migrateLocalEntries(context, localEntries, dbHelper);
+ }
+
+ private static void migrateLocalEntries(Context context,
+ HashMap<Integer, Entry> entries, WidgetDatabaseHelper dbHelper) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ String oldExtPath = prefs.getString(KEY_EXT_PATH, null);
+ if (oldExtPath != null) {
+ migrateLocalEntries(entries, dbHelper, oldExtPath);
+ return;
+ }
+ // If old external storage path is unknown, it could be either Pre-JB or JB version
+ // we need to try both.
+ migrateLocalEntries(entries, dbHelper, PRE_JB_EXT_PATH);
+ if (!entries.isEmpty() &&
+ Build.VERSION.SDK_INT > ApiHelper.VERSION_CODES.JELLY_BEAN) {
+ migrateLocalEntries(entries, dbHelper, JB_EXT_PATH);
+ }
+ }
+
+ private static void migrateLocalEntries(HashMap<Integer, Entry> entries,
+ WidgetDatabaseHelper dbHelper, String oldExtPath) {
+ File root = Environment.getExternalStorageDirectory();
+ // check the DCIM directory first; this should take care of 99% use cases
+ updatePath(new File(root, "DCIM"), entries, dbHelper, oldExtPath);
+ // check other directories if DCIM doesn't cut it
+ if (!entries.isEmpty()) updatePath(root, entries, dbHelper, oldExtPath);
+ }
+ private static void updatePath(File root, HashMap<Integer, Entry> entries,
+ WidgetDatabaseHelper dbHelper, String oldExtStorage) {
+ File[] files = root.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory() && !entries.isEmpty()) {
+ String path = file.getAbsolutePath();
+ String oldPath = oldExtStorage + path.substring(RELATIVE_PATH_START);
+ int oldBucketId = GalleryUtils.getBucketId(oldPath);
+ Entry entry = entries.remove(oldBucketId);
+ if (entry != null) {
+ int newBucketId = GalleryUtils.getBucketId(path);
+ String newAlbumPath = Path.fromString(entry.albumPath)
+ .getParent()
+ .getChild(newBucketId)
+ .toString();
+ Log.d(TAG, "migrate from " + entry.albumPath + " to " + newAlbumPath);
+ entry.albumPath = newAlbumPath;
+ // update entry's relative path
+ entry.relativePath = path.substring(RELATIVE_PATH_START);
+ dbHelper.updateEntry(entry);
+ }
+ updatePath(file, entries, dbHelper, oldExtStorage); // recursion
+ }
+ }
+ }
+ }
+
+ private static void updateEntryUsingRelativePath(Entry entry, WidgetDatabaseHelper dbHelper) {
+ String newPath = NEW_EXT_PATH + entry.relativePath;
+ int newBucketId = GalleryUtils.getBucketId(newPath);
+ String newAlbumPath = Path.fromString(entry.albumPath)
+ .getParent()
+ .getChild(newBucketId)
+ .toString();
+ entry.albumPath = newAlbumPath;
+ dbHelper.updateEntry(entry);
+ }
+}
diff --git a/src/com/android/gallery3d/provider/GalleryProvider.java b/src/com/android/gallery3d/provider/GalleryProvider.java
new file mode 100644
index 000000000..d6c7ccd4d
--- /dev/null
+++ b/src/com/android/gallery3d/provider/GalleryProvider.java
@@ -0,0 +1,228 @@
+/*
+ * 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 android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.util.Log;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.common.AsyncTaskUtil;
+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.picasasource.PicasaSource;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+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);
+
+ public static interface PicasaColumns {
+ public static final String USER_ACCOUNT = "user_account";
+ public static final String PICASA_ID = "picasa_id";
+ }
+
+ private static final String[] SUPPORTED_PICASA_COLUMNS = {
+ PicasaColumns.USER_ACCOUNT,
+ PicasaColumns.PICASA_ID,
+ ImageColumns.DISPLAY_NAME,
+ ImageColumns.SIZE,
+ ImageColumns.MIME_TYPE,
+ ImageColumns.DATE_TAKEN,
+ ImageColumns.LATITUDE,
+ ImageColumns.LONGITUDE,
+ ImageColumns.ORIENTATION};
+
+ private DataManager mDataManager;
+ private static Uri sBaseUri;
+
+ public static String getAuthority(Context context) {
+ return context.getPackageName() + ".provider";
+ }
+
+ public static Uri getUriFor(Context context, Path path) {
+ if (sBaseUri == null) {
+ sBaseUri = Uri.parse("content://" + context.getPackageName() + ".provider");
+ }
+ return sBaseUri.buildUpon()
+ .appendEncodedPath(path.toString().substring(1)) // ignore the leading '/'
+ .build();
+ }
+
+ @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;
+ }
+
+ // 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 {
+ return null;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private Cursor queryPicasaItem(MediaObject image, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) {
+ if (projection == null) projection = SUPPORTED_PICASA_COLUMNS;
+ 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 (PicasaColumns.USER_ACCOUNT.equals(column)) {
+ columnValues[i] = PicasaSource.getUserAccount(getContext(), image);
+ } else if (PicasaColumns.PICASA_ID.equals(column)) {
+ columnValues[i] = PicasaSource.getPicasaId(image);
+ } else 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 {
+ 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 static interface PipeDataWriter<T> {
+ void writeDataToPipe(ParcelFileDescriptor output, T args);
+ }
+
+ // Modified from ContentProvider.openPipeHelper. We are target at API LEVEL 10.
+ // But openPipeHelper is available in API LEVEL 11.
+ private static <T> ParcelFileDescriptor openPipeHelper(
+ final T args, final PipeDataWriter<T> func) throws FileNotFoundException {
+ try {
+ final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+ AsyncTask<Object, Object, Object> task = new AsyncTask<Object, Object, Object>() {
+ @Override
+ protected Object doInBackground(Object... params) {
+ try {
+ func.writeDataToPipe(pipe[1], args);
+ return null;
+ } finally {
+ Utils.closeSilently(pipe[1]);
+ }
+ }
+ };
+ AsyncTaskUtil.executeInParallel(task, (Object[]) null);
+ return pipe[0];
+ } catch (IOException e) {
+ throw new FileNotFoundException("failure making pipe");
+ }
+ }
+
+}
diff --git a/src/com/android/gallery3d/ui/AbstractSlotRenderer.java b/src/com/android/gallery3d/ui/AbstractSlotRenderer.java
new file mode 100644
index 000000000..729439dc3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AbstractSlotRenderer.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2012 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;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.glrenderer.FadeOutTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.NinePatchTexture;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.Texture;
+
+public abstract class AbstractSlotRenderer implements SlotView.SlotRenderer {
+
+ private final ResourceTexture mVideoOverlay;
+ private final ResourceTexture mVideoPlayIcon;
+ private final ResourceTexture mPanoramaIcon;
+ private final NinePatchTexture mFramePressed;
+ private final NinePatchTexture mFrameSelected;
+ private FadeOutTexture mFramePressedUp;
+
+ protected AbstractSlotRenderer(Context context) {
+ mVideoOverlay = new ResourceTexture(context, R.drawable.ic_video_thumb);
+ mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_gallery_play);
+ mPanoramaIcon = new ResourceTexture(context, R.drawable.ic_360pano_holo_light);
+ mFramePressed = new NinePatchTexture(context, R.drawable.grid_pressed);
+ mFrameSelected = new NinePatchTexture(context, R.drawable.grid_selected);
+ }
+
+ protected void drawContent(GLCanvas canvas,
+ Texture content, int width, int height, int rotation) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+
+ // The content is always rendered in to the largest square that fits
+ // inside the slot, aligned to the top of the slot.
+ width = height = Math.min(width, height);
+ if (rotation != 0) {
+ canvas.translate(width / 2, height / 2);
+ canvas.rotate(rotation, 0, 0, 1);
+ canvas.translate(-width / 2, -height / 2);
+ }
+
+ // Fit the content into the box
+ float scale = Math.min(
+ (float) width / content.getWidth(),
+ (float) height / content.getHeight());
+ canvas.scale(scale, scale, 1);
+ content.draw(canvas, 0, 0);
+
+ canvas.restore();
+ }
+
+ protected void drawVideoOverlay(GLCanvas canvas, int width, int height) {
+ // Scale the video overlay to the height of the thumbnail and put it
+ // on the left side.
+ ResourceTexture v = mVideoOverlay;
+ float scale = (float) height / v.getHeight();
+ int w = Math.round(scale * v.getWidth());
+ int h = Math.round(scale * v.getHeight());
+ v.draw(canvas, 0, 0, w, h);
+
+ int s = Math.min(width, height) / 6;
+ mVideoPlayIcon.draw(canvas, (width - s) / 2, (height - s) / 2, s, s);
+ }
+
+ protected void drawPanoramaIcon(GLCanvas canvas, int width, int height) {
+ int iconSize = Math.min(width, height) / 6;
+ mPanoramaIcon.draw(canvas, (width - iconSize) / 2, (height - iconSize) / 2,
+ iconSize, iconSize);
+ }
+
+ protected boolean isPressedUpFrameFinished() {
+ if (mFramePressedUp != null) {
+ if (mFramePressedUp.isAnimating()) {
+ return false;
+ } else {
+ mFramePressedUp = null;
+ }
+ }
+ return true;
+ }
+
+ protected void drawPressedUpFrame(GLCanvas canvas, int width, int height) {
+ if (mFramePressedUp == null) {
+ mFramePressedUp = new FadeOutTexture(mFramePressed);
+ }
+ drawFrame(canvas, mFramePressed.getPaddings(), mFramePressedUp, 0, 0, width, height);
+ }
+
+ protected void drawPressedFrame(GLCanvas canvas, int width, int height) {
+ drawFrame(canvas, mFramePressed.getPaddings(), mFramePressed, 0, 0, width, height);
+ }
+
+ protected void drawSelectedFrame(GLCanvas canvas, int width, int height) {
+ drawFrame(canvas, mFrameSelected.getPaddings(), mFrameSelected, 0, 0, width, height);
+ }
+
+ protected static void drawFrame(GLCanvas canvas, Rect padding, Texture frame,
+ int x, int y, int width, int height) {
+ frame.draw(canvas, x - padding.left, y - padding.top, width + padding.left + padding.right,
+ height + padding.top + padding.bottom);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ActionModeHandler.java b/src/com/android/gallery3d/ui/ActionModeHandler.java
new file mode 100644
index 000000000..6b4f10312
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ActionModeHandler.java
@@ -0,0 +1,501 @@
+/*
+ * 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.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.nfc.NfcAdapter;
+import android.os.Handler;
+import android.view.ActionMode;
+import android.view.ActionMode.Callback;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ShareActionProvider;
+import android.widget.ShareActionProvider.OnShareTargetSelectedListener;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.Path;
+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 java.util.ArrayList;
+
+public class ActionModeHandler implements Callback, PopupList.OnPopupItemClickListener {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "ActionModeHandler";
+
+ private static final int MAX_SELECTED_ITEMS_FOR_SHARE_INTENT = 300;
+ private static final int MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT = 10;
+
+ private static final int SUPPORT_MULTIPLE_MASK = MediaObject.SUPPORT_DELETE
+ | MediaObject.SUPPORT_ROTATE | MediaObject.SUPPORT_SHARE
+ | MediaObject.SUPPORT_CACHE;
+
+ public interface ActionModeListener {
+ public boolean onActionItemClicked(MenuItem item);
+ }
+
+ private final AbstractGalleryActivity mActivity;
+ private final MenuExecutor mMenuExecutor;
+ private final SelectionManager mSelectionManager;
+ private final NfcAdapter mNfcAdapter;
+ private Menu mMenu;
+ private MenuItem mSharePanoramaMenuItem;
+ private MenuItem mShareMenuItem;
+ private ShareActionProvider mSharePanoramaActionProvider;
+ private ShareActionProvider mShareActionProvider;
+ private SelectionMenu mSelectionMenu;
+ private ActionModeListener mListener;
+ private Future<?> mMenuTask;
+ private final Handler mMainHandler;
+ private ActionMode mActionMode;
+
+ private static class GetAllPanoramaSupports implements PanoramaSupportCallback {
+ private int mNumInfoRequired;
+ private JobContext mJobContext;
+ public boolean mAllPanoramas = true;
+ public boolean mAllPanorama360 = true;
+ public boolean mHasPanorama360 = false;
+ private Object mLock = new Object();
+
+ public GetAllPanoramaSupports(ArrayList<MediaObject> mediaObjects, JobContext jc) {
+ mJobContext = jc;
+ mNumInfoRequired = mediaObjects.size();
+ for (MediaObject mediaObject : mediaObjects) {
+ mediaObject.getPanoramaSupport(this);
+ }
+ }
+
+ @Override
+ public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+ boolean isPanorama360) {
+ synchronized (mLock) {
+ mNumInfoRequired--;
+ mAllPanoramas = isPanorama && mAllPanoramas;
+ mAllPanorama360 = isPanorama360 && mAllPanorama360;
+ mHasPanorama360 = mHasPanorama360 || isPanorama360;
+ if (mNumInfoRequired == 0 || mJobContext.isCancelled()) {
+ mLock.notifyAll();
+ }
+ }
+ }
+
+ public void waitForPanoramaSupport() {
+ synchronized (mLock) {
+ while (mNumInfoRequired != 0 && !mJobContext.isCancelled()) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException e) {
+ // May be a cancelled job context
+ }
+ }
+ }
+ }
+ }
+
+ public ActionModeHandler(
+ AbstractGalleryActivity activity, SelectionManager selectionManager) {
+ mActivity = Utils.checkNotNull(activity);
+ mSelectionManager = Utils.checkNotNull(selectionManager);
+ mMenuExecutor = new MenuExecutor(activity, selectionManager);
+ mMainHandler = new Handler(activity.getMainLooper());
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity.getAndroidContext());
+ }
+
+ public void startActionMode() {
+ Activity a = mActivity;
+ mActionMode = a.startActionMode(this);
+ View customView = LayoutInflater.from(a).inflate(
+ R.layout.action_mode, null);
+ mActionMode.setCustomView(customView);
+ mSelectionMenu = new SelectionMenu(a,
+ (Button) customView.findViewById(R.id.selection_menu), this);
+ updateSelectionMenu();
+ }
+
+ public void finishActionMode() {
+ mActionMode.finish();
+ }
+
+ public void setTitle(String title) {
+ mSelectionMenu.setTitle(title);
+ }
+
+ public void setActionModeListener(ActionModeListener listener) {
+ mListener = listener;
+ }
+
+ private WakeLockHoldingProgressListener mDeleteProgressListener;
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ GLRoot root = mActivity.getGLRoot();
+ root.lockRenderThread();
+ try {
+ boolean result;
+ // Give listener a chance to process this command before it's routed to
+ // ActionModeHandler, which handles command only based on the action id.
+ // Sometimes the listener may have more background information to handle
+ // an action command.
+ if (mListener != null) {
+ result = mListener.onActionItemClicked(item);
+ if (result) {
+ mSelectionManager.leaveSelectionMode();
+ return result;
+ }
+ }
+ ProgressListener listener = null;
+ String confirmMsg = null;
+ int action = item.getItemId();
+ if (action == R.id.action_delete) {
+ confirmMsg = mActivity.getResources().getQuantityString(
+ R.plurals.delete_selection, mSelectionManager.getSelectedCount());
+ if (mDeleteProgressListener == null) {
+ mDeleteProgressListener = new WakeLockHoldingProgressListener(mActivity,
+ "Gallery Delete Progress Listener");
+ }
+ listener = mDeleteProgressListener;
+ }
+ mMenuExecutor.onMenuClicked(item, confirmMsg, listener);
+ } finally {
+ root.unlockRenderThread();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onPopupItemClick(int itemId) {
+ GLRoot root = mActivity.getGLRoot();
+ root.lockRenderThread();
+ try {
+ if (itemId == R.id.action_select_all) {
+ updateSupportedOperation();
+ mMenuExecutor.onMenuClicked(itemId, null, false, true);
+ }
+ return true;
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ private void updateSelectionMenu() {
+ // update title
+ int count = mSelectionManager.getSelectedCount();
+ String format = mActivity.getResources().getQuantityString(
+ R.plurals.number_of_items_selected, count);
+ setTitle(String.format(format, count));
+
+ // For clients who call SelectionManager.selectAll() directly, we need to ensure the
+ // menu status is consistent with selection manager.
+ mSelectionMenu.updateSelectAllMode(mSelectionManager.inSelectAllMode());
+ }
+
+ private final OnShareTargetSelectedListener mShareTargetSelectedListener =
+ new OnShareTargetSelectedListener() {
+ @Override
+ public boolean onShareTargetSelected(ShareActionProvider source, Intent intent) {
+ mSelectionManager.leaveSelectionMode();
+ return false;
+ }
+ };
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ mode.getMenuInflater().inflate(R.menu.operation, menu);
+
+ mMenu = menu;
+ mSharePanoramaMenuItem = menu.findItem(R.id.action_share_panorama);
+ if (mSharePanoramaMenuItem != null) {
+ mSharePanoramaActionProvider = (ShareActionProvider) mSharePanoramaMenuItem
+ .getActionProvider();
+ mSharePanoramaActionProvider.setOnShareTargetSelectedListener(
+ mShareTargetSelectedListener);
+ mSharePanoramaActionProvider.setShareHistoryFileName("panorama_share_history.xml");
+ }
+ mShareMenuItem = menu.findItem(R.id.action_share);
+ if (mShareMenuItem != null) {
+ mShareActionProvider = (ShareActionProvider) mShareMenuItem
+ .getActionProvider();
+ mShareActionProvider.setOnShareTargetSelectedListener(
+ mShareTargetSelectedListener);
+ mShareActionProvider.setShareHistoryFileName("share_history.xml");
+ }
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mSelectionManager.leaveSelectionMode();
+ }
+
+ private ArrayList<MediaObject> getSelectedMediaObjects(JobContext jc) {
+ ArrayList<Path> unexpandedPaths = mSelectionManager.getSelected(false);
+ if (unexpandedPaths.isEmpty()) {
+ // This happens when starting selection mode from overflow menu
+ // (instead of long press a media object)
+ return null;
+ }
+ ArrayList<MediaObject> selected = new ArrayList<MediaObject>();
+ DataManager manager = mActivity.getDataManager();
+ for (Path path : unexpandedPaths) {
+ if (jc.isCancelled()) {
+ return null;
+ }
+ selected.add(manager.getMediaObject(path));
+ }
+
+ return selected;
+ }
+ // Menu options are determined by selection set itself.
+ // We cannot expand it because MenuExecuter executes it based on
+ // the selection set instead of the expanded result.
+ // e.g. LocalImage can be rotated but collections of them (LocalAlbum) can't.
+ private int computeMenuOptions(ArrayList<MediaObject> selected) {
+ int operation = MediaObject.SUPPORT_ALL;
+ int type = 0;
+ for (MediaObject mediaObject: selected) {
+ int support = mediaObject.getSupportedOperations();
+ type |= mediaObject.getMediaType();
+ operation &= support;
+ }
+
+ switch (selected.size()) {
+ case 1:
+ final String mimeType = MenuExecutor.getMimeType(type);
+ if (!GalleryUtils.isEditorAvailable(mActivity, mimeType)) {
+ operation &= ~MediaObject.SUPPORT_EDIT;
+ }
+ break;
+ default:
+ operation &= SUPPORT_MULTIPLE_MASK;
+ }
+
+ return operation;
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.JELLY_BEAN)
+ private void setNfcBeamPushUris(Uri[] uris) {
+ if (mNfcAdapter != null && ApiHelper.HAS_SET_BEAM_PUSH_URIS) {
+ mNfcAdapter.setBeamPushUrisCallback(null, mActivity);
+ mNfcAdapter.setBeamPushUris(uris, mActivity);
+ }
+ }
+
+ // Share intent needs to expand the selection set so we can get URI of
+ // each media item
+ private Intent computePanoramaSharingIntent(JobContext jc, int maxItems) {
+ ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems);
+ if (expandedPaths == null || expandedPaths.size() == 0) {
+ return new Intent();
+ }
+ final ArrayList<Uri> uris = new ArrayList<Uri>();
+ DataManager manager = mActivity.getDataManager();
+ final Intent intent = new Intent();
+ for (Path path : expandedPaths) {
+ if (jc.isCancelled()) return null;
+ uris.add(manager.getContentUri(path));
+ }
+
+ final int size = uris.size();
+ if (size > 0) {
+ if (size > 1) {
+ intent.setAction(Intent.ACTION_SEND_MULTIPLE);
+ intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360);
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+ } else {
+ intent.setAction(Intent.ACTION_SEND);
+ intent.setType(GalleryUtils.MIME_TYPE_PANORAMA360);
+ intent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+ }
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+
+ return intent;
+ }
+
+ private Intent computeSharingIntent(JobContext jc, int maxItems) {
+ ArrayList<Path> expandedPaths = mSelectionManager.getSelected(true, maxItems);
+ if (expandedPaths == null || expandedPaths.size() == 0) {
+ setNfcBeamPushUris(null);
+ return new Intent();
+ }
+ final ArrayList<Uri> uris = new ArrayList<Uri>();
+ DataManager manager = mActivity.getDataManager();
+ int type = 0;
+ final Intent intent = new Intent();
+ for (Path path : expandedPaths) {
+ if (jc.isCancelled()) return null;
+ int support = manager.getSupportedOperations(path);
+ type |= manager.getMediaType(path);
+
+ if ((support & MediaObject.SUPPORT_SHARE) != 0) {
+ uris.add(manager.getContentUri(path));
+ }
+ }
+
+ final int size = uris.size();
+ if (size > 0) {
+ final String mimeType = MenuExecutor.getMimeType(type);
+ if (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.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ setNfcBeamPushUris(uris.toArray(new Uri[uris.size()]));
+ } else {
+ setNfcBeamPushUris(null);
+ }
+
+ return intent;
+ }
+
+ public void updateSupportedOperation(Path path, boolean selected) {
+ // TODO: We need to improve the performance
+ updateSupportedOperation();
+ }
+
+ public void updateSupportedOperation() {
+ // Interrupt previous unfinished task, mMenuTask is only accessed in main thread
+ if (mMenuTask != null) mMenuTask.cancel();
+
+ updateSelectionMenu();
+
+ // Disable share actions until share intent is in good shape
+ if (mSharePanoramaMenuItem != null) mSharePanoramaMenuItem.setEnabled(false);
+ if (mShareMenuItem != null) mShareMenuItem.setEnabled(false);
+
+ // Generate sharing intent and update supported operations in the background
+ // The task can take a long time and be canceled in the mean time.
+ mMenuTask = mActivity.getThreadPool().submit(new Job<Void>() {
+ @Override
+ public Void run(final JobContext jc) {
+ // Pass1: Deal with unexpanded media object list for menu operation.
+ ArrayList<MediaObject> selected = getSelectedMediaObjects(jc);
+ if (selected == null) {
+ mMainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mMenuTask = null;
+ if (jc.isCancelled()) return;
+ // Disable all the operations when no item is selected
+ MenuExecutor.updateMenuOperation(mMenu, 0);
+ }
+ });
+ return null;
+ }
+ final int operation = computeMenuOptions(selected);
+ if (jc.isCancelled()) {
+ return null;
+ }
+ int numSelected = selected.size();
+ final boolean canSharePanoramas =
+ numSelected < MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT;
+ final boolean canShare =
+ numSelected < MAX_SELECTED_ITEMS_FOR_SHARE_INTENT;
+
+ final GetAllPanoramaSupports supportCallback = canSharePanoramas ?
+ new GetAllPanoramaSupports(selected, jc)
+ : null;
+
+ // Pass2: Deal with expanded media object list for sharing operation.
+ final Intent share_panorama_intent = canSharePanoramas ?
+ computePanoramaSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_PANORAMA_SHARE_INTENT)
+ : new Intent();
+ final Intent share_intent = canShare ?
+ computeSharingIntent(jc, MAX_SELECTED_ITEMS_FOR_SHARE_INTENT)
+ : new Intent();
+
+ if (canSharePanoramas) {
+ supportCallback.waitForPanoramaSupport();
+ }
+ if (jc.isCancelled()) {
+ return null;
+ }
+ mMainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mMenuTask = null;
+ if (jc.isCancelled()) return;
+ MenuExecutor.updateMenuOperation(mMenu, operation);
+ MenuExecutor.updateMenuForPanorama(mMenu,
+ canSharePanoramas && supportCallback.mAllPanorama360,
+ canSharePanoramas && supportCallback.mHasPanorama360);
+ if (mSharePanoramaMenuItem != null) {
+ mSharePanoramaMenuItem.setEnabled(true);
+ if (canSharePanoramas && supportCallback.mAllPanorama360) {
+ mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ mShareMenuItem.setTitle(
+ mActivity.getResources().getString(R.string.share_as_photo));
+ } else {
+ mSharePanoramaMenuItem.setVisible(false);
+ mShareMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ mShareMenuItem.setTitle(
+ mActivity.getResources().getString(R.string.share));
+ }
+ mSharePanoramaActionProvider.setShareIntent(share_panorama_intent);
+ }
+ if (mShareMenuItem != null) {
+ mShareMenuItem.setEnabled(canShare);
+ mShareActionProvider.setShareIntent(share_intent);
+ }
+ }
+ });
+ return null;
+ }
+ });
+ }
+
+ public void pause() {
+ if (mMenuTask != null) {
+ mMenuTask.cancel();
+ mMenuTask = null;
+ }
+ mMenuExecutor.pause();
+ }
+
+ public void destroy() {
+ mMenuExecutor.destroy();
+ }
+
+ public void resume() {
+ if (mSelectionManager.inSelectionMode()) updateSupportedOperation();
+ mMenuExecutor.resume();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumLabelMaker.java b/src/com/android/gallery3d/ui/AlbumLabelMaker.java
new file mode 100644
index 000000000..da1cac0bd
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumLabelMaker.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2012 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.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.Typeface;
+import android.text.TextPaint;
+import android.text.TextUtils;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.data.DataSourceType;
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class AlbumLabelMaker {
+ private static final int BORDER_SIZE = 0;
+
+ private final AlbumSetSlotRenderer.LabelSpec mSpec;
+ private final TextPaint mTitlePaint;
+ private final TextPaint mCountPaint;
+ private final Context mContext;
+
+ private int mLabelWidth;
+ private int mBitmapWidth;
+ private int mBitmapHeight;
+
+ private final LazyLoadedBitmap mLocalSetIcon;
+ private final LazyLoadedBitmap mPicasaIcon;
+ private final LazyLoadedBitmap mCameraIcon;
+
+ public AlbumLabelMaker(Context context, AlbumSetSlotRenderer.LabelSpec spec) {
+ mContext = context;
+ mSpec = spec;
+ mTitlePaint = getTextPaint(spec.titleFontSize, spec.titleColor, false);
+ mCountPaint = getTextPaint(spec.countFontSize, spec.countColor, false);
+
+ mLocalSetIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_folder);
+ mPicasaIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_picasa);
+ mCameraIcon = new LazyLoadedBitmap(R.drawable.frame_overlay_gallery_camera);
+ }
+
+ public static int getBorderSize() {
+ return BORDER_SIZE;
+ }
+
+ private Bitmap getOverlayAlbumIcon(int sourceType) {
+ switch (sourceType) {
+ case DataSourceType.TYPE_CAMERA:
+ return mCameraIcon.get();
+ case DataSourceType.TYPE_LOCAL:
+ return mLocalSetIcon.get();
+ case DataSourceType.TYPE_PICASA:
+ return mPicasaIcon.get();
+ }
+ return null;
+ }
+
+ private static TextPaint getTextPaint(int textSize, int color, boolean isBold) {
+ TextPaint paint = new TextPaint();
+ paint.setTextSize(textSize);
+ paint.setAntiAlias(true);
+ paint.setColor(color);
+ //paint.setShadowLayer(2f, 0f, 0f, Color.LTGRAY);
+ if (isBold) {
+ paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));
+ }
+ return paint;
+ }
+
+ private class LazyLoadedBitmap {
+ private Bitmap mBitmap;
+ private int mResId;
+
+ public LazyLoadedBitmap(int resId) {
+ mResId = resId;
+ }
+
+ public synchronized Bitmap get() {
+ if (mBitmap == null) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ mBitmap = BitmapFactory.decodeResource(
+ mContext.getResources(), mResId, options);
+ }
+ return mBitmap;
+ }
+ }
+
+ public synchronized void setLabelWidth(int width) {
+ if (mLabelWidth == width) return;
+ mLabelWidth = width;
+ int borders = 2 * BORDER_SIZE;
+ mBitmapWidth = width + borders;
+ mBitmapHeight = mSpec.labelBackgroundHeight + borders;
+ }
+
+ public ThreadPool.Job<Bitmap> requestLabel(
+ String title, String count, int sourceType) {
+ return new AlbumLabelJob(title, count, sourceType);
+ }
+
+ static void drawText(Canvas canvas,
+ int x, int y, String text, int lengthLimit, TextPaint p) {
+ // The TextPaint cannot be used concurrently
+ synchronized (p) {
+ text = TextUtils.ellipsize(
+ text, p, lengthLimit, TextUtils.TruncateAt.END).toString();
+ canvas.drawText(text, x, y - p.getFontMetricsInt().ascent, p);
+ }
+ }
+
+ private class AlbumLabelJob implements ThreadPool.Job<Bitmap> {
+ private final String mTitle;
+ private final String mCount;
+ private final int mSourceType;
+
+ public AlbumLabelJob(String title, String count, int sourceType) {
+ mTitle = title;
+ mCount = count;
+ mSourceType = sourceType;
+ }
+
+ @Override
+ public Bitmap run(JobContext jc) {
+ AlbumSetSlotRenderer.LabelSpec s = mSpec;
+
+ String title = mTitle;
+ String count = mCount;
+ Bitmap icon = getOverlayAlbumIcon(mSourceType);
+
+ Bitmap bitmap;
+ int labelWidth;
+
+ synchronized (this) {
+ labelWidth = mLabelWidth;
+ bitmap = GalleryBitmapPool.getInstance().get(mBitmapWidth, mBitmapHeight);
+ }
+
+ if (bitmap == null) {
+ int borders = 2 * BORDER_SIZE;
+ bitmap = Bitmap.createBitmap(labelWidth + borders,
+ s.labelBackgroundHeight + borders, Config.ARGB_8888);
+ }
+
+ Canvas canvas = new Canvas(bitmap);
+ canvas.clipRect(BORDER_SIZE, BORDER_SIZE,
+ bitmap.getWidth() - BORDER_SIZE,
+ bitmap.getHeight() - BORDER_SIZE);
+ canvas.drawColor(mSpec.backgroundColor, PorterDuff.Mode.SRC);
+
+ canvas.translate(BORDER_SIZE, BORDER_SIZE);
+
+ // draw title
+ if (jc.isCancelled()) return null;
+ int x = s.leftMargin + s.iconSize;
+ // TODO: is the offset relevant in new reskin?
+ // int y = s.titleOffset;
+ int y = (s.labelBackgroundHeight - s.titleFontSize) / 2;
+ drawText(canvas, x, y, title, labelWidth - s.leftMargin - x -
+ s.titleRightMargin, mTitlePaint);
+
+ // draw count
+ if (jc.isCancelled()) return null;
+ x = labelWidth - s.titleRightMargin;
+ y = (s.labelBackgroundHeight - s.countFontSize) / 2;
+ drawText(canvas, x, y, count,
+ labelWidth - x , mCountPaint);
+
+ // draw the icon
+ if (icon != null) {
+ if (jc.isCancelled()) return null;
+ float scale = (float) s.iconSize / icon.getWidth();
+ canvas.translate(s.leftMargin, (s.labelBackgroundHeight -
+ Math.round(scale * icon.getHeight()))/2f);
+ canvas.scale(scale, scale);
+ canvas.drawBitmap(icon, 0, 0, null);
+ }
+
+ return bitmap;
+ }
+ }
+
+ public void recycleLabel(Bitmap label) {
+ GalleryBitmapPool.getInstance().put(label);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
new file mode 100644
index 000000000..8149df4b3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSetSlidingWindow.java
@@ -0,0 +1,549 @@
+/*T
+ * 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.os.Message;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.app.AlbumSetDataLoader;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DataSourceType;
+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.glrenderer.BitmapTexture;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.glrenderer.TextureUploader;
+import com.android.gallery3d.glrenderer.TiledTexture;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+
+public class AlbumSetSlidingWindow implements AlbumSetDataLoader.DataListener {
+ private static final String TAG = "AlbumSetSlidingWindow";
+ private static final int MSG_UPDATE_ALBUM_ENTRY = 1;
+
+ public static interface Listener {
+ public void onSizeChanged(int size);
+ public void onContentChanged();
+ }
+
+ private final AlbumSetDataLoader mSource;
+ private int mSize;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private Listener mListener;
+
+ private final AlbumSetEntry mData[];
+ private final SynchronizedHandler mHandler;
+ private final ThreadPool mThreadPool;
+ private final AlbumLabelMaker mLabelMaker;
+ private final String mLoadingText;
+
+ private final TiledTexture.Uploader mContentUploader;
+ private final TextureUploader mLabelUploader;
+
+ private int mActiveRequestCount = 0;
+ private boolean mIsActive = false;
+ private BitmapTexture mLoadingLabel;
+
+ private int mSlotWidth;
+
+ public static class AlbumSetEntry {
+ public MediaSet album;
+ public MediaItem coverItem;
+ public Texture content;
+ public BitmapTexture labelTexture;
+ public TiledTexture bitmapTexture;
+ public Path setPath;
+ public String title;
+ public int totalCount;
+ public int sourceType;
+ public int cacheFlag;
+ public int cacheStatus;
+ public int rotation;
+ public boolean isWaitLoadingDisplayed;
+ public long setDataVersion;
+ public long coverDataVersion;
+ private BitmapLoader labelLoader;
+ private BitmapLoader coverLoader;
+ }
+
+ public AlbumSetSlidingWindow(AbstractGalleryActivity activity,
+ AlbumSetDataLoader source, AlbumSetSlotRenderer.LabelSpec labelSpec, int cacheSize) {
+ source.setModelListener(this);
+ mSource = source;
+ mData = new AlbumSetEntry[cacheSize];
+ mSize = source.size();
+ mThreadPool = activity.getThreadPool();
+
+ mLabelMaker = new AlbumLabelMaker(activity.getAndroidContext(), labelSpec);
+ mLoadingText = activity.getAndroidContext().getString(R.string.loading);
+ mContentUploader = new TiledTexture.Uploader(activity.getGLRoot());
+ mLabelUploader = new TextureUploader(activity.getGLRoot());
+
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ Utils.assertTrue(message.what == MSG_UPDATE_ALBUM_ENTRY);
+ ((EntryUpdater) message.obj).updateEntry();
+ }
+ };
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public AlbumSetEntry get(int slotIndex) {
+ if (!isActiveSlot(slotIndex)) {
+ Utils.fail("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) {
+ if (!(start <= end && end - start <= mData.length && end <= mSize)) {
+ Utils.fail("start = %s, end = %s, length = %s, size = %s",
+ start, end, mData.length, mSize);
+ }
+
+ AlbumSetEntry data[] = mData;
+ mActiveStart = start;
+ mActiveEnd = end;
+ 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) {
+ updateTextureUploadQueue();
+ 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;
+ AlbumSetEntry entry = mData[slotIndex % mData.length];
+ if (entry.coverLoader != null) entry.coverLoader.startLoad();
+ if (entry.labelLoader != null) entry.labelLoader.startLoad();
+ }
+
+ private void cancelImagesInSlot(int slotIndex) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumSetEntry entry = mData[slotIndex % mData.length];
+ if (entry.coverLoader != null) entry.coverLoader.cancelLoad();
+ if (entry.labelLoader != null) entry.labelLoader.cancelLoad();
+ }
+
+ private static long getDataVersion(MediaObject object) {
+ return object == null
+ ? MediaSet.INVALID_DATA_VERSION
+ : object.getDataVersion();
+ }
+
+ private void freeSlotContent(int slotIndex) {
+ AlbumSetEntry entry = mData[slotIndex % mData.length];
+ if (entry.coverLoader != null) entry.coverLoader.recycle();
+ if (entry.labelLoader != null) entry.labelLoader.recycle();
+ if (entry.labelTexture != null) entry.labelTexture.recycle();
+ if (entry.bitmapTexture != null) entry.bitmapTexture.recycle();
+ mData[slotIndex % mData.length] = null;
+ }
+
+ private boolean isLabelChanged(
+ AlbumSetEntry entry, String title, int totalCount, int sourceType) {
+ return !Utils.equals(entry.title, title)
+ || entry.totalCount != totalCount
+ || entry.sourceType != sourceType;
+ }
+
+ private void updateAlbumSetEntry(AlbumSetEntry entry, int slotIndex) {
+ MediaSet album = mSource.getMediaSet(slotIndex);
+ MediaItem cover = mSource.getCoverItem(slotIndex);
+ int totalCount = mSource.getTotalCount(slotIndex);
+
+ entry.album = album;
+ entry.setDataVersion = getDataVersion(album);
+ entry.cacheFlag = identifyCacheFlag(album);
+ entry.cacheStatus = identifyCacheStatus(album);
+ entry.setPath = (album == null) ? null : album.getPath();
+
+ String title = (album == null) ? "" : Utils.ensureNotNull(album.getName());
+ int sourceType = DataSourceType.identifySourceType(album);
+ if (isLabelChanged(entry, title, totalCount, sourceType)) {
+ entry.title = title;
+ entry.totalCount = totalCount;
+ entry.sourceType = sourceType;
+ if (entry.labelLoader != null) {
+ entry.labelLoader.recycle();
+ entry.labelLoader = null;
+ entry.labelTexture = null;
+ }
+ if (album != null) {
+ entry.labelLoader = new AlbumLabelLoader(
+ slotIndex, title, totalCount, sourceType);
+ }
+ }
+
+ entry.coverItem = cover;
+ if (getDataVersion(cover) != entry.coverDataVersion) {
+ entry.coverDataVersion = getDataVersion(cover);
+ entry.rotation = (cover == null) ? 0 : cover.getRotation();
+ if (entry.coverLoader != null) {
+ entry.coverLoader.recycle();
+ entry.coverLoader = null;
+ entry.bitmapTexture = null;
+ entry.content = null;
+ }
+ if (cover != null) {
+ entry.coverLoader = new AlbumCoverLoader(slotIndex, cover);
+ }
+ }
+ }
+
+ private void prepareSlotContent(int slotIndex) {
+ AlbumSetEntry entry = new AlbumSetEntry();
+ updateAlbumSetEntry(entry, slotIndex);
+ mData[slotIndex % mData.length] = entry;
+ }
+
+ private static boolean startLoadBitmap(BitmapLoader loader) {
+ if (loader == null) return false;
+ loader.startLoad();
+ return loader.isRequestInProgress();
+ }
+
+ private void uploadBackgroundTextureInSlot(int index) {
+ if (index < mContentStart || index >= mContentEnd) return;
+ AlbumSetEntry entry = mData[index % mData.length];
+ if (entry.bitmapTexture != null) {
+ mContentUploader.addTexture(entry.bitmapTexture);
+ }
+ if (entry.labelTexture != null) {
+ mLabelUploader.addBgTexture(entry.labelTexture);
+ }
+ }
+
+ private void updateTextureUploadQueue() {
+ if (!mIsActive) return;
+ mContentUploader.clear();
+ mLabelUploader.clear();
+
+ // Upload foreground texture
+ for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+ AlbumSetEntry entry = mData[i % mData.length];
+ if (entry.bitmapTexture != null) {
+ mContentUploader.addTexture(entry.bitmapTexture);
+ }
+ if (entry.labelTexture != null) {
+ mLabelUploader.addFgTexture(entry.labelTexture);
+ }
+ }
+
+ // add background textures
+ int range = Math.max(
+ (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+ for (int i = 0; i < range; ++i) {
+ uploadBackgroundTextureInSlot(mActiveEnd + i);
+ uploadBackgroundTextureInSlot(mActiveStart - i - 1);
+ }
+ }
+
+ private void updateAllImageRequests() {
+ mActiveRequestCount = 0;
+ for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+ AlbumSetEntry entry = mData[i % mData.length];
+ if (startLoadBitmap(entry.coverLoader)) ++mActiveRequestCount;
+ if (startLoadBitmap(entry.labelLoader)) ++mActiveRequestCount;
+ }
+ if (mActiveRequestCount == 0) {
+ requestNonactiveImages();
+ } else {
+ cancelNonactiveImages();
+ }
+ }
+
+ @Override
+ public void onSizeChanged(int size) {
+ if (mIsActive && mSize != size) {
+ mSize = size;
+ if (mListener != null) mListener.onSizeChanged(mSize);
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+ }
+
+ @Override
+ public void onContentChanged(int index) {
+ if (!mIsActive) {
+ // paused, ignore slot changed event
+ return;
+ }
+
+ // If the updated content is not cached, ignore it
+ if (index < mContentStart || index >= mContentEnd) {
+ Log.w(TAG, String.format(
+ "invalid update: %s is outside (%s, %s)",
+ index, mContentStart, mContentEnd) );
+ return;
+ }
+
+ AlbumSetEntry entry = mData[index % mData.length];
+ updateAlbumSetEntry(entry, index);
+ updateAllImageRequests();
+ updateTextureUploadQueue();
+ if (mListener != null && isActiveSlot(index)) {
+ mListener.onContentChanged();
+ }
+ }
+
+ public BitmapTexture getLoadingTexture() {
+ if (mLoadingLabel == null) {
+ Bitmap bitmap = mLabelMaker.requestLabel(
+ mLoadingText, "", DataSourceType.TYPE_NOT_CATEGORIZED)
+ .run(ThreadPool.JOB_CONTEXT_STUB);
+ mLoadingLabel = new BitmapTexture(bitmap);
+ mLoadingLabel.setOpaque(false);
+ }
+ return mLoadingLabel;
+ }
+
+ public void pause() {
+ mIsActive = false;
+ mLabelUploader.clear();
+ mContentUploader.clear();
+ TiledTexture.freeResources();
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ }
+
+ public void resume() {
+ mIsActive = true;
+ TiledTexture.prepareResources();
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ updateAllImageRequests();
+ }
+
+ private static interface EntryUpdater {
+ public void updateEntry();
+ }
+
+ private class AlbumCoverLoader extends BitmapLoader implements EntryUpdater {
+ private MediaItem mMediaItem;
+ private final int mSlotIndex;
+
+ public AlbumCoverLoader(int slotIndex, MediaItem item) {
+ mSlotIndex = slotIndex;
+ mMediaItem = item;
+ }
+
+ @Override
+ protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+ return mThreadPool.submit(mMediaItem.requestImage(
+ MediaItem.TYPE_MICROTHUMBNAIL), l);
+ }
+
+ @Override
+ protected void onLoadComplete(Bitmap bitmap) {
+ mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
+ }
+
+ @Override
+ public void updateEntry() {
+ Bitmap bitmap = getBitmap();
+ if (bitmap == null) return; // error or recycled
+
+ AlbumSetEntry entry = mData[mSlotIndex % mData.length];
+ TiledTexture texture = new TiledTexture(bitmap);
+ entry.bitmapTexture = texture;
+ entry.content = texture;
+
+ if (isActiveSlot(mSlotIndex)) {
+ mContentUploader.addTexture(texture);
+ --mActiveRequestCount;
+ if (mActiveRequestCount == 0) requestNonactiveImages();
+ if (mListener != null) mListener.onContentChanged();
+ } else {
+ mContentUploader.addTexture(texture);
+ }
+ }
+ }
+
+ 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 AlbumLabelLoader extends BitmapLoader implements EntryUpdater {
+ private final int mSlotIndex;
+ private final String mTitle;
+ private final int mTotalCount;
+ private final int mSourceType;
+
+ public AlbumLabelLoader(
+ int slotIndex, String title, int totalCount, int sourceType) {
+ mSlotIndex = slotIndex;
+ mTitle = title;
+ mTotalCount = totalCount;
+ mSourceType = sourceType;
+ }
+
+ @Override
+ protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+ return mThreadPool.submit(mLabelMaker.requestLabel(
+ mTitle, String.valueOf(mTotalCount), mSourceType), l);
+ }
+
+ @Override
+ protected void onLoadComplete(Bitmap bitmap) {
+ mHandler.obtainMessage(MSG_UPDATE_ALBUM_ENTRY, this).sendToTarget();
+ }
+
+ @Override
+ public void updateEntry() {
+ Bitmap bitmap = getBitmap();
+ if (bitmap == null) return; // Error or recycled
+
+ AlbumSetEntry entry = mData[mSlotIndex % mData.length];
+ BitmapTexture texture = new BitmapTexture(bitmap);
+ texture.setOpaque(false);
+ entry.labelTexture = texture;
+
+ if (isActiveSlot(mSlotIndex)) {
+ mLabelUploader.addFgTexture(texture);
+ --mActiveRequestCount;
+ if (mActiveRequestCount == 0) requestNonactiveImages();
+ if (mListener != null) mListener.onContentChanged();
+ } else {
+ mLabelUploader.addBgTexture(texture);
+ }
+ }
+ }
+
+ public void onSlotSizeChanged(int width, int height) {
+ if (mSlotWidth == width) return;
+
+ mSlotWidth = width;
+ mLoadingLabel = null;
+ mLabelMaker.setLabelWidth(mSlotWidth);
+
+ if (!mIsActive) return;
+
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ AlbumSetEntry entry = mData[i % mData.length];
+ if (entry.labelLoader != null) {
+ entry.labelLoader.recycle();
+ entry.labelLoader = null;
+ entry.labelTexture = null;
+ }
+ if (entry.album != null) {
+ entry.labelLoader = new AlbumLabelLoader(i,
+ entry.title, entry.totalCount, entry.sourceType);
+ }
+ }
+ updateAllImageRequests();
+ updateTextureUploadQueue();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java
new file mode 100644
index 000000000..5332ef89a
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSetSlotRenderer.java
@@ -0,0 +1,242 @@
+/*
+ * 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.AbstractGalleryActivity;
+import com.android.gallery3d.app.AlbumSetDataLoader;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.ColorTexture;
+import com.android.gallery3d.glrenderer.FadeInTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.glrenderer.TiledTexture;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+import com.android.gallery3d.ui.AlbumSetSlidingWindow.AlbumSetEntry;
+
+public class AlbumSetSlotRenderer extends AbstractSlotRenderer {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSetView";
+ private static final int CACHE_SIZE = 96;
+ private final int mPlaceholderColor;
+
+ private final ColorTexture mWaitLoadingTexture;
+ private final ResourceTexture mCameraOverlay;
+ private final AbstractGalleryActivity mActivity;
+ private final SelectionManager mSelectionManager;
+ protected final LabelSpec mLabelSpec;
+
+ protected AlbumSetSlidingWindow mDataWindow;
+ private SlotView mSlotView;
+
+ private int mPressedIndex = -1;
+ private boolean mAnimatePressedUp;
+ private Path mHighlightItemPath = null;
+ private boolean mInSelectionMode;
+
+ public static class LabelSpec {
+ public int labelBackgroundHeight;
+ public int titleOffset;
+ public int countOffset;
+ public int titleFontSize;
+ public int countFontSize;
+ public int leftMargin;
+ public int iconSize;
+ public int titleRightMargin;
+ public int backgroundColor;
+ public int titleColor;
+ public int countColor;
+ public int borderSize;
+ }
+
+ public AlbumSetSlotRenderer(AbstractGalleryActivity activity,
+ SelectionManager selectionManager,
+ SlotView slotView, LabelSpec labelSpec, int placeholderColor) {
+ super (activity);
+ mActivity = activity;
+ mSelectionManager = selectionManager;
+ mSlotView = slotView;
+ mLabelSpec = labelSpec;
+ mPlaceholderColor = placeholderColor;
+
+ mWaitLoadingTexture = new ColorTexture(mPlaceholderColor);
+ mWaitLoadingTexture.setSize(1, 1);
+ mCameraOverlay = new ResourceTexture(activity,
+ R.drawable.ic_cameraalbum_overlay);
+ }
+
+ public void setPressedIndex(int index) {
+ if (mPressedIndex == index) return;
+ mPressedIndex = index;
+ mSlotView.invalidate();
+ }
+
+ public void setPressedUp() {
+ if (mPressedIndex == -1) return;
+ mAnimatePressedUp = true;
+ mSlotView.invalidate();
+ }
+
+ public void setHighlightItemPath(Path path) {
+ if (mHighlightItemPath == path) return;
+ mHighlightItemPath = path;
+ mSlotView.invalidate();
+ }
+
+ public void setModel(AlbumSetDataLoader model) {
+ if (mDataWindow != null) {
+ mDataWindow.setListener(null);
+ mDataWindow = null;
+ mSlotView.setSlotCount(0);
+ }
+ if (model != null) {
+ mDataWindow = new AlbumSetSlidingWindow(
+ mActivity, model, mLabelSpec, CACHE_SIZE);
+ mDataWindow.setListener(new MyCacheListener());
+ mSlotView.setSlotCount(mDataWindow.size());
+ }
+ }
+
+ private static Texture checkLabelTexture(Texture texture) {
+ return ((texture instanceof UploadedTexture)
+ && ((UploadedTexture) texture).isUploading())
+ ? null
+ : texture;
+ }
+
+ private static Texture checkContentTexture(Texture texture) {
+ return ((texture instanceof TiledTexture)
+ && !((TiledTexture) texture).isReady())
+ ? null
+ : texture;
+ }
+
+ @Override
+ public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+ AlbumSetEntry entry = mDataWindow.get(index);
+ int renderRequestFlags = 0;
+ renderRequestFlags |= renderContent(canvas, entry, width, height);
+ renderRequestFlags |= renderLabel(canvas, entry, width, height);
+ renderRequestFlags |= renderOverlay(canvas, index, entry, width, height);
+ return renderRequestFlags;
+ }
+
+ protected int renderOverlay(
+ GLCanvas canvas, int index, AlbumSetEntry entry, int width, int height) {
+ int renderRequestFlags = 0;
+ if (entry.album != null && entry.album.isCameraRoll()) {
+ int uncoveredHeight = height - mLabelSpec.labelBackgroundHeight;
+ int dim = uncoveredHeight / 2;
+ mCameraOverlay.draw(canvas, (width - dim) / 2,
+ (uncoveredHeight - dim) / 2, dim, dim);
+ }
+ if (mPressedIndex == index) {
+ if (mAnimatePressedUp) {
+ drawPressedUpFrame(canvas, width, height);
+ renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+ if (isPressedUpFrameFinished()) {
+ mAnimatePressedUp = false;
+ mPressedIndex = -1;
+ }
+ } else {
+ drawPressedFrame(canvas, width, height);
+ }
+ } else if ((mHighlightItemPath != null) && (mHighlightItemPath == entry.setPath)) {
+ drawSelectedFrame(canvas, width, height);
+ } else if (mInSelectionMode && mSelectionManager.isItemSelected(entry.setPath)) {
+ drawSelectedFrame(canvas, width, height);
+ }
+ return renderRequestFlags;
+ }
+
+ protected int renderContent(
+ GLCanvas canvas, AlbumSetEntry entry, int width, int height) {
+ int renderRequestFlags = 0;
+
+ Texture content = checkContentTexture(entry.content);
+ if (content == null) {
+ content = mWaitLoadingTexture;
+ entry.isWaitLoadingDisplayed = true;
+ } else if (entry.isWaitLoadingDisplayed) {
+ entry.isWaitLoadingDisplayed = false;
+ content = new FadeInTexture(mPlaceholderColor, entry.bitmapTexture);
+ entry.content = content;
+ }
+ drawContent(canvas, content, width, height, entry.rotation);
+ if ((content instanceof FadeInTexture) &&
+ ((FadeInTexture) content).isAnimating()) {
+ renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+ }
+
+ return renderRequestFlags;
+ }
+
+ protected int renderLabel(
+ GLCanvas canvas, AlbumSetEntry entry, int width, int height) {
+ Texture content = checkLabelTexture(entry.labelTexture);
+ if (content == null) {
+ content = mWaitLoadingTexture;
+ }
+ int b = AlbumLabelMaker.getBorderSize();
+ int h = mLabelSpec.labelBackgroundHeight;
+ content.draw(canvas, -b, height - h + b, width + b + b, h);
+
+ return 0;
+ }
+
+ @Override
+ public void prepareDrawing() {
+ mInSelectionMode = mSelectionManager.inSelectionMode();
+ }
+
+ private class MyCacheListener implements AlbumSetSlidingWindow.Listener {
+
+ @Override
+ public void onSizeChanged(int size) {
+ mSlotView.setSlotCount(size);
+ }
+
+ @Override
+ public void onContentChanged() {
+ mSlotView.invalidate();
+ }
+ }
+
+ public void pause() {
+ mDataWindow.pause();
+ }
+
+ public void resume() {
+ mDataWindow.resume();
+ }
+
+ @Override
+ public void onVisibleRangeChanged(int visibleStart, int visibleEnd) {
+ if (mDataWindow != null) {
+ mDataWindow.setActiveWindow(visibleStart, visibleEnd);
+ }
+ }
+
+ @Override
+ public void onSlotSizeChanged(int width, int height) {
+ if (mDataWindow != null) {
+ mDataWindow.onSlotSizeChanged(width, height);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSlidingWindow.java b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
new file mode 100644
index 000000000..fec7d1e92
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSlidingWindow.java
@@ -0,0 +1,365 @@
+/*
+ * 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.os.Message;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.app.AlbumDataLoader;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.MediaObject.PanoramaSupportCallback;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.glrenderer.TiledTexture;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.JobLimiter;
+
+public class AlbumSlidingWindow implements AlbumDataLoader.DataListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumSlidingWindow";
+
+ private static final int MSG_UPDATE_ENTRY = 0;
+ private static final int JOB_LIMIT = 2;
+
+ public static interface Listener {
+ public void onSizeChanged(int size);
+ public void onContentChanged();
+ }
+
+ public static class AlbumEntry {
+ public MediaItem item;
+ public Path path;
+ public boolean isPanorama;
+ public int rotation;
+ public int mediaType;
+ public boolean isWaitDisplayed;
+ public TiledTexture bitmapTexture;
+ public Texture content;
+ private BitmapLoader contentLoader;
+ private PanoSupportListener mPanoSupportListener;
+ }
+
+ private final AlbumDataLoader mSource;
+ private final AlbumEntry mData[];
+ private final SynchronizedHandler mHandler;
+ private final JobLimiter mThreadPool;
+ private final TiledTexture.Uploader mTileUploader;
+
+ private int mSize;
+
+ private int mContentStart = 0;
+ private int mContentEnd = 0;
+
+ private int mActiveStart = 0;
+ private int mActiveEnd = 0;
+
+ private Listener mListener;
+
+ private int mActiveRequestCount = 0;
+ private boolean mIsActive = false;
+
+ private class PanoSupportListener implements PanoramaSupportCallback {
+ public final AlbumEntry mEntry;
+ public PanoSupportListener (AlbumEntry entry) {
+ mEntry = entry;
+ }
+ @Override
+ public void panoramaInfoAvailable(MediaObject mediaObject, boolean isPanorama,
+ boolean isPanorama360) {
+ if (mEntry != null) mEntry.isPanorama = isPanorama;
+ }
+ }
+
+ public AlbumSlidingWindow(AbstractGalleryActivity activity,
+ AlbumDataLoader source, int cacheSize) {
+ source.setDataListener(this);
+ mSource = source;
+ mData = new AlbumEntry[cacheSize];
+ mSize = source.size();
+
+ mHandler = new SynchronizedHandler(activity.getGLRoot()) {
+ @Override
+ public void handleMessage(Message message) {
+ Utils.assertTrue(message.what == MSG_UPDATE_ENTRY);
+ ((ThumbnailLoader) message.obj).updateEntry();
+ }
+ };
+
+ mThreadPool = new JobLimiter(activity.getThreadPool(), JOB_LIMIT);
+ mTileUploader = new TiledTexture.Uploader(activity.getGLRoot());
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public AlbumEntry get(int slotIndex) {
+ if (!isActiveSlot(slotIndex)) {
+ Utils.fail("invalid slot: %s outsides (%s, %s)",
+ slotIndex, mActiveStart, mActiveEnd);
+ }
+ return mData[slotIndex % mData.length];
+ }
+
+ 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) {
+ if (!(start <= end && end - start <= mData.length && end <= mSize)) {
+ Utils.fail("%s, %s, %s, %s", start, end, mData.length, mSize);
+ }
+ AlbumEntry data[] = mData;
+
+ mActiveStart = start;
+ mActiveEnd = end;
+
+ 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);
+ updateTextureUploadQueue();
+ if (mIsActive) updateAllImageRequests();
+ }
+
+ private void uploadBgTextureInSlot(int index) {
+ if (index < mContentEnd && index >= mContentStart) {
+ AlbumEntry entry = mData[index % mData.length];
+ if (entry.bitmapTexture != null) {
+ mTileUploader.addTexture(entry.bitmapTexture);
+ }
+ }
+ }
+
+ private void updateTextureUploadQueue() {
+ if (!mIsActive) return;
+ mTileUploader.clear();
+
+ // add foreground textures
+ for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+ AlbumEntry entry = mData[i % mData.length];
+ if (entry.bitmapTexture != null) {
+ mTileUploader.addTexture(entry.bitmapTexture);
+ }
+ }
+
+ // add background textures
+ int range = Math.max(
+ (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+ for (int i = 0; i < range; ++i) {
+ uploadBgTextureInSlot(mActiveEnd + i);
+ uploadBgTextureInSlot(mActiveStart - i - 1);
+ }
+ }
+
+ // 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);
+ requestSlotImage(mActiveStart - 1 - i);
+ }
+ }
+
+ // return whether the request is in progress or not
+ private boolean requestSlotImage(int slotIndex) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return false;
+ AlbumEntry entry = mData[slotIndex % mData.length];
+ if (entry.content != null || entry.item == null) return false;
+
+ // Set up the panorama callback
+ entry.mPanoSupportListener = new PanoSupportListener(entry);
+ entry.item.getPanoramaSupport(entry.mPanoSupportListener);
+
+ entry.contentLoader.startLoad();
+ return entry.contentLoader.isRequestInProgress();
+ }
+
+ private void cancelNonactiveImages() {
+ int range = Math.max(
+ (mContentEnd - mActiveEnd), (mActiveStart - mContentStart));
+ for (int i = 0 ;i < range; ++i) {
+ cancelSlotImage(mActiveEnd + i);
+ cancelSlotImage(mActiveStart - 1 - i);
+ }
+ }
+
+ private void cancelSlotImage(int slotIndex) {
+ if (slotIndex < mContentStart || slotIndex >= mContentEnd) return;
+ AlbumEntry item = mData[slotIndex % mData.length];
+ if (item.contentLoader != null) item.contentLoader.cancelLoad();
+ }
+
+ private void freeSlotContent(int slotIndex) {
+ AlbumEntry data[] = mData;
+ int index = slotIndex % data.length;
+ AlbumEntry entry = data[index];
+ if (entry.contentLoader != null) entry.contentLoader.recycle();
+ if (entry.bitmapTexture != null) entry.bitmapTexture.recycle();
+ data[index] = null;
+ }
+
+ private void prepareSlotContent(int slotIndex) {
+ AlbumEntry entry = new AlbumEntry();
+ MediaItem item = mSource.get(slotIndex); // item could be null;
+ entry.item = item;
+ entry.mediaType = (item == null)
+ ? MediaItem.MEDIA_TYPE_UNKNOWN
+ : entry.item.getMediaType();
+ entry.path = (item == null) ? null : item.getPath();
+ entry.rotation = (item == null) ? 0 : item.getRotation();
+ entry.contentLoader = new ThumbnailLoader(slotIndex, entry.item);
+ mData[slotIndex % mData.length] = entry;
+ }
+
+ private void updateAllImageRequests() {
+ mActiveRequestCount = 0;
+ for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) {
+ if (requestSlotImage(i)) ++mActiveRequestCount;
+ }
+ if (mActiveRequestCount == 0) {
+ requestNonactiveImages();
+ } else {
+ cancelNonactiveImages();
+ }
+ }
+
+ private class ThumbnailLoader extends BitmapLoader {
+ private final int mSlotIndex;
+ private final MediaItem mItem;
+
+ public ThumbnailLoader(int slotIndex, MediaItem item) {
+ mSlotIndex = slotIndex;
+ mItem = item;
+ }
+
+ @Override
+ protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+ return mThreadPool.submit(
+ mItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL), this);
+ }
+
+ @Override
+ protected void onLoadComplete(Bitmap bitmap) {
+ mHandler.obtainMessage(MSG_UPDATE_ENTRY, this).sendToTarget();
+ }
+
+ public void updateEntry() {
+ Bitmap bitmap = getBitmap();
+ if (bitmap == null) return; // error or recycled
+ AlbumEntry entry = mData[mSlotIndex % mData.length];
+ entry.bitmapTexture = new TiledTexture(bitmap);
+ entry.content = entry.bitmapTexture;
+
+ if (isActiveSlot(mSlotIndex)) {
+ mTileUploader.addTexture(entry.bitmapTexture);
+ --mActiveRequestCount;
+ if (mActiveRequestCount == 0) requestNonactiveImages();
+ if (mListener != null) mListener.onContentChanged();
+ } else {
+ mTileUploader.addTexture(entry.bitmapTexture);
+ }
+ }
+ }
+
+ @Override
+ public void onSizeChanged(int size) {
+ if (mSize != size) {
+ mSize = size;
+ if (mListener != null) mListener.onSizeChanged(mSize);
+ if (mContentEnd > mSize) mContentEnd = mSize;
+ if (mActiveEnd > mSize) mActiveEnd = mSize;
+ }
+ }
+
+ @Override
+ public void onContentChanged(int index) {
+ if (index >= mContentStart && index < mContentEnd && mIsActive) {
+ freeSlotContent(index);
+ prepareSlotContent(index);
+ updateAllImageRequests();
+ if (mListener != null && isActiveSlot(index)) {
+ mListener.onContentChanged();
+ }
+ }
+ }
+
+ public void resume() {
+ mIsActive = true;
+ TiledTexture.prepareResources();
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ prepareSlotContent(i);
+ }
+ updateAllImageRequests();
+ }
+
+ public void pause() {
+ mIsActive = false;
+ mTileUploader.clear();
+ TiledTexture.freeResources();
+ for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
+ freeSlotContent(i);
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AlbumSlotRenderer.java b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java
new file mode 100644
index 000000000..dc6c89b0e
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AlbumSlotRenderer.java
@@ -0,0 +1,201 @@
+/*
+ * 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.AbstractGalleryActivity;
+import com.android.gallery3d.app.AlbumDataLoader;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.ColorTexture;
+import com.android.gallery3d.glrenderer.FadeInTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.glrenderer.TiledTexture;
+
+public class AlbumSlotRenderer extends AbstractSlotRenderer {
+ @SuppressWarnings("unused")
+ private static final String TAG = "AlbumView";
+
+ public interface SlotFilter {
+ public boolean acceptSlot(int index);
+ }
+
+ private final int mPlaceholderColor;
+ private static final int CACHE_SIZE = 96;
+
+ private AlbumSlidingWindow mDataWindow;
+ private final AbstractGalleryActivity mActivity;
+ private final ColorTexture mWaitLoadingTexture;
+ private final SlotView mSlotView;
+ private final SelectionManager mSelectionManager;
+
+ private int mPressedIndex = -1;
+ private boolean mAnimatePressedUp;
+ private Path mHighlightItemPath = null;
+ private boolean mInSelectionMode;
+
+ private SlotFilter mSlotFilter;
+
+ public AlbumSlotRenderer(AbstractGalleryActivity activity, SlotView slotView,
+ SelectionManager selectionManager, int placeholderColor) {
+ super(activity);
+ mActivity = activity;
+ mSlotView = slotView;
+ mSelectionManager = selectionManager;
+ mPlaceholderColor = placeholderColor;
+
+ mWaitLoadingTexture = new ColorTexture(mPlaceholderColor);
+ mWaitLoadingTexture.setSize(1, 1);
+ }
+
+ public void setPressedIndex(int index) {
+ if (mPressedIndex == index) return;
+ mPressedIndex = index;
+ mSlotView.invalidate();
+ }
+
+ public void setPressedUp() {
+ if (mPressedIndex == -1) return;
+ mAnimatePressedUp = true;
+ mSlotView.invalidate();
+ }
+
+ public void setHighlightItemPath(Path path) {
+ if (mHighlightItemPath == path) return;
+ mHighlightItemPath = path;
+ mSlotView.invalidate();
+ }
+
+ public void setModel(AlbumDataLoader model) {
+ if (mDataWindow != null) {
+ mDataWindow.setListener(null);
+ mSlotView.setSlotCount(0);
+ mDataWindow = null;
+ }
+ if (model != null) {
+ mDataWindow = new AlbumSlidingWindow(mActivity, model, CACHE_SIZE);
+ mDataWindow.setListener(new MyDataModelListener());
+ mSlotView.setSlotCount(model.size());
+ }
+ }
+
+ private static Texture checkTexture(Texture texture) {
+ return (texture instanceof TiledTexture)
+ && !((TiledTexture) texture).isReady()
+ ? null
+ : texture;
+ }
+
+ @Override
+ public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+ if (mSlotFilter != null && !mSlotFilter.acceptSlot(index)) return 0;
+
+ AlbumSlidingWindow.AlbumEntry entry = mDataWindow.get(index);
+
+ int renderRequestFlags = 0;
+
+ Texture content = checkTexture(entry.content);
+ if (content == null) {
+ content = mWaitLoadingTexture;
+ entry.isWaitDisplayed = true;
+ } else if (entry.isWaitDisplayed) {
+ entry.isWaitDisplayed = false;
+ content = new FadeInTexture(mPlaceholderColor, entry.bitmapTexture);
+ entry.content = content;
+ }
+ drawContent(canvas, content, width, height, entry.rotation);
+ if ((content instanceof FadeInTexture) &&
+ ((FadeInTexture) content).isAnimating()) {
+ renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+ }
+
+ if (entry.mediaType == MediaObject.MEDIA_TYPE_VIDEO) {
+ drawVideoOverlay(canvas, width, height);
+ }
+
+ if (entry.isPanorama) {
+ drawPanoramaIcon(canvas, width, height);
+ }
+
+ renderRequestFlags |= renderOverlay(canvas, index, entry, width, height);
+
+ return renderRequestFlags;
+ }
+
+ private int renderOverlay(GLCanvas canvas, int index,
+ AlbumSlidingWindow.AlbumEntry entry, int width, int height) {
+ int renderRequestFlags = 0;
+ if (mPressedIndex == index) {
+ if (mAnimatePressedUp) {
+ drawPressedUpFrame(canvas, width, height);
+ renderRequestFlags |= SlotView.RENDER_MORE_FRAME;
+ if (isPressedUpFrameFinished()) {
+ mAnimatePressedUp = false;
+ mPressedIndex = -1;
+ }
+ } else {
+ drawPressedFrame(canvas, width, height);
+ }
+ } else if ((entry.path != null) && (mHighlightItemPath == entry.path)) {
+ drawSelectedFrame(canvas, width, height);
+ } else if (mInSelectionMode && mSelectionManager.isItemSelected(entry.path)) {
+ drawSelectedFrame(canvas, width, height);
+ }
+ return renderRequestFlags;
+ }
+
+ private class MyDataModelListener implements AlbumSlidingWindow.Listener {
+ @Override
+ public void onContentChanged() {
+ mSlotView.invalidate();
+ }
+
+ @Override
+ public void onSizeChanged(int size) {
+ mSlotView.setSlotCount(size);
+ }
+ }
+
+ public void resume() {
+ mDataWindow.resume();
+ }
+
+ public void pause() {
+ mDataWindow.pause();
+ }
+
+ @Override
+ public void prepareDrawing() {
+ mInSelectionMode = mSelectionManager.inSelectionMode();
+ }
+
+ @Override
+ public void onVisibleRangeChanged(int visibleStart, int visibleEnd) {
+ if (mDataWindow != null) {
+ mDataWindow.setActiveWindow(visibleStart, visibleEnd);
+ }
+ }
+
+ @Override
+ public void onSlotSizeChanged(int width, int height) {
+ // Do nothing
+ }
+
+ public void setSlotFilter(SlotFilter slotFilter) {
+ mSlotFilter = slotFilter;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/AnimationTime.java b/src/com/android/gallery3d/ui/AnimationTime.java
new file mode 100644
index 000000000..063677423
--- /dev/null
+++ b/src/com/android/gallery3d/ui/AnimationTime.java
@@ -0,0 +1,45 @@
+
+/*
+ * Copyright (C) 2012 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.os.SystemClock;
+
+//
+// The animation time should ideally be the vsync time the frame will be
+// displayed, but that is an unknown time in the future. So we use the system
+// time just after eglSwapBuffers (when GLSurfaceView.onDrawFrame is called)
+// as a approximation.
+//
+public class AnimationTime {
+ private static volatile long sTime;
+
+ // Sets current time as the animation time.
+ public static void update() {
+ sTime = SystemClock.uptimeMillis();
+ }
+
+ // Returns the animation time.
+ public static long get() {
+ return sTime;
+ }
+
+ public static long startTime() {
+ sTime = SystemClock.uptimeMillis();
+ return sTime;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/BitmapLoader.java b/src/com/android/gallery3d/ui/BitmapLoader.java
new file mode 100644
index 000000000..a708a90f3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapLoader.java
@@ -0,0 +1,108 @@
+/*
+ * 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 com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+
+// We use this class to
+// 1.) load bitmaps in background.
+// 2.) as a place holder for the loaded bitmap
+public abstract class BitmapLoader implements FutureListener<Bitmap> {
+ @SuppressWarnings("unused")
+ private static final String TAG = "BitmapLoader";
+
+ /* Transition Map:
+ * INIT -> REQUESTED, RECYCLED
+ * REQUESTED -> INIT (cancel), LOADED, ERROR, RECYCLED
+ * LOADED, ERROR -> RECYCLED
+ */
+ private static final int STATE_INIT = 0;
+ private static final int STATE_REQUESTED = 1;
+ private static final int STATE_LOADED = 2;
+ private static final int STATE_ERROR = 3;
+ private static final int STATE_RECYCLED = 4;
+
+ private int mState = STATE_INIT;
+ // mTask is not null only when a task is on the way
+ private Future<Bitmap> mTask;
+ private Bitmap mBitmap;
+
+ @Override
+ public void onFutureDone(Future<Bitmap> future) {
+ synchronized (this) {
+ mTask = null;
+ mBitmap = future.get();
+ if (mState == STATE_RECYCLED) {
+ if (mBitmap != null) {
+ GalleryBitmapPool.getInstance().put(mBitmap);
+ mBitmap = null;
+ }
+ return; // don't call callback
+ }
+ if (future.isCancelled() && mBitmap == null) {
+ if (mState == STATE_REQUESTED) mTask = submitBitmapTask(this);
+ return; // don't call callback
+ } else {
+ mState = mBitmap == null ? STATE_ERROR : STATE_LOADED;
+ }
+ }
+ onLoadComplete(mBitmap);
+ }
+
+ public synchronized void startLoad() {
+ if (mState == STATE_INIT) {
+ mState = STATE_REQUESTED;
+ if (mTask == null) mTask = submitBitmapTask(this);
+ }
+ }
+
+ public synchronized void cancelLoad() {
+ if (mState == STATE_REQUESTED) {
+ mState = STATE_INIT;
+ if (mTask != null) mTask.cancel();
+ }
+ }
+
+ // Recycle the loader and the bitmap
+ public synchronized void recycle() {
+ mState = STATE_RECYCLED;
+ if (mBitmap != null) {
+ GalleryBitmapPool.getInstance().put(mBitmap);
+ mBitmap = null;
+ }
+ if (mTask != null) mTask.cancel();
+ }
+
+ public synchronized boolean isRequestInProgress() {
+ return mState == STATE_REQUESTED;
+ }
+
+ public synchronized boolean isRecycled() {
+ return mState == STATE_RECYCLED;
+ }
+
+ public synchronized Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ abstract protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l);
+ abstract protected void onLoadComplete(Bitmap bitmap);
+}
diff --git a/src/com/android/gallery3d/ui/BitmapScreenNail.java b/src/com/android/gallery3d/ui/BitmapScreenNail.java
new file mode 100644
index 000000000..a3d403946
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapScreenNail.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2012 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.RectF;
+
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+public class BitmapScreenNail implements ScreenNail {
+ private final BitmapTexture mBitmapTexture;
+
+ public BitmapScreenNail(Bitmap bitmap) {
+ mBitmapTexture = new BitmapTexture(bitmap);
+ }
+
+ @Override
+ public int getWidth() {
+ return mBitmapTexture.getWidth();
+ }
+
+ @Override
+ public int getHeight() {
+ return mBitmapTexture.getHeight();
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+ mBitmapTexture.draw(canvas, x, y, width, height);
+ }
+
+ @Override
+ public void noDraw() {
+ // do nothing
+ }
+
+ @Override
+ public void recycle() {
+ mBitmapTexture.recycle();
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, RectF source, RectF dest) {
+ canvas.drawTexture(mBitmapTexture, source, dest);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/BitmapTileProvider.java b/src/com/android/gallery3d/ui/BitmapTileProvider.java
new file mode 100644
index 000000000..e1a8b7644
--- /dev/null
+++ b/src/com/android/gallery3d/ui/BitmapTileProvider.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.ui;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.photos.data.GalleryBitmapPool;
+
+import java.util.ArrayList;
+
+public class BitmapTileProvider implements TileImageView.TileSource {
+ private final ScreenNail mScreenNail;
+ 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<Bitmap> list = new ArrayList<Bitmap>();
+ list.add(bitmap);
+ while (bitmap.getWidth() > maxBackupSize
+ || bitmap.getHeight() > maxBackupSize) {
+ bitmap = BitmapUtils.resizeBitmapByScale(bitmap, 0.5f, false);
+ list.add(bitmap);
+ }
+
+ mScreenNail = new BitmapScreenNail(list.remove(list.size() - 1));
+ mMipmaps = list.toArray(new Bitmap[list.size()]);
+ mConfig = Config.ARGB_8888;
+ }
+
+ @Override
+ public ScreenNail getScreenNail() {
+ return mScreenNail;
+ }
+
+ @Override
+ public int getImageHeight() {
+ return mImageHeight;
+ }
+
+ @Override
+ public int getImageWidth() {
+ return mImageWidth;
+ }
+
+ @Override
+ public int getLevelCount() {
+ return mMipmaps.length;
+ }
+
+ @Override
+ public Bitmap getTile(int level, int x, int y, int tileSize) {
+ x >>= level;
+ y >>= level;
+
+ Bitmap result = GalleryBitmapPool.getInstance().get(tileSize, tileSize);
+ if (result == null) {
+ result = Bitmap.createBitmap(tileSize, tileSize, mConfig);
+ } else {
+ result.eraseColor(0);
+ }
+
+ Bitmap mipmap = mMipmaps[level];
+ Canvas canvas = new Canvas(result);
+ int offsetX = -x;
+ int offsetY = -y;
+ canvas.drawBitmap(mipmap, offsetX, offsetY, null);
+ return result;
+ }
+
+ public void recycle() {
+ if (mRecycled) return;
+ mRecycled = true;
+ for (Bitmap bitmap : mMipmaps) {
+ BitmapUtils.recycleSilently(bitmap);
+ }
+ if (mScreenNail != null) {
+ mScreenNail.recycle();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java b/src/com/android/gallery3d/ui/CacheStorageUsageInfo.java
new file mode 100644
index 000000000..46f7a2433
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CacheStorageUsageInfo.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.ui;
+
+import android.content.Context;
+import android.os.StatFs;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.File;
+
+public class CacheStorageUsageInfo {
+ @SuppressWarnings("unused")
+ private static final String TAG = "CacheStorageUsageInfo";
+
+ // number of bytes the storage has.
+ private long mTotalBytes;
+
+ // number of bytes already used.
+ private long mUsedBytes;
+
+ // number of bytes used for the cache (should be less then usedBytes).
+ private long mUsedCacheBytes;
+
+ // number of bytes used for the cache if all pending downloads (and removals) are completed.
+ private long mTargetCacheBytes;
+
+ private AbstractGalleryActivity mActivity;
+ private Context mContext;
+ private long mUserChangeDelta;
+
+ public CacheStorageUsageInfo(AbstractGalleryActivity activity) {
+ mActivity = activity;
+ mContext = activity.getAndroidContext();
+ }
+
+ public void increaseTargetCacheSize(long delta) {
+ mUserChangeDelta += delta;
+ }
+
+ public void loadStorageInfo(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();
+
+ mTotalBytes = blockSize * totalBlocks;
+ mUsedBytes = blockSize * (totalBlocks - availableBlocks);
+ mUsedCacheBytes = mActivity.getDataManager().getTotalUsedCacheSize();
+ mTargetCacheBytes = mActivity.getDataManager().getTotalTargetCacheSize();
+ }
+
+ public long getTotalBytes() {
+ return mTotalBytes;
+ }
+
+ public long getExpectedUsedBytes() {
+ return mUsedBytes - mUsedCacheBytes + mTargetCacheBytes + mUserChangeDelta;
+ }
+
+ public long getUsedBytes() {
+ // Should it be usedBytes - usedCacheBytes + targetCacheBytes ?
+ return mUsedBytes;
+ }
+
+ public long getFreeBytes() {
+ return mTotalBytes - mUsedBytes;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/CaptureAnimation.java b/src/com/android/gallery3d/ui/CaptureAnimation.java
new file mode 100644
index 000000000..87c054ab3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/CaptureAnimation.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+public class CaptureAnimation {
+ // The amount of change for zooming out.
+ private static final float ZOOM_DELTA = 0.2f;
+ // Pre-calculated value for convenience.
+ private static final float ZOOM_IN_BEGIN = 1f - ZOOM_DELTA;
+
+ private static final Interpolator sZoomOutInterpolator =
+ new DecelerateInterpolator();
+ private static final Interpolator sZoomInInterpolator =
+ new AccelerateInterpolator();
+ private static final Interpolator sSlideInterpolator =
+ new AccelerateDecelerateInterpolator();
+
+ // Calculate the slide factor based on the give time fraction.
+ public static float calculateSlide(float fraction) {
+ return sSlideInterpolator.getInterpolation(fraction);
+ }
+
+ // Calculate the scale factor based on the given time fraction.
+ public static float calculateScale(float fraction) {
+ float value;
+ if (fraction <= 0.5f) {
+ // Zoom in for the beginning.
+ value = 1f - ZOOM_DELTA *
+ sZoomOutInterpolator.getInterpolation(fraction * 2);
+ } else {
+ // Zoom out for the ending.
+ value = ZOOM_IN_BEGIN + ZOOM_DELTA *
+ sZoomInInterpolator.getInterpolation((fraction - 0.5f) * 2f);
+ }
+ return value;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DetailsAddressResolver.java b/src/com/android/gallery3d/ui/DetailsAddressResolver.java
new file mode 100644
index 000000000..8de667745
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DetailsAddressResolver.java
@@ -0,0 +1,118 @@
+/*
+ * 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 android.content.Context;
+import android.location.Address;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.ReverseGeocoder;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+public class DetailsAddressResolver {
+ private AddressResolvingListener mListener;
+ private final AbstractGalleryActivity mContext;
+ private Future<Address> mAddressLookupJob;
+ private final Handler mHandler;
+
+ private class AddressLookupJob implements Job<Address> {
+ private double[] mLatlng;
+
+ protected AddressLookupJob(double[] latlng) {
+ mLatlng = latlng;
+ }
+
+ @Override
+ public Address run(JobContext jc) {
+ ReverseGeocoder geocoder = new ReverseGeocoder(mContext.getAndroidContext());
+ return geocoder.lookupAddress(mLatlng[0], mLatlng[1], true);
+ }
+ }
+
+ public interface AddressResolvingListener {
+ public void onAddressAvailable(String address);
+ }
+
+ public DetailsAddressResolver(AbstractGalleryActivity context) {
+ mContext = context;
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ public String resolveAddress(double[] latlng, AddressResolvingListener listener) {
+ mListener = listener;
+ mAddressLookupJob = mContext.getThreadPool().submit(
+ new AddressLookupJob(latlng),
+ new FutureListener<Address>() {
+ @Override
+ public void onFutureDone(final Future<Address> future) {
+ mAddressLookupJob = null;
+ if (!future.isCancelled()) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ updateLocation(future.get());
+ }
+ });
+ }
+ }
+ });
+ return GalleryUtils.formatLatitudeLongitude("(%f,%f)", latlng[0], latlng[1]);
+ }
+
+ private void updateLocation(Address address) {
+ if (address != null) {
+ 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", DetailsHelper.getDetailsName(
+ context, MediaDetails.INDEX_LOCATION), addressText);
+ mListener.onAddressAvailable(text);
+ }
+ }
+
+ public void cancel() {
+ if (mAddressLookupJob != null) {
+ mAddressLookupJob.cancel();
+ mAddressLookupJob = null;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/DetailsHelper.java b/src/com/android/gallery3d/ui/DetailsHelper.java
new file mode 100644
index 000000000..47296f655
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DetailsHelper.java
@@ -0,0 +1,148 @@
+/*
+ * 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.Bitmap;
+import android.graphics.BitmapFactory;
+import android.view.View.MeasureSpec;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.ui.DetailsAddressResolver.AddressResolvingListener;
+
+public class DetailsHelper {
+ private static DetailsAddressResolver sAddressResolver;
+ private DetailsViewContainer mContainer;
+
+ public interface DetailsSource {
+ public int size();
+ public int setIndex();
+ public MediaDetails getDetails();
+ }
+
+ public interface CloseListener {
+ public void onClose();
+ }
+
+ public interface DetailsViewContainer {
+ public void reloadDetails();
+ public void setCloseListener(CloseListener listener);
+ public void show();
+ public void hide();
+ }
+
+ public interface ResolutionResolvingListener {
+ public void onResolutionAvailable(int width, int height);
+ }
+
+ public DetailsHelper(AbstractGalleryActivity activity, GLView rootPane, DetailsSource source) {
+ mContainer = new DialogDetailsView(activity, source);
+ }
+
+ public void layout(int left, int top, int right, int bottom) {
+ if (mContainer instanceof GLView) {
+ GLView view = (GLView) mContainer;
+ view.measure(MeasureSpec.UNSPECIFIED,
+ MeasureSpec.makeMeasureSpec(bottom - top, MeasureSpec.AT_MOST));
+ view.layout(0, top, view.getMeasuredWidth(), top + view.getMeasuredHeight());
+ }
+ }
+
+ public void reloadDetails() {
+ mContainer.reloadDetails();
+ }
+
+ public void setCloseListener(CloseListener listener) {
+ mContainer.setCloseListener(listener);
+ }
+
+ public static String resolveAddress(AbstractGalleryActivity activity, double[] latlng,
+ AddressResolvingListener listener) {
+ if (sAddressResolver == null) {
+ sAddressResolver = new DetailsAddressResolver(activity);
+ } else {
+ sAddressResolver.cancel();
+ }
+ return sAddressResolver.resolveAddress(latlng, listener);
+ }
+
+ public static void resolveResolution(String path, ResolutionResolvingListener listener) {
+ Bitmap bitmap = BitmapFactory.decodeFile(path);
+ if (bitmap == null) return;
+ listener.onResolutionAvailable(bitmap.getWidth(), bitmap.getHeight());
+ }
+
+ public static void pause() {
+ if (sAddressResolver != null) sAddressResolver.cancel();
+ }
+
+ public void show() {
+ mContainer.show();
+ }
+
+ public void hide() {
+ mContainer.hide();
+ }
+
+ public static String getDetailsName(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;
+ }
+ }
+}
+
+
diff --git a/src/com/android/gallery3d/ui/DialogDetailsView.java b/src/com/android/gallery3d/ui/DialogDetailsView.java
new file mode 100644
index 000000000..058c03654
--- /dev/null
+++ b/src/com/android/gallery3d/ui/DialogDetailsView.java
@@ -0,0 +1,288 @@
+/*
+ * 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 android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.text.format.Formatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaDetails;
+import com.android.gallery3d.ui.DetailsAddressResolver.AddressResolvingListener;
+import com.android.gallery3d.ui.DetailsHelper.CloseListener;
+import com.android.gallery3d.ui.DetailsHelper.DetailsSource;
+import com.android.gallery3d.ui.DetailsHelper.DetailsViewContainer;
+import com.android.gallery3d.ui.DetailsHelper.ResolutionResolvingListener;
+
+import java.util.ArrayList;
+import java.util.Map.Entry;
+
+public class DialogDetailsView implements DetailsViewContainer {
+ @SuppressWarnings("unused")
+ private static final String TAG = "DialogDetailsView";
+
+ private final AbstractGalleryActivity mActivity;
+ private DetailsAdapter mAdapter;
+ private MediaDetails mDetails;
+ private final DetailsSource mSource;
+ private int mIndex;
+ private Dialog mDialog;
+ private CloseListener mListener;
+
+ public DialogDetailsView(AbstractGalleryActivity activity, DetailsSource source) {
+ mActivity = activity;
+ mSource = source;
+ }
+
+ @Override
+ public void show() {
+ reloadDetails();
+ mDialog.show();
+ }
+
+ @Override
+ public void hide() {
+ mDialog.hide();
+ }
+
+ @Override
+ public void reloadDetails() {
+ int index = mSource.setIndex();
+ if (index == -1) return;
+ MediaDetails details = mSource.getDetails();
+ if (details != null) {
+ if (mIndex == index && mDetails == details) return;
+ mIndex = index;
+ mDetails = details;
+ setDetails(details);
+ }
+ }
+
+ private void setDetails(MediaDetails details) {
+ mAdapter = new DetailsAdapter(details);
+ String title = String.format(
+ mActivity.getAndroidContext().getString(R.string.details_title),
+ mIndex + 1, mSource.size());
+ ListView detailsList = (ListView) LayoutInflater.from(mActivity.getAndroidContext()).inflate(
+ R.layout.details_list, null, false);
+ detailsList.setAdapter(mAdapter);
+ mDialog = new AlertDialog.Builder(mActivity)
+ .setView(detailsList)
+ .setTitle(title)
+ .setPositiveButton(R.string.close, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ mDialog.dismiss();
+ }
+ })
+ .create();
+
+ mDialog.setOnDismissListener(new OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ if (mListener != null) {
+ mListener.onClose();
+ }
+ }
+ });
+ }
+
+
+ private class DetailsAdapter extends BaseAdapter
+ implements AddressResolvingListener, ResolutionResolvingListener {
+ private final ArrayList<String> mItems;
+ private int mLocationIndex;
+ private int mWidthIndex = -1;
+ private int mHeightIndex = -1;
+
+ public DetailsAdapter(MediaDetails details) {
+ Context context = mActivity.getAndroidContext();
+ mItems = new ArrayList<String>(details.size());
+ mLocationIndex = -1;
+ setDetails(context, details);
+ }
+
+ private void setDetails(Context context, MediaDetails details) {
+ boolean resolutionIsValid = true;
+ String path = null;
+ for (Entry<Integer, Object> detail : details) {
+ String value;
+ switch (detail.getKey()) {
+ case MediaDetails.INDEX_LOCATION: {
+ double[] latlng = (double[]) detail.getValue();
+ mLocationIndex = mItems.size();
+ value = DetailsHelper.resolveAddress(mActivity, latlng, this);
+ 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;
+ }
+ case MediaDetails.INDEX_WIDTH:
+ mWidthIndex = mItems.size();
+ value = detail.getValue().toString();
+ if (value.equalsIgnoreCase("0")) {
+ value = context.getString(R.string.unknown);
+ resolutionIsValid = false;
+ }
+ break;
+ case MediaDetails.INDEX_HEIGHT: {
+ mHeightIndex = mItems.size();
+ value = detail.getValue().toString();
+ if (value.equalsIgnoreCase("0")) {
+ value = context.getString(R.string.unknown);
+ resolutionIsValid = false;
+ }
+ break;
+ }
+ case MediaDetails.INDEX_PATH:
+ // Get the path and then fall through to the default case
+ path = detail.getValue().toString();
+ default: {
+ Object valueObj = detail.getValue();
+ // This shouldn't happen, log its key to help us diagnose the problem.
+ if (valueObj == null) {
+ Utils.fail("%s's value is Null",
+ DetailsHelper.getDetailsName(context, detail.getKey()));
+ }
+ value = valueObj.toString();
+ }
+ }
+ int key = detail.getKey();
+ if (details.hasUnit(key)) {
+ value = String.format("%s: %s %s", DetailsHelper.getDetailsName(
+ context, key), value, context.getString(details.getUnit(key)));
+ } else {
+ value = String.format("%s: %s", DetailsHelper.getDetailsName(
+ context, key), value);
+ }
+ mItems.add(value);
+ if (!resolutionIsValid) {
+ DetailsHelper.resolveResolution(path, this);
+ }
+ }
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return false;
+ }
+
+ @Override
+ public int getCount() {
+ return mItems.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mDetails.getDetail(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView tv;
+ if (convertView == null) {
+ tv = (TextView) LayoutInflater.from(mActivity.getAndroidContext()).inflate(
+ R.layout.details, parent, false);
+ } else {
+ tv = (TextView) convertView;
+ }
+ tv.setText(mItems.get(position));
+ return tv;
+ }
+
+ @Override
+ public void onAddressAvailable(String address) {
+ mItems.set(mLocationIndex, address);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onResolutionAvailable(int width, int height) {
+ if (width == 0 || height == 0) return;
+ // Update the resolution with the new width and height
+ Context context = mActivity.getAndroidContext();
+ String widthString = String.format("%s: %d", DetailsHelper.getDetailsName(
+ context, MediaDetails.INDEX_WIDTH), width);
+ String heightString = String.format("%s: %d", DetailsHelper.getDetailsName(
+ context, MediaDetails.INDEX_HEIGHT), height);
+ mItems.set(mWidthIndex, String.valueOf(widthString));
+ mItems.set(mHeightIndex, String.valueOf(heightString));
+ notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void setCloseListener(CloseListener listener) {
+ mListener = listener;
+ }
+}
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/EdgeEffect.java b/src/com/android/gallery3d/ui/EdgeEffect.java
new file mode 100644
index 000000000..87ff0c5d3
--- /dev/null
+++ b/src/com/android/gallery3d/ui/EdgeEffect.java
@@ -0,0 +1,443 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+
+// This is copied from android.widget.EdgeEffect with some small modifications:
+// (1) Copy the images (overscroll_{edge|glow}.png) to local resources.
+// (2) Use "GLCanvas" instead of "Canvas" for draw()'s parameter.
+// (3) Use a private Drawable class (which inherits from ResourceTexture)
+// instead of android.graphics.drawable.Drawable to hold the images.
+// The private Drawable class is used to translate original Canvas calls to
+// corresponding GLCanvas calls.
+
+/**
+ * This class performs the graphical effect used at the edges of scrollable widgets
+ * when the user scrolls beyond the content bounds in 2D space.
+ *
+ * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
+ * instance for each edge that should show the effect, feed it input data using
+ * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()},
+ * and draw the effect using {@link #draw(Canvas)} in the widget's overridden
+ * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns
+ * false after drawing, the edge effect's animation is not yet complete and the widget
+ * should schedule another drawing pass to continue the animation.</p>
+ *
+ * <p>When drawing, widgets should draw their main content and child views first,
+ * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code>
+ * method. (This will invoke onDraw and dispatch drawing to child views as needed.)
+ * The edge effect may then be drawn on top of the view's content using the
+ * {@link #draw(Canvas)} method.</p>
+ */
+public class EdgeEffect {
+ @SuppressWarnings("unused")
+ private static final String TAG = "EdgeEffect";
+
+ // Time it will take the effect to fully recede in ms
+ private static final int RECEDE_TIME = 1000;
+
+ // Time it will take before a pulled glow begins receding in ms
+ private static final int PULL_TIME = 167;
+
+ // Time it will take in ms for a pulled glow to decay to partial strength before release
+ private static final int PULL_DECAY_TIME = 1000;
+
+ private static final float MAX_ALPHA = 0.8f;
+ private static final float HELD_EDGE_ALPHA = 0.7f;
+ private static final float HELD_EDGE_SCALE_Y = 0.5f;
+ private static final float HELD_GLOW_ALPHA = 0.5f;
+ private static final float HELD_GLOW_SCALE_Y = 0.5f;
+
+ private static final float MAX_GLOW_HEIGHT = 4.f;
+
+ private static final float PULL_GLOW_BEGIN = 1.f;
+ private static final float PULL_EDGE_BEGIN = 0.6f;
+
+ // Minimum velocity that will be absorbed
+ private static final int MIN_VELOCITY = 100;
+
+ private static final float EPSILON = 0.001f;
+
+ private final Drawable mEdge;
+ private final Drawable mGlow;
+ private int mWidth;
+ private int mHeight;
+ private final int MIN_WIDTH = 300;
+ private final int mMinWidth;
+
+ private float mEdgeAlpha;
+ private float mEdgeScaleY;
+ private float mGlowAlpha;
+ private float mGlowScaleY;
+
+ private float mEdgeAlphaStart;
+ private float mEdgeAlphaFinish;
+ private float mEdgeScaleYStart;
+ private float mEdgeScaleYFinish;
+ private float mGlowAlphaStart;
+ private float mGlowAlphaFinish;
+ private float mGlowScaleYStart;
+ private float mGlowScaleYFinish;
+
+ private long mStartTime;
+ private float mDuration;
+
+ private final Interpolator mInterpolator;
+
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_PULL = 1;
+ private static final int STATE_ABSORB = 2;
+ private static final int STATE_RECEDE = 3;
+ private static final int STATE_PULL_DECAY = 4;
+
+ // How much dragging should effect the height of the edge image.
+ // Number determined by user testing.
+ private static final int PULL_DISTANCE_EDGE_FACTOR = 7;
+
+ // How much dragging should effect the height of the glow image.
+ // Number determined by user testing.
+ private static final int PULL_DISTANCE_GLOW_FACTOR = 7;
+ private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f;
+
+ private static final int VELOCITY_EDGE_FACTOR = 8;
+ private static final int VELOCITY_GLOW_FACTOR = 16;
+
+ private int mState = STATE_IDLE;
+
+ private float mPullDistance;
+
+ /**
+ * Construct a new EdgeEffect with a theme appropriate for the provided context.
+ * @param context Context used to provide theming and resource information for the EdgeEffect
+ */
+ public EdgeEffect(Context context) {
+ mEdge = new Drawable(context, R.drawable.overscroll_edge);
+ mGlow = new Drawable(context, R.drawable.overscroll_glow);
+
+ mMinWidth = (int) (context.getResources().getDisplayMetrics().density * MIN_WIDTH + 0.5f);
+ mInterpolator = new DecelerateInterpolator();
+ }
+
+ /**
+ * Set the size of this edge effect in pixels.
+ *
+ * @param width Effect width in pixels
+ * @param height Effect height in pixels
+ */
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ /**
+ * Reports if this EdgeEffect's animation is finished. If this method returns false
+ * after a call to {@link #draw(Canvas)} the host widget should schedule another
+ * drawing pass to continue the animation.
+ *
+ * @return true if animation is finished, false if drawing should continue on the next frame.
+ */
+ public boolean isFinished() {
+ return mState == STATE_IDLE;
+ }
+
+ /**
+ * Immediately finish the current animation.
+ * After this call {@link #isFinished()} will return true.
+ */
+ public void finish() {
+ mState = STATE_IDLE;
+ }
+
+ /**
+ * A view should call this when content is pulled away from an edge by the user.
+ * This will update the state of the current visual effect and its associated animation.
+ * The host view should always {@link android.view.View#invalidate()} after this
+ * and draw the results accordingly.
+ *
+ * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
+ * 1.f (full length of the view) or negative values to express change
+ * back toward the edge reached to initiate the effect.
+ */
+ public void onPull(float deltaDistance) {
+ final long now = AnimationTime.get();
+ if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
+ return;
+ }
+ if (mState != STATE_PULL) {
+ mGlowScaleY = PULL_GLOW_BEGIN;
+ }
+ mState = STATE_PULL;
+
+ mStartTime = now;
+ mDuration = PULL_TIME;
+
+ mPullDistance += deltaDistance;
+ float distance = Math.abs(mPullDistance);
+
+ mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA));
+ mEdgeScaleY = mEdgeScaleYStart = Math.max(
+ HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f));
+
+ mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
+ mGlowAlpha +
+ (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
+
+ float glowChange = Math.abs(deltaDistance);
+ if (deltaDistance > 0 && mPullDistance < 0) {
+ glowChange = -glowChange;
+ }
+ if (mPullDistance == 0) {
+ mGlowScaleY = 0;
+ }
+
+ // Do not allow glow to get larger than MAX_GLOW_HEIGHT.
+ mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max(
+ 0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR));
+
+ mEdgeAlphaFinish = mEdgeAlpha;
+ mEdgeScaleYFinish = mEdgeScaleY;
+ mGlowAlphaFinish = mGlowAlpha;
+ mGlowScaleYFinish = mGlowScaleY;
+ }
+
+ /**
+ * Call when the object is released after being pulled.
+ * This will begin the "decay" phase of the effect. After calling this method
+ * the host view should {@link android.view.View#invalidate()} and thereby
+ * draw the results accordingly.
+ */
+ public void onRelease() {
+ mPullDistance = 0;
+
+ if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
+ return;
+ }
+
+ mState = STATE_RECEDE;
+ mEdgeAlphaStart = mEdgeAlpha;
+ mEdgeScaleYStart = mEdgeScaleY;
+ mGlowAlphaStart = mGlowAlpha;
+ mGlowScaleYStart = mGlowScaleY;
+
+ mEdgeAlphaFinish = 0.f;
+ mEdgeScaleYFinish = 0.f;
+ mGlowAlphaFinish = 0.f;
+ mGlowScaleYFinish = 0.f;
+
+ mStartTime = AnimationTime.get();
+ mDuration = RECEDE_TIME;
+ }
+
+ /**
+ * Call when the effect absorbs an impact at the given velocity.
+ * Used when a fling reaches the scroll boundary.
+ *
+ * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
+ * the method <code>getCurrVelocity</code> will provide a reasonable approximation
+ * to use here.</p>
+ *
+ * @param velocity Velocity at impact in pixels per second.
+ */
+ public void onAbsorb(int velocity) {
+ mState = STATE_ABSORB;
+ velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));
+
+ mStartTime = AnimationTime.get();
+ mDuration = 0.1f + (velocity * 0.03f);
+
+ // The edge should always be at least partially visible, regardless
+ // of velocity.
+ mEdgeAlphaStart = 0.f;
+ mEdgeScaleY = mEdgeScaleYStart = 0.f;
+ // The glow depends more on the velocity, and therefore starts out
+ // nearly invisible.
+ mGlowAlphaStart = 0.5f;
+ mGlowScaleYStart = 0.f;
+
+ // Factor the velocity by 8. Testing on device shows this works best to
+ // reflect the strength of the user's scrolling.
+ mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1));
+ // Edge should never get larger than the size of its asset.
+ mEdgeScaleYFinish = Math.max(
+ HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f));
+
+ // Growth for the size of the glow should be quadratic to properly
+ // respond
+ // to a user's scrolling speed. The faster the scrolling speed, the more
+ // intense the effect should be for both the size and the saturation.
+ mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f);
+ // Alpha should change for the glow as well as size.
+ mGlowAlphaFinish = Math.max(
+ mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
+ }
+
+
+ /**
+ * Draw into the provided canvas. Assumes that the canvas has been rotated
+ * accordingly and the size has been set. The effect will be drawn the full
+ * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
+ * 1.f of height.
+ *
+ * @param canvas Canvas to draw into
+ * @return true if drawing should continue beyond this frame to continue the
+ * animation
+ */
+ public boolean draw(GLCanvas canvas) {
+ update();
+
+ final int edgeHeight = mEdge.getIntrinsicHeight();
+ final int edgeWidth = mEdge.getIntrinsicWidth();
+ final int glowHeight = mGlow.getIntrinsicHeight();
+ final int glowWidth = mGlow.getIntrinsicWidth();
+
+ mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
+
+ int glowBottom = (int) Math.min(
+ glowHeight * mGlowScaleY * glowHeight/ glowWidth * 0.6f,
+ glowHeight * MAX_GLOW_HEIGHT);
+ if (mWidth < mMinWidth) {
+ // Center the glow and clip it.
+ int glowLeft = (mWidth - mMinWidth)/2;
+ mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom);
+ } else {
+ // Stretch the glow to fit.
+ mGlow.setBounds(0, 0, mWidth, glowBottom);
+ }
+
+ mGlow.draw(canvas);
+
+ mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));
+
+ int edgeBottom = (int) (edgeHeight * mEdgeScaleY);
+ if (mWidth < mMinWidth) {
+ // Center the edge and clip it.
+ int edgeLeft = (mWidth - mMinWidth)/2;
+ mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom);
+ } else {
+ // Stretch the edge to fit.
+ mEdge.setBounds(0, 0, mWidth, edgeBottom);
+ }
+ mEdge.draw(canvas);
+
+ return mState != STATE_IDLE;
+ }
+
+ private void update() {
+ final long time = AnimationTime.get();
+ final float t = Math.min((time - mStartTime) / mDuration, 1.f);
+
+ final float interp = mInterpolator.getInterpolation(t);
+
+ mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp;
+ mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp;
+ mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
+ mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
+
+ if (t >= 1.f - EPSILON) {
+ switch (mState) {
+ case STATE_ABSORB:
+ mState = STATE_RECEDE;
+ mStartTime = AnimationTime.get();
+ mDuration = RECEDE_TIME;
+
+ mEdgeAlphaStart = mEdgeAlpha;
+ mEdgeScaleYStart = mEdgeScaleY;
+ mGlowAlphaStart = mGlowAlpha;
+ mGlowScaleYStart = mGlowScaleY;
+
+ // After absorb, the glow and edge should fade to nothing.
+ mEdgeAlphaFinish = 0.f;
+ mEdgeScaleYFinish = 0.f;
+ mGlowAlphaFinish = 0.f;
+ mGlowScaleYFinish = 0.f;
+ break;
+ case STATE_PULL:
+ mState = STATE_PULL_DECAY;
+ mStartTime = AnimationTime.get();
+ mDuration = PULL_DECAY_TIME;
+
+ mEdgeAlphaStart = mEdgeAlpha;
+ mEdgeScaleYStart = mEdgeScaleY;
+ mGlowAlphaStart = mGlowAlpha;
+ mGlowScaleYStart = mGlowScaleY;
+
+ // After pull, the glow and edge should fade to nothing.
+ mEdgeAlphaFinish = 0.f;
+ mEdgeScaleYFinish = 0.f;
+ mGlowAlphaFinish = 0.f;
+ mGlowScaleYFinish = 0.f;
+ break;
+ case STATE_PULL_DECAY:
+ // When receding, we want edge to decrease more slowly
+ // than the glow.
+ float factor = mGlowScaleYFinish != 0 ? 1
+ / (mGlowScaleYFinish * mGlowScaleYFinish)
+ : Float.MAX_VALUE;
+ mEdgeScaleY = mEdgeScaleYStart +
+ (mEdgeScaleYFinish - mEdgeScaleYStart) *
+ interp * factor;
+ mState = STATE_RECEDE;
+ break;
+ case STATE_RECEDE:
+ mState = STATE_IDLE;
+ break;
+ }
+ }
+ }
+
+ private static class Drawable extends ResourceTexture {
+ private Rect mBounds = new Rect();
+ private int mAlpha = 255;
+
+ public Drawable(Context context, int resId) {
+ super(context, resId);
+ }
+
+ public int getIntrinsicWidth() {
+ return getWidth();
+ }
+
+ public int getIntrinsicHeight() {
+ return getHeight();
+ }
+
+ public void setBounds(int left, int top, int right, int bottom) {
+ mBounds.set(left, top, right, bottom);
+ }
+
+ public void setAlpha(int alpha) {
+ mAlpha = alpha;
+ }
+
+ public void draw(GLCanvas canvas) {
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.multiplyAlpha(mAlpha / 255.0f);
+ Rect b = mBounds;
+ draw(canvas, b.left, b.top, b.width(), b.height());
+ canvas.restore();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/EdgeView.java b/src/com/android/gallery3d/ui/EdgeView.java
new file mode 100644
index 000000000..051de18fa
--- /dev/null
+++ b/src/com/android/gallery3d/ui/EdgeView.java
@@ -0,0 +1,132 @@
+/*
+ * 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 android.content.Context;
+import android.opengl.Matrix;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+// EdgeView draws EdgeEffect (blue glow) at four sides of the view.
+public class EdgeView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "EdgeView";
+
+ public static final int INVALID_DIRECTION = -1;
+ public static final int TOP = 0;
+ public static final int LEFT = 1;
+ public static final int BOTTOM = 2;
+ public static final int RIGHT = 3;
+
+ // Each edge effect has a transform matrix, and each matrix has 16 elements.
+ // We put all the elements in one array. These constants specify the
+ // starting index of each matrix.
+ private static final int TOP_M = TOP * 16;
+ private static final int LEFT_M = LEFT * 16;
+ private static final int BOTTOM_M = BOTTOM * 16;
+ private static final int RIGHT_M = RIGHT * 16;
+
+ private EdgeEffect[] mEffect = new EdgeEffect[4];
+ private float[] mMatrix = new float[4 * 16];
+
+ public EdgeView(Context context) {
+ for (int i = 0; i < 4; i++) {
+ mEffect[i] = new EdgeEffect(context);
+ }
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ if (!changeSize) return;
+
+ int w = right - left;
+ int h = bottom - top;
+ for (int i = 0; i < 4; i++) {
+ if ((i & 1) == 0) { // top or bottom
+ mEffect[i].setSize(w, h);
+ } else { // left or right
+ mEffect[i].setSize(h, w);
+ }
+ }
+
+ // Set up transforms for the four edges. Without transforms an
+ // EdgeEffect draws the TOP edge from (0, 0) to (w, Y * h) where Y
+ // is some factor < 1. For other edges we need to move, rotate, and
+ // flip the effects into proper places.
+ Matrix.setIdentityM(mMatrix, TOP_M);
+ Matrix.setIdentityM(mMatrix, LEFT_M);
+ Matrix.setIdentityM(mMatrix, BOTTOM_M);
+ Matrix.setIdentityM(mMatrix, RIGHT_M);
+
+ Matrix.rotateM(mMatrix, LEFT_M, 90, 0, 0, 1);
+ Matrix.scaleM(mMatrix, LEFT_M, 1, -1, 1);
+
+ Matrix.translateM(mMatrix, BOTTOM_M, 0, h, 0);
+ Matrix.scaleM(mMatrix, BOTTOM_M, 1, -1, 1);
+
+ Matrix.translateM(mMatrix, RIGHT_M, w, 0, 0);
+ Matrix.rotateM(mMatrix, RIGHT_M, 90, 0, 0, 1);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ super.render(canvas);
+ boolean more = false;
+ for (int i = 0; i < 4; i++) {
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ canvas.multiplyMatrix(mMatrix, i * 16);
+ more |= mEffect[i].draw(canvas);
+ canvas.restore();
+ }
+ if (more) {
+ invalidate();
+ }
+ }
+
+ // Called when the content is pulled away from the edge.
+ // offset is in pixels. direction is one of {TOP, LEFT, BOTTOM, RIGHT}.
+ public void onPull(int offset, int direction) {
+ int fullLength = ((direction & 1) == 0) ? getWidth() : getHeight();
+ mEffect[direction].onPull((float)offset / fullLength);
+ if (!mEffect[direction].isFinished()) {
+ invalidate();
+ }
+ }
+
+ // Call when the object is released after being pulled.
+ public void onRelease() {
+ boolean more = false;
+ for (int i = 0; i < 4; i++) {
+ mEffect[i].onRelease();
+ more |= !mEffect[i].isFinished();
+ }
+ if (more) {
+ invalidate();
+ }
+ }
+
+ // Call when the effect absorbs an impact at the given velocity.
+ // Used when a fling reaches the scroll boundary. velocity is in pixels
+ // per second. direction is one of {TOP, LEFT, BOTTOM, RIGHT}.
+ public void onAbsorb(int velocity, int direction) {
+ mEffect[direction].onAbsorb(velocity);
+ if (!mEffect[direction].isFinished()) {
+ invalidate();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/FlingScroller.java b/src/com/android/gallery3d/ui/FlingScroller.java
new file mode 100644
index 000000000..6f98c64f9
--- /dev/null
+++ b/src/com/android/gallery3d/ui/FlingScroller.java
@@ -0,0 +1,141 @@
+/*
+ * 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;
+
+
+// This is a customized version of Scroller, with a interface similar to
+// android.widget.Scroller. It does fling only, not scroll.
+//
+// The differences between the this Scroller and the system one are:
+//
+// (1) The velocity does not change because of min/max limit.
+// (2) The duration is different.
+// (3) The deceleration curve is different.
+class FlingScroller {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FlingController";
+
+ // The fling duration (in milliseconds) when velocity is 1 pixel/second
+ private static final float FLING_DURATION_PARAM = 50f;
+ private static final int DECELERATED_FACTOR = 4;
+
+ private int mStartX, mStartY;
+ private int mMinX, mMinY, mMaxX, mMaxY;
+ private double mSinAngle;
+ private double mCosAngle;
+ private int mDuration;
+ private int mDistance;
+ private int mFinalX, mFinalY;
+
+ private int mCurrX, mCurrY;
+ private double mCurrV;
+
+ public int getFinalX() {
+ return mFinalX;
+ }
+
+ public int getFinalY() {
+ return mFinalY;
+ }
+
+ public int getDuration() {
+ return mDuration;
+ }
+
+ public int getCurrX() {
+ return mCurrX;
+
+ }
+
+ public int getCurrY() {
+ return mCurrY;
+ }
+
+ public int getCurrVelocityX() {
+ return (int)Math.round(mCurrV * mCosAngle);
+ }
+
+ public int getCurrVelocityY() {
+ return (int)Math.round(mCurrV * mSinAngle);
+ }
+
+ public void fling(int startX, int startY, int velocityX, int velocityY,
+ int minX, int maxX, int minY, int maxY) {
+ mStartX = startX;
+ mStartY = startY;
+ mMinX = minX;
+ mMinY = minY;
+ mMaxX = maxX;
+ mMaxY = maxY;
+
+ double velocity = Math.hypot(velocityX, velocityY);
+ mSinAngle = velocityY / velocity;
+ mCosAngle = velocityX / velocity;
+ //
+ // The position formula: x(t) = s + (e - s) * (1 - (1 - t / T) ^ d)
+ // velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T
+ // Thus,
+ // v0 = d * (e - s) / T => (e - s) = v0 * T / d
+ //
+
+ // Ta = T_ref * (Va / V_ref) ^ (1 / (d - 1)); V_ref = 1 pixel/second;
+ mDuration = (int)Math.round(FLING_DURATION_PARAM
+ * Math.pow(Math.abs(velocity), 1.0 / (DECELERATED_FACTOR - 1)));
+
+ // (e - s) = v0 * T / d
+ mDistance = (int)Math.round(
+ velocity * mDuration / DECELERATED_FACTOR / 1000);
+
+ mFinalX = getX(1.0f);
+ mFinalY = getY(1.0f);
+ }
+
+ public void computeScrollOffset(float progress) {
+ progress = Math.min(progress, 1);
+ float f = 1 - progress;
+ f = 1 - (float) Math.pow(f, DECELERATED_FACTOR);
+ mCurrX = getX(f);
+ mCurrY = getY(f);
+ mCurrV = getV(progress);
+ }
+
+ private int getX(float f) {
+ int r = (int) Math.round(mStartX + f * mDistance * mCosAngle);
+ if (mCosAngle > 0 && mStartX <= mMaxX) {
+ r = Math.min(r, mMaxX);
+ } else if (mCosAngle < 0 && mStartX >= mMinX) {
+ r = Math.max(r, mMinX);
+ }
+ return r;
+ }
+
+ private int getY(float f) {
+ int r = (int) Math.round(mStartY + f * mDistance * mSinAngle);
+ if (mSinAngle > 0 && mStartY <= mMaxY) {
+ r = Math.min(r, mMaxY);
+ } else if (mSinAngle < 0 && mStartY >= mMinY) {
+ r = Math.max(r, mMinY);
+ }
+ return r;
+ }
+
+ private double getV(float progress) {
+ // velocity formula: v(t) = d * (e - s) * (1 - t / T) ^ (d - 1) / T
+ return DECELERATED_FACTOR * mDistance * 1000 *
+ Math.pow(1 - progress, DECELERATED_FACTOR - 1) / mDuration;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GLRoot.java b/src/com/android/gallery3d/ui/GLRoot.java
new file mode 100644
index 000000000..33a82eaf7
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLRoot.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;
+
+import android.content.Context;
+import android.graphics.Matrix;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+public interface GLRoot {
+
+ // Listener will be called when GL is idle AND before each frame.
+ // Mainly used for uploading textures.
+ public static interface OnGLIdleListener {
+ public boolean onGLIdle(
+ GLCanvas canvas, boolean renderRequested);
+ }
+
+ public void addOnGLIdleListener(OnGLIdleListener listener);
+ public void registerLaunchedAnimation(CanvasAnimation animation);
+ public void requestRenderForced();
+ public void requestRender();
+ public void requestLayoutContentPane();
+
+ public void lockRenderThread();
+ public void unlockRenderThread();
+
+ public void setContentPane(GLView content);
+ public void setOrientationSource(OrientationSource source);
+ public int getDisplayRotation();
+ public int getCompensation();
+ public Matrix getCompensationMatrix();
+ public void freeze();
+ public void unfreeze();
+ public void setLightsOutMode(boolean enabled);
+
+ public Context getContext();
+}
diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java
new file mode 100644
index 000000000..dc898d83d
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLRootView.java
@@ -0,0 +1,630 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PixelFormat;
+import android.opengl.GLSurfaceView;
+import android.os.Build;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.View;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.GLES11Canvas;
+import com.android.gallery3d.glrenderer.GLES20Canvas;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.MotionEventHelper;
+import com.android.gallery3d.util.Profile;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.concurrent.locks.Condition;
+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 <code>GLView</code>s. 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
+// <code>GLRootView</code> 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 boolean DEBUG_PROFILE = false;
+ private static final boolean DEBUG_PROFILE_SLOW_ONLY = false;
+
+ private static final int FLAG_INITIALIZED = 1;
+ private static final int FLAG_NEED_LAYOUT = 2;
+
+ private GL11 mGL;
+ private GLCanvas mCanvas;
+ private GLView mContentView;
+
+ private OrientationSource mOrientationSource;
+ // mCompensation is the difference between the UI orientation on GLCanvas
+ // and the framework orientation. See OrientationManager for details.
+ private int mCompensation;
+ // mCompensationMatrix maps the coordinates of touch events. It is kept sync
+ // with mCompensation.
+ private Matrix mCompensationMatrix = new Matrix();
+ private int mDisplayRotation;
+
+ private int mFlags = FLAG_NEED_LAYOUT;
+ private volatile boolean mRenderRequested = false;
+
+ private final ArrayList<CanvasAnimation> mAnimations =
+ new ArrayList<CanvasAnimation>();
+
+ private final ArrayDeque<OnGLIdleListener> mIdleListeners =
+ new ArrayDeque<OnGLIdleListener>();
+
+ private final IdleRunner mIdleRunner = new IdleRunner();
+
+ private final ReentrantLock mRenderLock = new ReentrantLock();
+ private final Condition mFreezeCondition =
+ mRenderLock.newCondition();
+ private boolean mFreeze;
+
+ private long mLastDrawFinishTime;
+ private boolean mInDownState = false;
+ private boolean mFirstDraw = true;
+
+ public GLRootView(Context context) {
+ this(context, null);
+ }
+
+ public GLRootView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mFlags |= FLAG_INITIALIZED;
+ setBackgroundDrawable(null);
+ setEGLContextClientVersion(ApiHelper.HAS_GLES20_REQUIRED ? 2 : 1);
+ if (ApiHelper.USE_888_PIXEL_FORMAT) {
+ setEGLConfigChooser(8, 8, 8, 0, 0, 0);
+ } else {
+ setEGLConfigChooser(5, 6, 5, 0, 0, 0);
+ }
+ setRenderer(this);
+ if (ApiHelper.USE_888_PIXEL_FORMAT) {
+ getHolder().setFormat(PixelFormat.RGB_888);
+ } else {
+ getHolder().setFormat(PixelFormat.RGB_565);
+ }
+
+ // Uncomment this to enable gl error check.
+ // setDebugFlags(DEBUG_CHECK_GL_ERROR);
+ }
+
+ @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();
+ }
+ }
+
+ @Override
+ public void requestRenderForced() {
+ superRequestRender();
+ }
+
+ @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;
+ if (ApiHelper.HAS_POST_ON_ANIMATION) {
+ postOnAnimation(mRequestRenderOnAnimationFrame);
+ } else {
+ super.requestRender();
+ }
+ }
+
+ private Runnable mRequestRenderOnAnimationFrame = new Runnable() {
+ @Override
+ public void run() {
+ superRequestRender();
+ }
+ };
+
+ private void superRequestRender() {
+ 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 w = getWidth();
+ int h = getHeight();
+ int displayRotation = 0;
+ int compensation = 0;
+
+ // Get the new orientation values
+ if (mOrientationSource != null) {
+ displayRotation = mOrientationSource.getDisplayRotation();
+ compensation = mOrientationSource.getCompensation();
+ } else {
+ displayRotation = 0;
+ compensation = 0;
+ }
+
+ if (mCompensation != compensation) {
+ mCompensation = compensation;
+ if (mCompensation % 180 != 0) {
+ mCompensationMatrix.setRotate(mCompensation);
+ // move center to origin before rotation
+ mCompensationMatrix.preTranslate(-w / 2, -h / 2);
+ // align with the new origin after rotation
+ mCompensationMatrix.postTranslate(h / 2, w / 2);
+ } else {
+ mCompensationMatrix.setRotate(mCompensation, w / 2, h / 2);
+ }
+ }
+ mDisplayRotation = displayRotation;
+
+ // Do the actual layout.
+ if (mCompensation % 180 != 0) {
+ int tmp = w;
+ w = h;
+ h = tmp;
+ }
+ Log.i(TAG, "layout content pane " + w + "x" + h
+ + " (compensation " + mCompensation + ")");
+ if (mContentView != null && w != 0 && h != 0) {
+ mContentView.layout(0, 0, w, h);
+ }
+ // 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);
+ }
+ mRenderLock.lock();
+ try {
+ mGL = gl;
+ mCanvas = ApiHelper.HAS_GLES20_REQUIRED ? new GLES20Canvas() : new GLES11Canvas(gl);
+ BasicTexture.invalidateAllTextures();
+ } finally {
+ mRenderLock.unlock();
+ }
+
+ if (DEBUG_FPS || DEBUG_PROFILE) {
+ setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+ } else {
+ setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+ }
+ }
+
+ /**
+ * 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();
+ if (DEBUG_PROFILE) {
+ Log.d(TAG, "Start profiling");
+ Profile.enable(20); // take a sample every 20ms
+ }
+ GL11 gl = (GL11) gl1;
+ Utils.assertTrue(mGL == gl);
+
+ mCanvas.setSize(width, height);
+ }
+
+ 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) {
+ AnimationTime.update();
+ long t0;
+ if (DEBUG_PROFILE_SLOW_ONLY) {
+ Profile.hold();
+ t0 = System.nanoTime();
+ }
+ mRenderLock.lock();
+
+ while (mFreeze) {
+ mFreezeCondition.awaitUninterruptibly();
+ }
+
+ try {
+ onDrawFrameLocked(gl);
+ } finally {
+ mRenderLock.unlock();
+ }
+
+ // We put a black cover View in front of the SurfaceView and hide it
+ // after the first draw. This prevents the SurfaceView being transparent
+ // before the first draw.
+ if (mFirstDraw) {
+ mFirstDraw = false;
+ post(new Runnable() {
+ @Override
+ public void run() {
+ View root = getRootView();
+ View cover = root.findViewById(R.id.gl_root_cover);
+ cover.setVisibility(GONE);
+ }
+ });
+ }
+
+ if (DEBUG_PROFILE_SLOW_ONLY) {
+ long t = System.nanoTime();
+ long durationInMs = (t - mLastDrawFinishTime) / 1000000;
+ long durationDrawInMs = (t - t0) / 1000000;
+ mLastDrawFinishTime = t;
+
+ if (durationInMs > 34) { // 34ms -> we skipped at least 2 frames
+ Log.v(TAG, "----- SLOW (" + durationDrawInMs + "/" +
+ durationInMs + ") -----");
+ Profile.commit();
+ } else {
+ Profile.drop();
+ }
+ }
+ }
+
+ 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 ((mOrientationSource != null
+ && mDisplayRotation != mOrientationSource.getDisplayRotation())
+ || (mFlags & FLAG_NEED_LAYOUT) != 0) {
+ layoutContentPane();
+ }
+
+ mCanvas.save(GLCanvas.SAVE_FLAG_ALL);
+ rotateCanvas(-mCompensation);
+ if (mContentView != null) {
+ mContentView.render(mCanvas);
+ } else {
+ // Make sure we always draw something to prevent displaying garbage
+ mCanvas.clearBuffer();
+ }
+ mCanvas.restore();
+
+ if (!mAnimations.isEmpty()) {
+ long now = AnimationTime.get();
+ for (int i = 0, n = mAnimations.size(); i < n; i++) {
+ mAnimations.get(i).setStartTime(now);
+ }
+ mAnimations.clear();
+ }
+
+ if (UploadedTexture.uploadLimitReached()) {
+ requestRender();
+ }
+
+ synchronized (mIdleListeners) {
+ if (!mIdleListeners.isEmpty()) mIdleRunner.enable();
+ }
+
+ if (DEBUG_INVALIDATE) {
+ mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor);
+ mInvalidateColor = ~mInvalidateColor;
+ }
+
+ if (DEBUG_DRAWING_STAT) {
+ mCanvas.dumpStatisticsAndClear();
+ }
+ }
+
+ private void rotateCanvas(int degrees) {
+ if (degrees == 0) return;
+ int w = getWidth();
+ int h = getHeight();
+ int cx = w / 2;
+ int cy = h / 2;
+ mCanvas.translate(cx, cy);
+ mCanvas.rotate(degrees, 0, 0, 1);
+ if (degrees % 180 != 0) {
+ mCanvas.translate(-cy, -cx);
+ } else {
+ mCanvas.translate(-cx, -cy);
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ if (!isEnabled()) return false;
+
+ int action = event.getAction();
+ if (action == MotionEvent.ACTION_CANCEL
+ || action == MotionEvent.ACTION_UP) {
+ mInDownState = false;
+ } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+
+ if (mCompensation != 0) {
+ event = MotionEventHelper.transformEvent(event, mCompensationMatrix);
+ }
+
+ 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();
+ }
+ }
+
+ 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 (mIdleListeners.isEmpty()) return;
+ listener = mIdleListeners.removeFirst();
+ }
+ mRenderLock.lock();
+ boolean keepInQueue;
+ try {
+ keepInQueue = listener.onGLIdle(mCanvas, mRenderRequested);
+ } finally {
+ mRenderLock.unlock();
+ }
+ synchronized (mIdleListeners) {
+ if (keepInQueue) mIdleListeners.addLast(listener);
+ if (!mRenderRequested && !mIdleListeners.isEmpty()) 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();
+ }
+
+ @Override
+ public void onPause() {
+ unfreeze();
+ super.onPause();
+ if (DEBUG_PROFILE) {
+ Log.d(TAG, "Stop profiling");
+ Profile.disableAll();
+ Profile.dumpToFile("/sdcard/gallery.prof");
+ Profile.reset();
+ }
+ }
+
+ @Override
+ public void setOrientationSource(OrientationSource source) {
+ mOrientationSource = source;
+ }
+
+ @Override
+ public int getDisplayRotation() {
+ return mDisplayRotation;
+ }
+
+ @Override
+ public int getCompensation() {
+ return mCompensation;
+ }
+
+ @Override
+ public Matrix getCompensationMatrix() {
+ return mCompensationMatrix;
+ }
+
+ @Override
+ public void freeze() {
+ mRenderLock.lock();
+ mFreeze = true;
+ mRenderLock.unlock();
+ }
+
+ @Override
+ public void unfreeze() {
+ mRenderLock.lock();
+ mFreeze = false;
+ mFreezeCondition.signalAll();
+ mRenderLock.unlock();
+ }
+
+ @Override
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ public void setLightsOutMode(boolean enabled) {
+ if (!ApiHelper.HAS_SET_SYSTEM_UI_VISIBILITY) return;
+
+ int flags = 0;
+ if (enabled) {
+ flags = STATUS_BAR_HIDDEN;
+ if (ApiHelper.HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE) {
+ flags |= (SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+ }
+ setSystemUiVisibility(flags);
+ }
+
+ // We need to unfreeze in the following methods and in onPause().
+ // These methods will wait on GLThread. If we have freezed the GLRootView,
+ // the GLThread will wait on main thread to call unfreeze and cause dead
+ // lock.
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+ unfreeze();
+ super.surfaceChanged(holder, format, w, h);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ unfreeze();
+ super.surfaceCreated(holder);
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ unfreeze();
+ super.surfaceDestroyed(holder);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ unfreeze();
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ unfreeze();
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/GLView.java b/src/com/android/gallery3d/ui/GLView.java
new file mode 100644
index 000000000..83de19fe4
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GLView.java
@@ -0,0 +1,465 @@
+/*
+ * 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.os.SystemClock;
+import android.view.MotionEvent;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.anim.StateTransitionAnimation;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+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;
+
+ public interface OnClickListener {
+ void onClick(GLView v);
+ }
+
+ protected final Rect mBounds = new Rect();
+ protected final Rect mPaddings = new Rect();
+
+ private GLRoot mRoot;
+ protected GLView mParent;
+ private ArrayList<GLView> 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;
+
+ private float [] mBackgroundColor;
+ private StateTransitionAnimation mTransition;
+
+ public void startAnimation(CanvasAnimation animation) {
+ GLRoot root = getGLRoot();
+ if (root == null) throw new IllegalStateException();
+ mAnimation = animation;
+ if (mAnimation != null) {
+ 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<GLView>();
+ }
+ 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) {
+ boolean transitionActive = false;
+ if (mTransition != null && mTransition.calculate(AnimationTime.get())) {
+ invalidate();
+ transitionActive = mTransition.isActive();
+ }
+ renderBackground(canvas);
+ canvas.save();
+ if (transitionActive) {
+ mTransition.applyContentTransform(this, canvas);
+ }
+ for (int i = 0, n = getComponentCount(); i < n; ++i) {
+ renderChild(canvas, getComponent(i));
+ }
+ canvas.restore();
+ if (transitionActive) {
+ mTransition.applyOverlay(this, canvas);
+ }
+ }
+
+ public void setIntroAnimation(StateTransitionAnimation intro) {
+ mTransition = intro;
+ if (mTransition != null) mTransition.start();
+ }
+
+ public float [] getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ public void setBackgroundColor(float [] color) {
+ mBackgroundColor = color;
+ }
+
+ protected void renderBackground(GLCanvas view) {
+ if (mBackgroundColor != null) {
+ view.clearBuffer(mBackgroundColor);
+ }
+ if (mTransition != null && mTransition.isActive()) {
+ mTransition.applyBackground(this, view);
+ return;
+ }
+ }
+
+ 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);
+
+ CanvasAnimation anim = component.mAnimation;
+ if (anim != null) {
+ canvas.save(anim.getCanvasSaveFlags());
+ if (anim.calculate(AnimationTime.get())) {
+ invalidate();
+ } else {
+ component.mAnimation = null;
+ }
+ anim.apply(canvas);
+ }
+ component.render(canvas);
+ if (anim != null) canvas.restore();
+ canvas.translate(-xoffset, -yoffset);
+ }
+
+ 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 layout(int left, int top, int right, int bottom) {
+ boolean sizeChanged = setBounds(left, top, right, bottom);
+ mViewFlags &= ~FLAG_LAYOUT_REQUESTED;
+ // We call onLayout no matter sizeChanged is true or not because the
+ // orientation may change without changing the size of the View (for
+ // example, rotate the device by 180 degrees), and we want to handle
+ // orientation change in onLayout.
+ onLayout(sizeChanged, 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/GestureRecognizer.java b/src/com/android/gallery3d/ui/GestureRecognizer.java
new file mode 100644
index 000000000..1e5250b9b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/GestureRecognizer.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2012 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.os.SystemClock;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+
+// This class aggregates three gesture detectors: GestureDetector,
+// ScaleGestureDetector, and DownUpDetector.
+public class GestureRecognizer {
+ @SuppressWarnings("unused")
+ private static final String TAG = "GestureRecognizer";
+
+ public interface Listener {
+ boolean onSingleTapUp(float x, float y);
+ boolean onDoubleTap(float x, float y);
+ boolean onScroll(float dx, float dy, float totalX, float totalY);
+ boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
+ boolean onScaleBegin(float focusX, float focusY);
+ boolean onScale(float focusX, float focusY, float scale);
+ void onScaleEnd();
+ void onDown(float x, float y);
+ void onUp();
+ }
+
+ private final GestureDetector mGestureDetector;
+ private final ScaleGestureDetector mScaleDetector;
+ private final DownUpDetector mDownUpDetector;
+ private final Listener mListener;
+
+ public GestureRecognizer(Context context, Listener listener) {
+ mListener = listener;
+ mGestureDetector = new GestureDetector(context, new MyGestureListener(),
+ null, true /* ignoreMultitouch */);
+ mScaleDetector = new ScaleGestureDetector(
+ context, new MyScaleListener());
+ mDownUpDetector = new DownUpDetector(new MyDownUpListener());
+ }
+
+ public void onTouchEvent(MotionEvent event) {
+ mGestureDetector.onTouchEvent(event);
+ mScaleDetector.onTouchEvent(event);
+ mDownUpDetector.onTouchEvent(event);
+ }
+
+ public boolean isDown() {
+ return mDownUpDetector.isDown();
+ }
+
+ public void cancelScale() {
+ long now = SystemClock.uptimeMillis();
+ MotionEvent cancelEvent = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+ mScaleDetector.onTouchEvent(cancelEvent);
+ cancelEvent.recycle();
+ }
+
+ private class MyGestureListener
+ extends GestureDetector.SimpleOnGestureListener {
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return mListener.onSingleTapUp(e.getX(), e.getY());
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ return mListener.onDoubleTap(e.getX(), e.getY());
+ }
+
+ @Override
+ public boolean onScroll(
+ MotionEvent e1, MotionEvent e2, float dx, float dy) {
+ return mListener.onScroll(
+ dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY());
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ return mListener.onFling(e1, e2, velocityX, velocityY);
+ }
+ }
+
+ private class MyScaleListener
+ extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ return mListener.onScaleBegin(
+ detector.getFocusX(), detector.getFocusY());
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ return mListener.onScale(detector.getFocusX(),
+ detector.getFocusY(), detector.getScaleFactor());
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ mListener.onScaleEnd();
+ }
+ }
+
+ private class MyDownUpListener implements DownUpDetector.DownUpListener {
+ @Override
+ public void onDown(MotionEvent e) {
+ mListener.onDown(e.getX(), e.getY());
+ }
+
+ @Override
+ public void onUp(MotionEvent e) {
+ mListener.onUp();
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/Log.java b/src/com/android/gallery3d/ui/Log.java
new file mode 100644
index 000000000..5570763bb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Log.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;
+
+// TODO: Delete this
+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..d210bd1f1
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ManageCacheDrawer.java
@@ -0,0 +1,116 @@
+/*
+ * 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 com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.data.DataSourceType;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.StringTexture;
+import com.android.gallery3d.ui.AlbumSetSlidingWindow.AlbumSetEntry;
+
+public class ManageCacheDrawer extends AlbumSetSlotRenderer {
+ private final ResourceTexture mCheckedItem;
+ private final ResourceTexture mUnCheckedItem;
+ private final SelectionManager mSelectionManager;
+
+ private final ResourceTexture mLocalAlbumIcon;
+ private final StringTexture mCachingText;
+
+ private final int mCachePinSize;
+ private final int mCachePinMargin;
+
+ public ManageCacheDrawer(AbstractGalleryActivity activity, SelectionManager selectionManager,
+ SlotView slotView, LabelSpec labelSpec, int cachePinSize, int cachePinMargin) {
+ super(activity, selectionManager, slotView, labelSpec,
+ activity.getResources().getColor(R.color.cache_placeholder));
+ Context context = activity;
+ 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);
+ mCachingText = StringTexture.newInstance(cachingLabel, 12, 0xffffffff);
+ mSelectionManager = selectionManager;
+ mCachePinSize = cachePinSize;
+ mCachePinMargin = cachePinMargin;
+ }
+
+ private static boolean isLocal(int dataSourceType) {
+ return dataSourceType != DataSourceType.TYPE_PICASA;
+ }
+
+ @Override
+ public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height) {
+ AlbumSetEntry entry = mDataWindow.get(index);
+
+ boolean wantCache = entry.cacheFlag == MediaSet.CACHE_FLAG_FULL;
+ boolean isCaching = wantCache && (
+ entry.cacheStatus != MediaSet.CACHE_STATUS_CACHED_FULL);
+ boolean selected = mSelectionManager.isItemSelected(entry.setPath);
+ boolean chooseToCache = wantCache ^ selected;
+ boolean available = isLocal(entry.sourceType) || chooseToCache;
+
+ int renderRequestFlags = 0;
+
+ if (!available) {
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.multiplyAlpha(0.6f);
+ }
+ renderRequestFlags |= renderContent(canvas, entry, width, height);
+ if (!available) canvas.restore();
+
+ renderRequestFlags |= renderLabel(canvas, entry, width, height);
+
+ drawCachingPin(canvas, entry.setPath,
+ entry.sourceType, isCaching, chooseToCache, width, height);
+
+ renderRequestFlags |= renderOverlay(canvas, index, entry, width, height);
+ return renderRequestFlags;
+ }
+
+ private void drawCachingPin(GLCanvas canvas, Path path, int dataSourceType,
+ boolean isCaching, boolean chooseToCache, int width, int height) {
+ ResourceTexture icon;
+ if (isLocal(dataSourceType)) {
+ icon = mLocalAlbumIcon;
+ } else if (chooseToCache) {
+ icon = mCheckedItem;
+ } else {
+ icon = mUnCheckedItem;
+ }
+
+ // show the icon in right bottom
+ int s = mCachePinSize;
+ int m = mCachePinMargin;
+ icon.draw(canvas, width - m - s, height - s, s, s);
+
+ if (isCaching) {
+ int w = mCachingText.getWidth();
+ int h = mCachingText.getHeight();
+ // Show the caching text in bottom center
+ mCachingText.draw(canvas, (width - w) / 2, height - h);
+ }
+ }
+}
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..29def0527
--- /dev/null
+++ b/src/com/android/gallery3d/ui/MenuExecutor.java
@@ -0,0 +1,448 @@
+/*
+ * 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.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Message;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+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.filtershow.crop.CropActivity;
+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 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_TASK_START = 3;
+ private static final int MSG_DO_SHARE = 4;
+
+ 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;
+ // wait the operation to finish when we want to stop it.
+ private boolean mWaitOnStop;
+ private boolean mPaused;
+
+ private final AbstractGalleryActivity mActivity;
+ private final SelectionManager mSelectionManager;
+ private final Handler mHandler;
+
+ private static ProgressDialog createProgressDialog(
+ 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);
+ }
+ return dialog;
+ }
+
+ public interface ProgressListener {
+ public void onConfirmDialogShown();
+ public void onConfirmDialogDismissed(boolean confirmed);
+ public void onProgressStart();
+ public void onProgressUpdate(int index);
+ public void onProgressComplete(int result);
+ }
+
+ public MenuExecutor(
+ AbstractGalleryActivity 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_START: {
+ if (message.obj != null) {
+ ProgressListener listener = (ProgressListener) message.obj;
+ listener.onProgressStart();
+ }
+ break;
+ }
+ case MSG_TASK_COMPLETE: {
+ stopTaskAndDismissDialog();
+ if (message.obj != null) {
+ ProgressListener listener = (ProgressListener) message.obj;
+ listener.onProgressComplete(message.arg1);
+ }
+ mSelectionManager.leaveSelectionMode();
+ break;
+ }
+ case MSG_TASK_UPDATE: {
+ if (mDialog != null && !mPaused) 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;
+ }
+ }
+ }
+ };
+ }
+
+ private void stopTaskAndDismissDialog() {
+ if (mTask != null) {
+ if (!mWaitOnStop) mTask.cancel();
+ if (mDialog != null && mDialog.isShowing()) mDialog.dismiss();
+ mDialog = null;
+ mTask = null;
+ }
+ }
+
+ public void resume() {
+ mPaused = false;
+ if (mDialog != null) mDialog.show();
+ }
+
+ public void pause() {
+ mPaused = true;
+ if (mDialog != null && mDialog.isShowing()) mDialog.hide();
+ }
+
+ public void destroy() {
+ stopTaskAndDismissDialog();
+ }
+
+ private void onProgressUpdate(int index, ProgressListener listener) {
+ mHandler.sendMessage(
+ mHandler.obtainMessage(MSG_TASK_UPDATE, index, 0, listener));
+ }
+
+ private void onProgressStart(ProgressListener listener) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_START, listener));
+ }
+
+ private void onProgressComplete(int result, ProgressListener listener) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_TASK_COMPLETE, result, 0, listener));
+ }
+
+ 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 supportTrim = (supported & MediaObject.SUPPORT_TRIM) != 0;
+ boolean supportMute = (supported & MediaObject.SUPPORT_MUTE) != 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;
+
+ setMenuItemVisible(menu, R.id.action_delete, supportDelete);
+ setMenuItemVisible(menu, R.id.action_rotate_ccw, supportRotate);
+ setMenuItemVisible(menu, R.id.action_rotate_cw, supportRotate);
+ setMenuItemVisible(menu, R.id.action_crop, supportCrop);
+ setMenuItemVisible(menu, R.id.action_trim, supportTrim);
+ setMenuItemVisible(menu, R.id.action_mute, supportMute);
+ // Hide panorama until call to updateMenuForPanorama corrects it
+ setMenuItemVisible(menu, R.id.action_share_panorama, false);
+ setMenuItemVisible(menu, R.id.action_share, supportShare);
+ setMenuItemVisible(menu, R.id.action_setas, supportSetAs);
+ setMenuItemVisible(menu, R.id.action_show_on_map, supportShowOnMap);
+ setMenuItemVisible(menu, R.id.action_edit, supportEdit);
+ setMenuItemVisible(menu, R.id.action_simple_edit, supportEdit);
+ setMenuItemVisible(menu, R.id.action_details, supportInfo);
+ }
+
+ public static void updateMenuForPanorama(Menu menu, boolean shareAsPanorama360,
+ boolean disablePanorama360Options) {
+ setMenuItemVisible(menu, R.id.action_share_panorama, shareAsPanorama360);
+ if (disablePanorama360Options) {
+ setMenuItemVisible(menu, R.id.action_rotate_ccw, false);
+ setMenuItemVisible(menu, R.id.action_rotate_cw, false);
+ }
+ }
+
+ private static void setMenuItemVisible(Menu menu, int itemId, boolean visible) {
+ MenuItem item = menu.findItem(itemId);
+ if (item != null) item.setVisible(visible);
+ }
+
+ private Path getSingleSelectedPath() {
+ ArrayList<Path> ids = mSelectionManager.getSelected(true);
+ Utils.assertTrue(ids.size() == 1);
+ return ids.get(0);
+ }
+
+ private Intent getIntentBySingleSelectedPath(String action) {
+ DataManager manager = mActivity.getDataManager();
+ Path path = getSingleSelectedPath();
+ String mimeType = getMimeType(manager.getMediaType(path));
+ return new Intent(action).setDataAndType(manager.getContentUri(path), mimeType);
+ }
+
+ private void onMenuClicked(int action, ProgressListener listener) {
+ onMenuClicked(action, listener, false, true);
+ }
+
+ public void onMenuClicked(int action, ProgressListener listener,
+ boolean waitOnStop, boolean showDialog) {
+ int title;
+ switch (action) {
+ case R.id.action_select_all:
+ if (mSelectionManager.inSelectAllMode()) {
+ mSelectionManager.deSelectAll();
+ } else {
+ mSelectionManager.selectAll();
+ }
+ return;
+ case R.id.action_crop: {
+ Intent intent = getIntentBySingleSelectedPath(CropActivity.CROP_ACTION);
+ ((Activity) mActivity).startActivity(intent);
+ return;
+ }
+ case R.id.action_edit: {
+ Intent intent = getIntentBySingleSelectedPath(Intent.ACTION_EDIT)
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ ((Activity) mActivity).startActivity(Intent.createChooser(intent, null));
+ return;
+ }
+ case R.id.action_setas: {
+ Intent intent = getIntentBySingleSelectedPath(Intent.ACTION_ATTACH_DATA)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.putExtra("mimeType", intent.getType());
+ Activity activity = mActivity;
+ activity.startActivity(Intent.createChooser(
+ intent, activity.getString(R.string.set_as)));
+ return;
+ }
+ case R.id.action_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;
+ default:
+ return;
+ }
+ startAction(action, title, listener, waitOnStop, showDialog);
+ }
+
+ private class ConfirmDialogListener implements OnClickListener, OnCancelListener {
+ private final int mActionId;
+ private final ProgressListener mListener;
+
+ public ConfirmDialogListener(int actionId, ProgressListener listener) {
+ mActionId = actionId;
+ mListener = listener;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ if (mListener != null) {
+ mListener.onConfirmDialogDismissed(true);
+ }
+ onMenuClicked(mActionId, mListener);
+ } else {
+ if (mListener != null) {
+ mListener.onConfirmDialogDismissed(false);
+ }
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (mListener != null) {
+ mListener.onConfirmDialogDismissed(false);
+ }
+ }
+ }
+
+ public void onMenuClicked(MenuItem menuItem, String confirmMsg,
+ final ProgressListener listener) {
+ final int action = menuItem.getItemId();
+
+ if (confirmMsg != null) {
+ if (listener != null) listener.onConfirmDialogShown();
+ ConfirmDialogListener cdl = new ConfirmDialogListener(action, listener);
+ new AlertDialog.Builder(mActivity.getAndroidContext())
+ .setMessage(confirmMsg)
+ .setOnCancelListener(cdl)
+ .setPositiveButton(R.string.ok, cdl)
+ .setNegativeButton(R.string.cancel, cdl)
+ .create().show();
+ } else {
+ onMenuClicked(action, listener);
+ }
+ }
+
+ public void startAction(int action, int title, ProgressListener listener) {
+ startAction(action, title, listener, false, true);
+ }
+
+ public void startAction(int action, int title, ProgressListener listener,
+ boolean waitOnStop, boolean showDialog) {
+ ArrayList<Path> ids = mSelectionManager.getSelected(false);
+ stopTaskAndDismissDialog();
+
+ Activity activity = mActivity;
+ if (showDialog) {
+ mDialog = createProgressDialog(activity, title, ids.size());
+ mDialog.show();
+ } else {
+ mDialog = null;
+ }
+ MediaOperation operation = new MediaOperation(action, ids, listener);
+ mTask = mActivity.getBatchServiceThreadPoolIfAvailable().submit(operation, null);
+ mWaitOnStop = waitOnStop;
+ }
+
+ public void startSingleItemAction(int action, Path targetPath) {
+ ArrayList<Path> ids = new ArrayList<Path>(1);
+ ids.add(targetPath);
+ mDialog = null;
+ MediaOperation operation = new MediaOperation(action, ids, null);
+ mTask = mActivity.getBatchServiceThreadPoolIfAvailable().submit(operation, null);
+ mWaitOnStop = false;
+ }
+
+ public static String getMimeType(int type) {
+ switch (type) {
+ case MediaObject.MEDIA_TYPE_IMAGE :
+ return GalleryUtils.MIME_TYPE_IMAGE;
+ case MediaObject.MEDIA_TYPE_VIDEO :
+ return GalleryUtils.MIME_TYPE_VIDEO;
+ default: return GalleryUtils.MIME_TYPE_ALL;
+ }
+ }
+
+ private boolean execute(
+ DataManager manager, JobContext jc, int cmd, Path path) {
+ boolean result = true;
+ Log.v(TAG, "Execute cmd: " + cmd + " for " + path);
+ long startTime = System.currentTimeMillis();
+
+ switch (cmd) {
+ case R.id.action_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(mActivity, latlng[0], latlng[1]);
+ }
+ break;
+ }
+ default:
+ throw new AssertionError();
+ }
+ Log.v(TAG, "It takes " + (System.currentTimeMillis() - startTime) +
+ " ms to execute cmd for " + path);
+ return result;
+ }
+
+ private class MediaOperation implements Job<Void> {
+ private final ArrayList<Path> mItems;
+ private final int mOperation;
+ private final ProgressListener mListener;
+
+ public MediaOperation(int operation, ArrayList<Path> items,
+ ProgressListener listener) {
+ mOperation = operation;
+ mItems = items;
+ mListener = listener;
+ }
+
+ @Override
+ public Void run(JobContext jc) {
+ int index = 0;
+ DataManager manager = mActivity.getDataManager();
+ int result = EXECUTION_RESULT_SUCCESS;
+ try {
+ onProgressStart(mListener);
+ for (Path id : mItems) {
+ if (jc.isCancelled()) {
+ result = EXECUTION_RESULT_CANCEL;
+ break;
+ }
+ if (!execute(manager, jc, mOperation, id)) {
+ result = EXECUTION_RESULT_FAIL;
+ }
+ onProgressUpdate(index++, mListener);
+ }
+ } catch (Throwable th) {
+ Log.e(TAG, "failed to execute operation " + mOperation
+ + " : " + th);
+ } finally {
+ onProgressComplete(result, mListener);
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/OrientationSource.java b/src/com/android/gallery3d/ui/OrientationSource.java
new file mode 100644
index 000000000..e13ce1cec
--- /dev/null
+++ b/src/com/android/gallery3d/ui/OrientationSource.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2012 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 OrientationSource {
+ public int getDisplayRotation();
+ public int getCompensation();
+}
diff --git a/src/com/android/gallery3d/ui/Paper.java b/src/com/android/gallery3d/ui/Paper.java
new file mode 100644
index 000000000..b36f5c3a2
--- /dev/null
+++ b/src/com/android/gallery3d/ui/Paper.java
@@ -0,0 +1,183 @@
+/*
+ * 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.opengl.Matrix;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.common.Utils;
+
+// This class does the overscroll effect.
+class Paper {
+ @SuppressWarnings("unused")
+ private static final String TAG = "Paper";
+ private static final int ROTATE_FACTOR = 4;
+ private EdgeAnimation mAnimationLeft = new EdgeAnimation();
+ private EdgeAnimation mAnimationRight = new EdgeAnimation();
+ private int mWidth;
+ private float[] mMatrix = new float[16];
+
+ public void overScroll(float distance) {
+ distance /= mWidth; // make it relative to width
+ if (distance < 0) {
+ mAnimationLeft.onPull(-distance);
+ } else {
+ mAnimationRight.onPull(distance);
+ }
+ }
+
+ public void edgeReached(float velocity) {
+ velocity /= mWidth; // make it relative to width
+ if (velocity < 0) {
+ mAnimationRight.onAbsorb(-velocity);
+ } else {
+ mAnimationLeft.onAbsorb(velocity);
+ }
+ }
+
+ public void onRelease() {
+ mAnimationLeft.onRelease();
+ mAnimationRight.onRelease();
+ }
+
+ public boolean advanceAnimation() {
+ // Note that we use "|" because we want both animations get updated.
+ return mAnimationLeft.update() | mAnimationRight.update();
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ }
+
+ public float[] getTransform(Rect rect, float scrollX) {
+ float left = mAnimationLeft.getValue();
+ float right = mAnimationRight.getValue();
+ float screenX = rect.centerX() - scrollX;
+ // We linearly interpolate the value [left, right] for the screenX
+ // range int [-1/4, 5/4]*mWidth. So if part of the thumbnail is outside
+ // the screen, we still get some transform.
+ float x = screenX + mWidth / 4;
+ int range = 3 * mWidth / 2;
+ float t = ((range - x) * left - x * right) / range;
+ // 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, rect.centerX(), rect.centerY(), 0);
+ Matrix.rotateM(mMatrix, 0, degrees, 0, 1, 0);
+ Matrix.translateM(mMatrix, 0, mMatrix, 0, -rect.width() / 2, -rect.height() / 2, 0);
+ return mMatrix;
+ }
+}
+
+// This class follows the structure of frameworks's EdgeEffect class.
+class EdgeAnimation {
+ @SuppressWarnings("unused")
+ private static final String TAG = "EdgeAnimation";
+
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_PULL = 1;
+ private static final int STATE_ABSORB = 2;
+ private static final int STATE_RELEASE = 3;
+
+ // Time it will take the effect to fully done in ms
+ private static final int ABSORB_TIME = 200;
+ private static final int RELEASE_TIME = 500;
+
+ private static final float VELOCITY_FACTOR = 0.1f;
+
+ private final Interpolator mInterpolator;
+
+ private int mState;
+ private float mValue;
+
+ private float mValueStart;
+ private float mValueFinish;
+ private long mStartTime;
+ private long mDuration;
+
+ public EdgeAnimation() {
+ mInterpolator = new DecelerateInterpolator();
+ mState = STATE_IDLE;
+ }
+
+ private void startAnimation(float start, float finish, long duration,
+ int newState) {
+ mValueStart = start;
+ mValueFinish = finish;
+ mDuration = duration;
+ mStartTime = now();
+ mState = newState;
+ }
+
+ // The deltaDistance's magnitude is in the range of -1 (no change) to 1.
+ // The value 1 is the full length of the view. Negative values means the
+ // movement is in the opposite direction.
+ public void onPull(float deltaDistance) {
+ if (mState == STATE_ABSORB) return;
+ mValue = Utils.clamp(mValue + deltaDistance, -1.0f, 1.0f);
+ mState = STATE_PULL;
+ }
+
+ public void onRelease() {
+ if (mState == STATE_IDLE || mState == STATE_ABSORB) return;
+ startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE);
+ }
+
+ public void onAbsorb(float velocity) {
+ float finish = Utils.clamp(mValue + velocity * VELOCITY_FACTOR,
+ -1.0f, 1.0f);
+ startAnimation(mValue, finish, ABSORB_TIME, STATE_ABSORB);
+ }
+
+ public boolean update() {
+ if (mState == STATE_IDLE) return false;
+ if (mState == STATE_PULL) return true;
+
+ float t = Utils.clamp((float)(now() - mStartTime) / mDuration, 0.0f, 1.0f);
+ /* Use linear interpolation for absorb, quadratic for others */
+ float interp = (mState == STATE_ABSORB)
+ ? t : mInterpolator.getInterpolation(t);
+
+ mValue = mValueStart + (mValueFinish - mValueStart) * interp;
+
+ if (t >= 1.0f) {
+ switch (mState) {
+ case STATE_ABSORB:
+ startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE);
+ break;
+ case STATE_RELEASE:
+ mState = STATE_IDLE;
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ public float getValue() {
+ return mValue;
+ }
+
+ private long now() {
+ return AnimationTime.get();
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PhotoFallbackEffect.java b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java
new file mode 100644
index 000000000..4603285a4
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PhotoFallbackEffect.java
@@ -0,0 +1,179 @@
+/*
+ * 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.graphics.RectF;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.AlbumSlotRenderer.SlotFilter;
+
+import java.util.ArrayList;
+
+public class PhotoFallbackEffect extends Animation implements SlotFilter {
+
+ private static final int ANIM_DURATION = 300;
+ private static final Interpolator ANIM_INTERPOLATE = new DecelerateInterpolator(1.5f);
+
+ public static class Entry {
+ public int index;
+ public Path path;
+ public Rect source;
+ public Rect dest;
+ public RawTexture texture;
+
+ public Entry(Path path, Rect source, RawTexture texture) {
+ this.path = path;
+ this.source = source;
+ this.texture = texture;
+ }
+ }
+
+ public interface PositionProvider {
+ public Rect getPosition(int index);
+ public int getItemIndex(Path path);
+ }
+
+ private RectF mSource = new RectF();
+ private RectF mTarget = new RectF();
+ private float mProgress;
+ private PositionProvider mPositionProvider;
+
+ private ArrayList<Entry> mList = new ArrayList<Entry>();
+
+ public PhotoFallbackEffect() {
+ setDuration(ANIM_DURATION);
+ setInterpolator(ANIM_INTERPOLATE);
+ }
+
+ public void addEntry(Path path, Rect rect, RawTexture texture) {
+ mList.add(new Entry(path, rect, texture));
+ }
+
+ public Entry getEntry(Path path) {
+ for (int i = 0, n = mList.size(); i < n; ++i) {
+ Entry entry = mList.get(i);
+ if (entry.path == path) return entry;
+ }
+ return null;
+ }
+
+ public boolean draw(GLCanvas canvas) {
+ boolean more = calculate(AnimationTime.get());
+ for (int i = 0, n = mList.size(); i < n; ++i) {
+ Entry entry = mList.get(i);
+ if (entry.index < 0) continue;
+ entry.dest = mPositionProvider.getPosition(entry.index);
+ drawEntry(canvas, entry);
+ }
+ return more;
+ }
+
+ private void drawEntry(GLCanvas canvas, Entry entry) {
+ if (!entry.texture.isLoaded()) return;
+
+ int w = entry.texture.getWidth();
+ int h = entry.texture.getHeight();
+
+ Rect s = entry.source;
+ Rect d = entry.dest;
+
+ // the following calculation is based on d.width() == d.height()
+
+ float p = mProgress;
+
+ float fullScale = (float) d.height() / Math.min(s.width(), s.height());
+ float scale = fullScale * p + 1 * (1 - p);
+
+ float cx = d.centerX() * p + s.centerX() * (1 - p);
+ float cy = d.centerY() * p + s.centerY() * (1 - p);
+
+ float ch = s.height() * scale;
+ float cw = s.width() * scale;
+
+ if (w > h) {
+ // draw the center part
+ mTarget.set(cx - ch / 2, cy - ch / 2, cx + ch / 2, cy + ch / 2);
+ mSource.set((w - h) / 2, 0, (w + h) / 2, h);
+ canvas.drawTexture(entry.texture, mSource, mTarget);
+
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.multiplyAlpha(1 - p);
+
+ // draw the left part
+ mTarget.set(cx - cw / 2, cy - ch / 2, cx - ch / 2, cy + ch / 2);
+ mSource.set(0, 0, (w - h) / 2, h);
+ canvas.drawTexture(entry.texture, mSource, mTarget);
+
+ // draw the right part
+ mTarget.set(cx + ch / 2, cy - ch / 2, cx + cw / 2, cy + ch / 2);
+ mSource.set((w + h) / 2, 0, w, h);
+ canvas.drawTexture(entry.texture, mSource, mTarget);
+
+ canvas.restore();
+ } else {
+ // draw the center part
+ mTarget.set(cx - cw / 2, cy - cw / 2, cx + cw / 2, cy + cw / 2);
+ mSource.set(0, (h - w) / 2, w, (h + w) / 2);
+ canvas.drawTexture(entry.texture, mSource, mTarget);
+
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.multiplyAlpha(1 - p);
+
+ // draw the upper part
+ mTarget.set(cx - cw / 2, cy - ch / 2, cx + cw / 2, cy - cw / 2);
+ mSource.set(0, 0, w, (h - w) / 2);
+ canvas.drawTexture(entry.texture, mSource, mTarget);
+
+ // draw the bottom part
+ mTarget.set(cx - cw / 2, cy + cw / 2, cx + cw / 2, cy + ch / 2);
+ mSource.set(0, (w + h) / 2, w, h);
+ canvas.drawTexture(entry.texture, mSource, mTarget);
+
+ canvas.restore();
+ }
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mProgress = progress;
+ }
+
+ public void setPositionProvider(PositionProvider provider) {
+ mPositionProvider = provider;
+ if (mPositionProvider != null) {
+ for (int i = 0, n = mList.size(); i < n; ++i) {
+ Entry entry = mList.get(i);
+ entry.index = mPositionProvider.getItemIndex(entry.path);
+ }
+ }
+ }
+
+ @Override
+ public boolean acceptSlot(int index) {
+ for (int i = 0, n = mList.size(); i < n; ++i) {
+ Entry entry = mList.get(i);
+ if (entry.index == index) return false;
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PhotoView.java b/src/com/android/gallery3d/ui/PhotoView.java
new file mode 100644
index 000000000..7afa20348
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PhotoView.java
@@ -0,0 +1,1858 @@
+/*
+ * 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.content.res.Configuration;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Message;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
+import android.view.animation.AccelerateInterpolator;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.StringTexture;
+import com.android.gallery3d.glrenderer.Texture;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.RangeArray;
+import com.android.gallery3d.util.UsageStatistics;
+
+public class PhotoView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "PhotoView";
+ private final int mPlaceholderColor;
+
+ public static final int INVALID_SIZE = -1;
+ public static final long INVALID_DATA_VERSION =
+ MediaObject.INVALID_DATA_VERSION;
+
+ public static class Size {
+ public int width;
+ public int height;
+ }
+
+ public interface Model extends TileImageView.TileSource {
+ public int getCurrentIndex();
+ public void moveTo(int index);
+
+ // Returns the size for the specified picture. If the size information is
+ // not avaiable, width = height = 0.
+ public void getImageSize(int offset, Size size);
+
+ // Returns the media item for the specified picture.
+ public MediaItem getMediaItem(int offset);
+
+ // Returns the rotation for the specified picture.
+ public int getImageRotation(int offset);
+
+ // This amends the getScreenNail() method of TileImageView.Model to get
+ // ScreenNail at previous (negative offset) or next (positive offset)
+ // positions. Returns null if the specified ScreenNail is unavailable.
+ public ScreenNail getScreenNail(int offset);
+
+ // Set this to true if we need the model to provide full images.
+ public void setNeedFullImage(boolean enabled);
+
+ // Returns true if the item is the Camera preview.
+ public boolean isCamera(int offset);
+
+ // Returns true if the item is the Panorama.
+ public boolean isPanorama(int offset);
+
+ // Returns true if the item is a static image that represents camera
+ // preview.
+ public boolean isStaticCamera(int offset);
+
+ // Returns true if the item is a Video.
+ public boolean isVideo(int offset);
+
+ // Returns true if the item can be deleted.
+ public boolean isDeletable(int offset);
+
+ public static final int LOADING_INIT = 0;
+ public static final int LOADING_COMPLETE = 1;
+ public static final int LOADING_FAIL = 2;
+
+ public int getLoadingState(int offset);
+
+ // When data change happens, we need to decide which MediaItem to focus
+ // on.
+ //
+ // 1. If focus hint path != null, we try to focus on it if we can find
+ // it. This is used for undo a deletion, so we can focus on the
+ // undeleted item.
+ //
+ // 2. Otherwise try to focus on the MediaItem that is currently focused,
+ // if we can find it.
+ //
+ // 3. Otherwise try to focus on the previous MediaItem or the next
+ // MediaItem, depending on the value of focus hint direction.
+ public static final int FOCUS_HINT_NEXT = 0;
+ public static final int FOCUS_HINT_PREVIOUS = 1;
+ public void setFocusHintDirection(int direction);
+ public void setFocusHintPath(Path path);
+ }
+
+ public interface Listener {
+ public void onSingleTapUp(int x, int y);
+ public void onFullScreenChanged(boolean full);
+ public void onActionBarAllowed(boolean allowed);
+ public void onActionBarWanted();
+ public void onCurrentImageUpdated();
+ public void onDeleteImage(Path path, int offset);
+ public void onUndoDeleteImage();
+ public void onCommitDeleteImage();
+ public void onFilmModeChanged(boolean enabled);
+ public void onPictureCenter(boolean isCamera);
+ public void onUndoBarVisibilityChanged(boolean visible);
+ }
+
+ // The rules about orientation locking:
+ //
+ // (1) We need to lock the orientation if we are in page mode camera
+ // preview, so there is no (unwanted) rotation animation when the user
+ // rotates the device.
+ //
+ // (2) We need to unlock the orientation if we want to show the action bar
+ // because the action bar follows the system orientation.
+ //
+ // The rules about action bar:
+ //
+ // (1) If we are in film mode, we don't show action bar.
+ //
+ // (2) If we go from camera to gallery with capture animation, we show
+ // action bar.
+ private static final int MSG_CANCEL_EXTRA_SCALING = 2;
+ private static final int MSG_SWITCH_FOCUS = 3;
+ private static final int MSG_CAPTURE_ANIMATION_DONE = 4;
+ private static final int MSG_DELETE_ANIMATION_DONE = 5;
+ private static final int MSG_DELETE_DONE = 6;
+ private static final int MSG_UNDO_BAR_TIMEOUT = 7;
+ private static final int MSG_UNDO_BAR_FULL_CAMERA = 8;
+
+ private static final float SWIPE_THRESHOLD = 300f;
+
+ private static final float DEFAULT_TEXT_SIZE = 20;
+ private static float TRANSITION_SCALE_FACTOR = 0.74f;
+ private static final int ICON_RATIO = 6;
+
+ // whether we want to apply card deck effect in page mode.
+ private static final boolean CARD_EFFECT = true;
+
+ // whether we want to apply offset effect in film mode.
+ private static final boolean OFFSET_EFFECT = true;
+
+ // Used to calculate the scaling factor for the card deck effect.
+ private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f);
+
+ // Used to calculate the alpha factor for the fading animation.
+ private AccelerateInterpolator mAlphaInterpolator =
+ new AccelerateInterpolator(0.9f);
+
+ // We keep this many previous ScreenNails. (also this many next ScreenNails)
+ public static final int SCREEN_NAIL_MAX = 3;
+
+ // These are constants for the delete gesture.
+ private static final int SWIPE_ESCAPE_VELOCITY = 500; // dp/sec
+ private static final int MAX_DISMISS_VELOCITY = 2500; // dp/sec
+ private static final int SWIPE_ESCAPE_DISTANCE = 150; // dp
+
+ // The picture entries, the valid index is from -SCREEN_NAIL_MAX to
+ // SCREEN_NAIL_MAX.
+ private final RangeArray<Picture> mPictures =
+ new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX);
+ private Size[] mSizes = new Size[2 * SCREEN_NAIL_MAX + 1];
+
+ private final MyGestureListener mGestureListener;
+ private final GestureRecognizer mGestureRecognizer;
+ private final PositionController mPositionController;
+
+ private Listener mListener;
+ private Model mModel;
+ private StringTexture mNoThumbnailText;
+ private TileImageView mTileView;
+ private EdgeView mEdgeView;
+ private UndoBarView mUndoBar;
+ private Texture mVideoPlayIcon;
+
+ private SynchronizedHandler mHandler;
+
+ private boolean mCancelExtraScalingPending;
+ private boolean mFilmMode = false;
+ private boolean mWantPictureCenterCallbacks = false;
+ private int mDisplayRotation = 0;
+ private int mCompensation = 0;
+ private boolean mFullScreenCamera;
+ private Rect mCameraRelativeFrame = new Rect();
+ private Rect mCameraRect = new Rect();
+ private boolean mFirst = true;
+
+ // [mPrevBound, mNextBound] is the range of index for all pictures in the
+ // model, if we assume the index of current focused picture is 0. So if
+ // there are some previous pictures, mPrevBound < 0, and if there are some
+ // next pictures, mNextBound > 0.
+ private int mPrevBound;
+ private int mNextBound;
+
+ // This variable prevents us doing snapback until its values goes to 0. This
+ // happens if the user gesture is still in progress or we are in a capture
+ // animation.
+ private int mHolding;
+ private static final int HOLD_TOUCH_DOWN = 1;
+ private static final int HOLD_CAPTURE_ANIMATION = 2;
+ private static final int HOLD_DELETE = 4;
+
+ // mTouchBoxIndex is the index of the box that is touched by the down
+ // gesture in film mode. The value Integer.MAX_VALUE means no box was
+ // touched.
+ private int mTouchBoxIndex = Integer.MAX_VALUE;
+ // Whether the box indicated by mTouchBoxIndex is deletable. Only meaningful
+ // if mTouchBoxIndex is not Integer.MAX_VALUE.
+ private boolean mTouchBoxDeletable;
+ // This is the index of the last deleted item. This is only used as a hint
+ // to hide the undo button when we are too far away from the deleted
+ // item. The value Integer.MAX_VALUE means there is no such hint.
+ private int mUndoIndexHint = Integer.MAX_VALUE;
+
+ private Context mContext;
+
+ public PhotoView(AbstractGalleryActivity activity) {
+ mTileView = new TileImageView(activity);
+ addComponent(mTileView);
+ mContext = activity.getAndroidContext();
+ mPlaceholderColor = mContext.getResources().getColor(
+ R.color.photo_placeholder);
+ mEdgeView = new EdgeView(mContext);
+ addComponent(mEdgeView);
+ mUndoBar = new UndoBarView(mContext);
+ addComponent(mUndoBar);
+ mUndoBar.setVisibility(GLView.INVISIBLE);
+ mUndoBar.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(GLView v) {
+ mListener.onUndoDeleteImage();
+ hideUndoBar();
+ }
+ });
+ mNoThumbnailText = StringTexture.newInstance(
+ mContext.getString(R.string.no_thumbnail),
+ DEFAULT_TEXT_SIZE, Color.WHITE);
+
+ mHandler = new MyHandler(activity.getGLRoot());
+
+ mGestureListener = new MyGestureListener();
+ mGestureRecognizer = new GestureRecognizer(mContext, mGestureListener);
+
+ mPositionController = new PositionController(mContext,
+ new PositionController.Listener() {
+
+ @Override
+ public void invalidate() {
+ PhotoView.this.invalidate();
+ }
+
+ @Override
+ public boolean isHoldingDown() {
+ return (mHolding & HOLD_TOUCH_DOWN) != 0;
+ }
+
+ @Override
+ public boolean isHoldingDelete() {
+ return (mHolding & HOLD_DELETE) != 0;
+ }
+
+ @Override
+ public void onPull(int offset, int direction) {
+ mEdgeView.onPull(offset, direction);
+ }
+
+ @Override
+ public void onRelease() {
+ mEdgeView.onRelease();
+ }
+
+ @Override
+ public void onAbsorb(int velocity, int direction) {
+ mEdgeView.onAbsorb(velocity, direction);
+ }
+ });
+ mVideoPlayIcon = new ResourceTexture(mContext, R.drawable.ic_control_play);
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ if (i == 0) {
+ mPictures.put(i, new FullPicture());
+ } else {
+ mPictures.put(i, new ScreenNailPicture(i));
+ }
+ }
+ }
+
+ public void stopScrolling() {
+ mPositionController.stopScrolling();
+ }
+
+ public void setModel(Model model) {
+ mModel = model;
+ mTileView.setModel(mModel);
+ }
+
+ class MyHandler extends SynchronizedHandler {
+ public MyHandler(GLRoot root) {
+ super(root);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_CANCEL_EXTRA_SCALING: {
+ mGestureRecognizer.cancelScale();
+ mPositionController.setExtraScalingRange(false);
+ mCancelExtraScalingPending = false;
+ break;
+ }
+ case MSG_SWITCH_FOCUS: {
+ switchFocus();
+ break;
+ }
+ case MSG_CAPTURE_ANIMATION_DONE: {
+ // message.arg1 is the offset parameter passed to
+ // switchWithCaptureAnimation().
+ captureAnimationDone(message.arg1);
+ break;
+ }
+ case MSG_DELETE_ANIMATION_DONE: {
+ // message.obj is the Path of the MediaItem which should be
+ // deleted. message.arg1 is the offset of the image.
+ mListener.onDeleteImage((Path) message.obj, message.arg1);
+ // Normally a box which finishes delete animation will hold
+ // position until the underlying MediaItem is actually
+ // deleted, and HOLD_DELETE will be cancelled that time. In
+ // case the MediaItem didn't actually get deleted in 2
+ // seconds, we will cancel HOLD_DELETE and make it bounce
+ // back.
+
+ // We make sure there is at most one MSG_DELETE_DONE
+ // in the handler.
+ mHandler.removeMessages(MSG_DELETE_DONE);
+ Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
+ mHandler.sendMessageDelayed(m, 2000);
+
+ int numberOfPictures = mNextBound - mPrevBound + 1;
+ if (numberOfPictures == 2) {
+ if (mModel.isCamera(mNextBound)
+ || mModel.isCamera(mPrevBound)) {
+ numberOfPictures--;
+ }
+ }
+ showUndoBar(numberOfPictures <= 1);
+ break;
+ }
+ case MSG_DELETE_DONE: {
+ if (!mHandler.hasMessages(MSG_DELETE_ANIMATION_DONE)) {
+ mHolding &= ~HOLD_DELETE;
+ snapback();
+ }
+ break;
+ }
+ case MSG_UNDO_BAR_TIMEOUT: {
+ checkHideUndoBar(UNDO_BAR_TIMEOUT);
+ break;
+ }
+ case MSG_UNDO_BAR_FULL_CAMERA: {
+ checkHideUndoBar(UNDO_BAR_FULL_CAMERA);
+ break;
+ }
+ default: throw new AssertionError(message.what);
+ }
+ }
+ }
+
+ public void setWantPictureCenterCallbacks(boolean wanted) {
+ mWantPictureCenterCallbacks = wanted;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Data/Image change notifications
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) {
+ mPrevBound = prevBound;
+ mNextBound = nextBound;
+
+ // Update mTouchBoxIndex
+ if (mTouchBoxIndex != Integer.MAX_VALUE) {
+ int k = mTouchBoxIndex;
+ mTouchBoxIndex = Integer.MAX_VALUE;
+ for (int i = 0; i < 2 * SCREEN_NAIL_MAX + 1; i++) {
+ if (fromIndex[i] == k) {
+ mTouchBoxIndex = i - SCREEN_NAIL_MAX;
+ break;
+ }
+ }
+ }
+
+ // Hide undo button if we are too far away
+ if (mUndoIndexHint != Integer.MAX_VALUE) {
+ if (Math.abs(mUndoIndexHint - mModel.getCurrentIndex()) >= 3) {
+ hideUndoBar();
+ }
+ }
+
+ // Update the ScreenNails.
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ Picture p = mPictures.get(i);
+ p.reload();
+ mSizes[i + SCREEN_NAIL_MAX] = p.getSize();
+ }
+
+ boolean wasDeleting = mPositionController.hasDeletingBox();
+
+ // Move the boxes
+ mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0,
+ mModel.isCamera(0), mSizes);
+
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ setPictureSize(i);
+ }
+
+ boolean isDeleting = mPositionController.hasDeletingBox();
+
+ // If the deletion is done, make HOLD_DELETE persist for only the time
+ // needed for a snapback animation.
+ if (wasDeleting && !isDeleting) {
+ mHandler.removeMessages(MSG_DELETE_DONE);
+ Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
+ mHandler.sendMessageDelayed(
+ m, PositionController.SNAPBACK_ANIMATION_TIME);
+ }
+
+ invalidate();
+ }
+
+ public boolean isDeleting() {
+ return (mHolding & HOLD_DELETE) != 0
+ && mPositionController.hasDeletingBox();
+ }
+
+ public void notifyImageChange(int index) {
+ if (index == 0) {
+ mListener.onCurrentImageUpdated();
+ }
+ mPictures.get(index).reload();
+ setPictureSize(index);
+ invalidate();
+ }
+
+ private void setPictureSize(int index) {
+ Picture p = mPictures.get(index);
+ mPositionController.setImageSize(index, p.getSize(),
+ index == 0 && p.isCamera() ? mCameraRect : null);
+ }
+
+ @Override
+ protected void onLayout(
+ boolean changeSize, int left, int top, int right, int bottom) {
+ int w = right - left;
+ int h = bottom - top;
+ mTileView.layout(0, 0, w, h);
+ mEdgeView.layout(0, 0, w, h);
+ mUndoBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ mUndoBar.layout(0, h - mUndoBar.getMeasuredHeight(), w, h);
+
+ GLRoot root = getGLRoot();
+ int displayRotation = root.getDisplayRotation();
+ int compensation = root.getCompensation();
+ if (mDisplayRotation != displayRotation
+ || mCompensation != compensation) {
+ mDisplayRotation = displayRotation;
+ mCompensation = compensation;
+
+ // We need to change the size and rotation of the Camera ScreenNail,
+ // but we don't want it to animate because the size doen't actually
+ // change in the eye of the user.
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ Picture p = mPictures.get(i);
+ if (p.isCamera()) {
+ p.forceSize();
+ }
+ }
+ }
+
+ updateCameraRect();
+ mPositionController.setConstrainedFrame(mCameraRect);
+ if (changeSize) {
+ mPositionController.setViewSize(getWidth(), getHeight());
+ }
+ }
+
+ // Update the camera rectangle due to layout change or camera relative frame
+ // change.
+ private void updateCameraRect() {
+ // Get the width and height in framework orientation because the given
+ // mCameraRelativeFrame is in that coordinates.
+ int w = getWidth();
+ int h = getHeight();
+ if (mCompensation % 180 != 0) {
+ int tmp = w;
+ w = h;
+ h = tmp;
+ }
+ int l = mCameraRelativeFrame.left;
+ int t = mCameraRelativeFrame.top;
+ int r = mCameraRelativeFrame.right;
+ int b = mCameraRelativeFrame.bottom;
+
+ // Now convert it to the coordinates we are using.
+ switch (mCompensation) {
+ case 0: mCameraRect.set(l, t, r, b); break;
+ case 90: mCameraRect.set(h - b, l, h - t, r); break;
+ case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break;
+ case 270: mCameraRect.set(t, w - r, b, w - l); break;
+ }
+
+ Log.d(TAG, "compensation = " + mCompensation
+ + ", CameraRelativeFrame = " + mCameraRelativeFrame
+ + ", mCameraRect = " + mCameraRect);
+ }
+
+ public void setCameraRelativeFrame(Rect frame) {
+ mCameraRelativeFrame.set(frame);
+ updateCameraRect();
+ // Originally we do
+ // mPositionController.setConstrainedFrame(mCameraRect);
+ // here, but it is moved to a parameter of the setImageSize() call, so
+ // it can be updated atomically with the CameraScreenNail's size change.
+ }
+
+ // Returns the rotation we need to do to the camera texture before drawing
+ // it to the canvas, assuming the camera texture is correct when the device
+ // is in its natural orientation.
+ private int getCameraRotation() {
+ return (mCompensation - mDisplayRotation + 360) % 360;
+ }
+
+ private int getPanoramaRotation() {
+ // This function is magic
+ // The issue here is that Pano makes bad assumptions about rotation and
+ // orientation. The first is it assumes only two rotations are possible,
+ // 0 and 90. Thus, if display rotation is >= 180, we invert the output.
+ // The second is that it assumes landscape is a 90 rotation from portrait,
+ // however on landscape devices this is not true. Thus, if we are in portrait
+ // on a landscape device, we need to invert the output
+ int orientation = mContext.getResources().getConfiguration().orientation;
+ boolean invertPortrait = (orientation == Configuration.ORIENTATION_PORTRAIT
+ && (mDisplayRotation == 90 || mDisplayRotation == 270));
+ boolean invert = (mDisplayRotation >= 180);
+ if (invert != invertPortrait) {
+ return (mCompensation + 180) % 360;
+ }
+ return mCompensation;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Pictures
+ ////////////////////////////////////////////////////////////////////////////
+
+ private interface Picture {
+ void reload();
+ void draw(GLCanvas canvas, Rect r);
+ void setScreenNail(ScreenNail s);
+ boolean isCamera(); // whether the picture is a camera preview
+ boolean isDeletable(); // whether the picture can be deleted
+ void forceSize(); // called when mCompensation changes
+ Size getSize();
+ }
+
+ class FullPicture implements Picture {
+ private int mRotation;
+ private boolean mIsCamera;
+ private boolean mIsPanorama;
+ private boolean mIsStaticCamera;
+ private boolean mIsVideo;
+ private boolean mIsDeletable;
+ private int mLoadingState = Model.LOADING_INIT;
+ private Size mSize = new Size();
+
+ @Override
+ public void reload() {
+ // mImageWidth and mImageHeight will get updated
+ mTileView.notifyModelInvalidated();
+
+ mIsCamera = mModel.isCamera(0);
+ mIsPanorama = mModel.isPanorama(0);
+ mIsStaticCamera = mModel.isStaticCamera(0);
+ mIsVideo = mModel.isVideo(0);
+ mIsDeletable = mModel.isDeletable(0);
+ mLoadingState = mModel.getLoadingState(0);
+ setScreenNail(mModel.getScreenNail(0));
+ updateSize();
+ }
+
+ @Override
+ public Size getSize() {
+ return mSize;
+ }
+
+ @Override
+ public void forceSize() {
+ updateSize();
+ mPositionController.forceImageSize(0, mSize);
+ }
+
+ private void updateSize() {
+ if (mIsPanorama) {
+ mRotation = getPanoramaRotation();
+ } else if (mIsCamera && !mIsStaticCamera) {
+ mRotation = getCameraRotation();
+ } else {
+ mRotation = mModel.getImageRotation(0);
+ }
+
+ int w = mTileView.mImageWidth;
+ int h = mTileView.mImageHeight;
+ mSize.width = getRotated(mRotation, w, h);
+ mSize.height = getRotated(mRotation, h, w);
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, Rect r) {
+ drawTileView(canvas, r);
+
+ // We want to have the following transitions:
+ // (1) Move camera preview out of its place: switch to film mode
+ // (2) Move camera preview into its place: switch to page mode
+ // The extra mWasCenter check makes sure (1) does not apply if in
+ // page mode, we move _to_ the camera preview from another picture.
+
+ // Holdings except touch-down prevent the transitions.
+ if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return;
+
+ if (mWantPictureCenterCallbacks && mPositionController.isCenter()) {
+ mListener.onPictureCenter(mIsCamera);
+ }
+ }
+
+ @Override
+ public void setScreenNail(ScreenNail s) {
+ mTileView.setScreenNail(s);
+ }
+
+ @Override
+ public boolean isCamera() {
+ return mIsCamera;
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return mIsDeletable;
+ }
+
+ private void drawTileView(GLCanvas canvas, Rect r) {
+ float imageScale = mPositionController.getImageScale();
+ int viewW = getWidth();
+ int viewH = getHeight();
+ float cx = r.exactCenterX();
+ float cy = r.exactCenterY();
+ float scale = 1f; // the scaling factor due to card effect
+
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
+ float filmRatio = mPositionController.getFilmRatio();
+ boolean wantsCardEffect = CARD_EFFECT && !mIsCamera
+ && filmRatio != 1f && !mPictures.get(-1).isCamera()
+ && !mPositionController.inOpeningAnimation();
+ boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
+ && filmRatio == 1f && r.centerY() != viewH / 2;
+ if (wantsCardEffect) {
+ // Calculate the move-out progress value.
+ int left = r.left;
+ int right = r.right;
+ float progress = calculateMoveOutProgress(left, right, viewW);
+ progress = Utils.clamp(progress, -1f, 1f);
+
+ // We only want to apply the fading animation if the scrolling
+ // movement is to the right.
+ if (progress < 0) {
+ scale = getScrollScale(progress);
+ float alpha = getScrollAlpha(progress);
+ scale = interpolate(filmRatio, scale, 1f);
+ alpha = interpolate(filmRatio, alpha, 1f);
+
+ imageScale *= scale;
+ canvas.multiplyAlpha(alpha);
+
+ float cxPage; // the cx value in page mode
+ if (right - left <= viewW) {
+ // If the picture is narrower than the view, keep it at
+ // the center of the view.
+ cxPage = viewW / 2f;
+ } else {
+ // If the picture is wider than the view (it's
+ // zoomed-in), keep the left edge of the object align
+ // the the left edge of the view.
+ cxPage = (right - left) * scale / 2f;
+ }
+ cx = interpolate(filmRatio, cxPage, cx);
+ }
+ } else if (wantsOffsetEffect) {
+ float offset = (float) (r.centerY() - viewH / 2) / viewH;
+ float alpha = getOffsetAlpha(offset);
+ canvas.multiplyAlpha(alpha);
+ }
+
+ // Draw the tile view.
+ setTileViewPosition(cx, cy, viewW, viewH, imageScale);
+ renderChild(canvas, mTileView);
+
+ // Draw the play video icon and the message.
+ canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f));
+ int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f);
+ if (mIsVideo) drawVideoPlayIcon(canvas, s);
+ if (mLoadingState == Model.LOADING_FAIL) {
+ drawLoadingFailMessage(canvas);
+ }
+
+ // Draw a debug indicator showing which picture has focus (index ==
+ // 0).
+ //canvas.fillRect(-10, -10, 20, 20, 0x80FF00FF);
+
+ canvas.restore();
+ }
+
+ // Set the position of the tile view
+ private void setTileViewPosition(float cx, float cy,
+ int viewW, int viewH, float scale) {
+ // Find out the bitmap coordinates of the center of the view
+ int imageW = mPositionController.getImageWidth();
+ int imageH = mPositionController.getImageHeight();
+ int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f);
+ int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f);
+
+ int inverseX = imageW - centerX;
+ int inverseY = imageH - centerY;
+ int x, y;
+ switch (mRotation) {
+ case 0: x = centerX; y = centerY; break;
+ case 90: x = centerY; y = inverseX; break;
+ case 180: x = inverseX; y = inverseY; break;
+ case 270: x = inverseY; y = centerX; break;
+ default:
+ throw new RuntimeException(String.valueOf(mRotation));
+ }
+ mTileView.setPosition(x, y, scale, mRotation);
+ }
+ }
+
+ private class ScreenNailPicture implements Picture {
+ private int mIndex;
+ private int mRotation;
+ private ScreenNail mScreenNail;
+ private boolean mIsCamera;
+ private boolean mIsPanorama;
+ private boolean mIsStaticCamera;
+ private boolean mIsVideo;
+ private boolean mIsDeletable;
+ private int mLoadingState = Model.LOADING_INIT;
+ private Size mSize = new Size();
+
+ public ScreenNailPicture(int index) {
+ mIndex = index;
+ }
+
+ @Override
+ public void reload() {
+ mIsCamera = mModel.isCamera(mIndex);
+ mIsPanorama = mModel.isPanorama(mIndex);
+ mIsStaticCamera = mModel.isStaticCamera(mIndex);
+ mIsVideo = mModel.isVideo(mIndex);
+ mIsDeletable = mModel.isDeletable(mIndex);
+ mLoadingState = mModel.getLoadingState(mIndex);
+ setScreenNail(mModel.getScreenNail(mIndex));
+ updateSize();
+ }
+
+ @Override
+ public Size getSize() {
+ return mSize;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, Rect r) {
+ if (mScreenNail == null) {
+ // Draw a placeholder rectange if there should be a picture in
+ // this position (but somehow there isn't).
+ if (mIndex >= mPrevBound && mIndex <= mNextBound) {
+ drawPlaceHolder(canvas, r);
+ }
+ return;
+ }
+ int w = getWidth();
+ int h = getHeight();
+ if (r.left >= w || r.right <= 0 || r.top >= h || r.bottom <= 0) {
+ mScreenNail.noDraw();
+ return;
+ }
+
+ float filmRatio = mPositionController.getFilmRatio();
+ boolean wantsCardEffect = CARD_EFFECT && mIndex > 0
+ && filmRatio != 1f && !mPictures.get(0).isCamera();
+ boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
+ && filmRatio == 1f && r.centerY() != h / 2;
+ int cx = wantsCardEffect
+ ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f)
+ : r.centerX();
+ int cy = r.centerY();
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.translate(cx, cy);
+ if (wantsCardEffect) {
+ float progress = (float) (w / 2 - r.centerX()) / w;
+ progress = Utils.clamp(progress, -1, 1);
+ float alpha = getScrollAlpha(progress);
+ float scale = getScrollScale(progress);
+ alpha = interpolate(filmRatio, alpha, 1f);
+ scale = interpolate(filmRatio, scale, 1f);
+ canvas.multiplyAlpha(alpha);
+ canvas.scale(scale, scale, 1);
+ } else if (wantsOffsetEffect) {
+ float offset = (float) (r.centerY() - h / 2) / h;
+ float alpha = getOffsetAlpha(offset);
+ canvas.multiplyAlpha(alpha);
+ }
+ if (mRotation != 0) {
+ canvas.rotate(mRotation, 0, 0, 1);
+ }
+ int drawW = getRotated(mRotation, r.width(), r.height());
+ int drawH = getRotated(mRotation, r.height(), r.width());
+ mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH);
+ if (isScreenNailAnimating()) {
+ invalidate();
+ }
+ int s = Math.min(drawW, drawH);
+ if (mIsVideo) drawVideoPlayIcon(canvas, s);
+ if (mLoadingState == Model.LOADING_FAIL) {
+ drawLoadingFailMessage(canvas);
+ }
+ canvas.restore();
+ }
+
+ private boolean isScreenNailAnimating() {
+ return (mScreenNail instanceof TiledScreenNail)
+ && ((TiledScreenNail) mScreenNail).isAnimating();
+ }
+
+ @Override
+ public void setScreenNail(ScreenNail s) {
+ mScreenNail = s;
+ }
+
+ @Override
+ public void forceSize() {
+ updateSize();
+ mPositionController.forceImageSize(mIndex, mSize);
+ }
+
+ private void updateSize() {
+ if (mIsPanorama) {
+ mRotation = getPanoramaRotation();
+ } else if (mIsCamera && !mIsStaticCamera) {
+ mRotation = getCameraRotation();
+ } else {
+ mRotation = mModel.getImageRotation(mIndex);
+ }
+
+ if (mScreenNail != null) {
+ mSize.width = mScreenNail.getWidth();
+ mSize.height = mScreenNail.getHeight();
+ } else {
+ // If we don't have ScreenNail available, we can still try to
+ // get the size information of it.
+ mModel.getImageSize(mIndex, mSize);
+ }
+
+ int w = mSize.width;
+ int h = mSize.height;
+ mSize.width = getRotated(mRotation, w, h);
+ mSize.height = getRotated(mRotation, h, w);
+ }
+
+ @Override
+ public boolean isCamera() {
+ return mIsCamera;
+ }
+
+ @Override
+ public boolean isDeletable() {
+ return mIsDeletable;
+ }
+ }
+
+ // Draw a gray placeholder in the specified rectangle.
+ private void drawPlaceHolder(GLCanvas canvas, Rect r) {
+ canvas.fillRect(r.left, r.top, r.width(), r.height(), mPlaceholderColor);
+ }
+
+ // Draw the video play icon (in the place where the spinner was)
+ private void drawVideoPlayIcon(GLCanvas canvas, int side) {
+ int s = side / ICON_RATIO;
+ // Draw the video play icon at the center
+ mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s);
+ }
+
+ // Draw the "no thumbnail" message
+ private void drawLoadingFailMessage(GLCanvas canvas) {
+ StringTexture m = mNoThumbnailText;
+ m.draw(canvas, -m.getWidth() / 2, -m.getHeight() / 2);
+ }
+
+ private static int getRotated(int degree, int original, int theother) {
+ return (degree % 180 == 0) ? original : theother;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Gestures Handling
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ mGestureRecognizer.onTouchEvent(event);
+ return true;
+ }
+
+ private class MyGestureListener implements GestureRecognizer.Listener {
+ private boolean mIgnoreUpEvent = false;
+ // If we can change mode for this scale gesture.
+ private boolean mCanChangeMode;
+ // If we have changed the film mode in this scaling gesture.
+ private boolean mModeChanged;
+ // If this scaling gesture should be ignored.
+ private boolean mIgnoreScalingGesture;
+ // whether the down action happened while the view is scrolling.
+ private boolean mDownInScrolling;
+ // If we should ignore all gestures other than onSingleTapUp.
+ private boolean mIgnoreSwipingGesture;
+ // If a scrolling has happened after a down gesture.
+ private boolean mScrolledAfterDown;
+ // If the first scrolling move is in X direction. In the film mode, X
+ // direction scrolling is normal scrolling. but Y direction scrolling is
+ // a delete gesture.
+ private boolean mFirstScrollX;
+ // The accumulated Y delta that has been sent to mPositionController.
+ private int mDeltaY;
+ // The accumulated scaling change from a scaling gesture.
+ private float mAccScale;
+ // If an onFling happened after the last onDown
+ private boolean mHadFling;
+
+ @Override
+ public boolean onSingleTapUp(float x, float y) {
+ // On crespo running Android 2.3.6 (gingerbread), a pinch out gesture results in the
+ // following call sequence: onDown(), onUp() and then onSingleTapUp(). The correct
+ // sequence for a single-tap-up gesture should be: onDown(), onSingleTapUp() and onUp().
+ // The call sequence for a pinch out gesture in JB is: onDown(), then onUp() and there's
+ // no onSingleTapUp(). Base on these observations, the following condition is added to
+ // filter out the false alarm where onSingleTapUp() is called within a pinch out
+ // gesture. The framework fix went into ICS. Refer to b/4588114.
+ if (Build.VERSION.SDK_INT < ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ if ((mHolding & HOLD_TOUCH_DOWN) == 0) {
+ return true;
+ }
+ }
+
+ // We do this in addition to onUp() because we want the snapback of
+ // setFilmMode to happen.
+ mHolding &= ~HOLD_TOUCH_DOWN;
+
+ if (mFilmMode && !mDownInScrolling) {
+ switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f));
+
+ // If this is a lock screen photo, let the listener handle the
+ // event. Tapping on lock screen photo should take the user
+ // directly to the lock screen.
+ MediaItem item = mModel.getMediaItem(0);
+ int supported = 0;
+ if (item != null) supported = item.getSupportedOperations();
+ if ((supported & MediaItem.SUPPORT_ACTION) == 0) {
+ setFilmMode(false);
+ mIgnoreUpEvent = true;
+ return true;
+ }
+ }
+
+ if (mListener != null) {
+ // Do the inverse transform of the touch coordinates.
+ Matrix m = getGLRoot().getCompensationMatrix();
+ Matrix inv = new Matrix();
+ m.invert(inv);
+ float[] pts = new float[] {x, y};
+ inv.mapPoints(pts);
+ mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f));
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(float x, float y) {
+ if (mIgnoreSwipingGesture) return true;
+ if (mPictures.get(0).isCamera()) return false;
+ PositionController controller = mPositionController;
+ float scale = controller.getImageScale();
+ // onDoubleTap happened on the second ACTION_DOWN.
+ // We need to ignore the next UP event.
+ mIgnoreUpEvent = true;
+ if (scale <= .75f || controller.isAtMinimalScale()) {
+ controller.zoomIn(x, y, Math.max(1.0f, scale * 1.5f));
+ } else {
+ controller.resetToFullView();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(float dx, float dy, float totalX, float totalY) {
+ if (mIgnoreSwipingGesture) return true;
+ if (!mScrolledAfterDown) {
+ mScrolledAfterDown = true;
+ mFirstScrollX = (Math.abs(dx) > Math.abs(dy));
+ }
+
+ int dxi = (int) (-dx + 0.5f);
+ int dyi = (int) (-dy + 0.5f);
+ if (mFilmMode) {
+ if (mFirstScrollX) {
+ mPositionController.scrollFilmX(dxi);
+ } else {
+ if (mTouchBoxIndex == Integer.MAX_VALUE) return true;
+ int newDeltaY = calculateDeltaY(totalY);
+ int d = newDeltaY - mDeltaY;
+ if (d != 0) {
+ mPositionController.scrollFilmY(mTouchBoxIndex, d);
+ mDeltaY = newDeltaY;
+ }
+ }
+ } else {
+ mPositionController.scrollPage(dxi, dyi);
+ }
+ return true;
+ }
+
+ private int calculateDeltaY(float delta) {
+ if (mTouchBoxDeletable) return (int) (delta + 0.5f);
+
+ // don't let items that can't be deleted be dragged more than
+ // maxScrollDistance, and make it harder and harder to drag.
+ int size = getHeight();
+ float maxScrollDistance = 0.15f * size;
+ if (Math.abs(delta) >= size) {
+ delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
+ } else {
+ delta = maxScrollDistance *
+ FloatMath.sin((delta / size) * (float) (Math.PI / 2));
+ }
+ return (int) (delta + 0.5f);
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ if (mIgnoreSwipingGesture) return true;
+ if (mModeChanged) return true;
+ if (swipeImages(velocityX, velocityY)) {
+ mIgnoreUpEvent = true;
+ } else {
+ flingImages(velocityX, velocityY, Math.abs(e2.getY() - e1.getY()));
+ }
+ mHadFling = true;
+ return true;
+ }
+
+ private boolean flingImages(float velocityX, float velocityY, float dY) {
+ int vx = (int) (velocityX + 0.5f);
+ int vy = (int) (velocityY + 0.5f);
+ if (!mFilmMode) {
+ return mPositionController.flingPage(vx, vy);
+ }
+ if (Math.abs(velocityX) > Math.abs(velocityY)) {
+ return mPositionController.flingFilmX(vx);
+ }
+ // If we scrolled in Y direction fast enough, treat it as a delete
+ // gesture.
+ if (!mFilmMode || mTouchBoxIndex == Integer.MAX_VALUE
+ || !mTouchBoxDeletable) {
+ return false;
+ }
+ int maxVelocity = GalleryUtils.dpToPixel(MAX_DISMISS_VELOCITY);
+ int escapeVelocity = GalleryUtils.dpToPixel(SWIPE_ESCAPE_VELOCITY);
+ int escapeDistance = GalleryUtils.dpToPixel(SWIPE_ESCAPE_DISTANCE);
+ int centerY = mPositionController.getPosition(mTouchBoxIndex)
+ .centerY();
+ boolean fastEnough = (Math.abs(vy) > escapeVelocity)
+ && (Math.abs(vy) > Math.abs(vx))
+ && ((vy > 0) == (centerY > getHeight() / 2))
+ && dY >= escapeDistance;
+ if (fastEnough) {
+ vy = Math.min(vy, maxVelocity);
+ int duration = mPositionController.flingFilmY(mTouchBoxIndex, vy);
+ if (duration >= 0) {
+ mPositionController.setPopFromTop(vy < 0);
+ deleteAfterAnimation(duration);
+ // We reset mTouchBoxIndex, so up() won't check if Y
+ // scrolled far enough to be a delete gesture.
+ mTouchBoxIndex = Integer.MAX_VALUE;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void deleteAfterAnimation(int duration) {
+ MediaItem item = mModel.getMediaItem(mTouchBoxIndex);
+ if (item == null) return;
+ mListener.onCommitDeleteImage();
+ mUndoIndexHint = mModel.getCurrentIndex() + mTouchBoxIndex;
+ mHolding |= HOLD_DELETE;
+ Message m = mHandler.obtainMessage(MSG_DELETE_ANIMATION_DONE);
+ m.obj = item.getPath();
+ m.arg1 = mTouchBoxIndex;
+ mHandler.sendMessageDelayed(m, duration);
+ }
+
+ @Override
+ public boolean onScaleBegin(float focusX, float focusY) {
+ if (mIgnoreSwipingGesture) return true;
+ // We ignore the scaling gesture if it is a camera preview.
+ mIgnoreScalingGesture = mPictures.get(0).isCamera();
+ if (mIgnoreScalingGesture) {
+ return true;
+ }
+ mPositionController.beginScale(focusX, focusY);
+ // We can change mode if we are in film mode, or we are in page
+ // mode and at minimal scale.
+ mCanChangeMode = mFilmMode
+ || mPositionController.isAtMinimalScale();
+ mAccScale = 1f;
+ return true;
+ }
+
+ @Override
+ public boolean onScale(float focusX, float focusY, float scale) {
+ if (mIgnoreSwipingGesture) return true;
+ if (mIgnoreScalingGesture) return true;
+ if (mModeChanged) return true;
+ if (Float.isNaN(scale) || Float.isInfinite(scale)) return false;
+
+ int outOfRange = mPositionController.scaleBy(scale, focusX, focusY);
+
+ // We wait for a large enough scale change before changing mode.
+ // Otherwise we may mistakenly treat a zoom-in gesture as zoom-out
+ // or vice versa.
+ mAccScale *= scale;
+ boolean largeEnough = (mAccScale < 0.97f || mAccScale > 1.03f);
+
+ // If mode changes, we treat this scaling gesture has ended.
+ if (mCanChangeMode && largeEnough) {
+ if ((outOfRange < 0 && !mFilmMode) ||
+ (outOfRange > 0 && mFilmMode)) {
+ stopExtraScalingIfNeeded();
+
+ // Removing the touch down flag allows snapback to happen
+ // for film mode change.
+ mHolding &= ~HOLD_TOUCH_DOWN;
+ if (mFilmMode) {
+ UsageStatistics.setPendingTransitionCause(
+ UsageStatistics.TRANSITION_PINCH_OUT);
+ } else {
+ UsageStatistics.setPendingTransitionCause(
+ UsageStatistics.TRANSITION_PINCH_IN);
+ }
+ setFilmMode(!mFilmMode);
+
+
+ // We need to call onScaleEnd() before setting mModeChanged
+ // to true.
+ onScaleEnd();
+ mModeChanged = true;
+ return true;
+ }
+ }
+
+ if (outOfRange != 0) {
+ startExtraScalingIfNeeded();
+ } else {
+ stopExtraScalingIfNeeded();
+ }
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd() {
+ if (mIgnoreSwipingGesture) return;
+ if (mIgnoreScalingGesture) return;
+ if (mModeChanged) return;
+ mPositionController.endScale();
+ }
+
+ private void startExtraScalingIfNeeded() {
+ if (!mCancelExtraScalingPending) {
+ mHandler.sendEmptyMessageDelayed(
+ MSG_CANCEL_EXTRA_SCALING, 700);
+ mPositionController.setExtraScalingRange(true);
+ mCancelExtraScalingPending = true;
+ }
+ }
+
+ private void stopExtraScalingIfNeeded() {
+ if (mCancelExtraScalingPending) {
+ mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING);
+ mPositionController.setExtraScalingRange(false);
+ mCancelExtraScalingPending = false;
+ }
+ }
+
+ @Override
+ public void onDown(float x, float y) {
+ checkHideUndoBar(UNDO_BAR_TOUCHED);
+
+ mDeltaY = 0;
+ mModeChanged = false;
+
+ if (mIgnoreSwipingGesture) return;
+
+ mHolding |= HOLD_TOUCH_DOWN;
+
+ if (mFilmMode && mPositionController.isScrolling()) {
+ mDownInScrolling = true;
+ mPositionController.stopScrolling();
+ } else {
+ mDownInScrolling = false;
+ }
+ mHadFling = false;
+ mScrolledAfterDown = false;
+ if (mFilmMode) {
+ int xi = (int) (x + 0.5f);
+ int yi = (int) (y + 0.5f);
+ // We only care about being within the x bounds, necessary for
+ // handling very wide images which are otherwise very hard to fling
+ mTouchBoxIndex = mPositionController.hitTest(xi, getHeight() / 2);
+
+ if (mTouchBoxIndex < mPrevBound || mTouchBoxIndex > mNextBound) {
+ mTouchBoxIndex = Integer.MAX_VALUE;
+ } else {
+ mTouchBoxDeletable =
+ mPictures.get(mTouchBoxIndex).isDeletable();
+ }
+ } else {
+ mTouchBoxIndex = Integer.MAX_VALUE;
+ }
+ }
+
+ @Override
+ public void onUp() {
+ if (mIgnoreSwipingGesture) return;
+
+ mHolding &= ~HOLD_TOUCH_DOWN;
+ mEdgeView.onRelease();
+
+ // If we scrolled in Y direction far enough, treat it as a delete
+ // gesture.
+ if (mFilmMode && mScrolledAfterDown && !mFirstScrollX
+ && mTouchBoxIndex != Integer.MAX_VALUE) {
+ Rect r = mPositionController.getPosition(mTouchBoxIndex);
+ int h = getHeight();
+ if (Math.abs(r.centerY() - h * 0.5f) > 0.4f * h) {
+ int duration = mPositionController
+ .flingFilmY(mTouchBoxIndex, 0);
+ if (duration >= 0) {
+ mPositionController.setPopFromTop(r.centerY() < h * 0.5f);
+ deleteAfterAnimation(duration);
+ }
+ }
+ }
+
+ if (mIgnoreUpEvent) {
+ mIgnoreUpEvent = false;
+ return;
+ }
+
+ if (!(mFilmMode && !mHadFling && mFirstScrollX
+ && snapToNeighborImage())) {
+ snapback();
+ }
+ }
+
+ public void setSwipingEnabled(boolean enabled) {
+ mIgnoreSwipingGesture = !enabled;
+ }
+ }
+
+ public void setSwipingEnabled(boolean enabled) {
+ mGestureListener.setSwipingEnabled(enabled);
+ }
+
+ private void updateActionBar() {
+ boolean isCamera = mPictures.get(0).isCamera();
+ if (isCamera && !mFilmMode) {
+ // Move into camera in page mode, lock
+ mListener.onActionBarAllowed(false);
+ } else {
+ mListener.onActionBarAllowed(true);
+ if (mFilmMode) mListener.onActionBarWanted();
+ }
+ }
+
+ public void setFilmMode(boolean enabled) {
+ if (mFilmMode == enabled) return;
+ mFilmMode = enabled;
+ mPositionController.setFilmMode(mFilmMode);
+ mModel.setNeedFullImage(!enabled);
+ mModel.setFocusHintDirection(
+ mFilmMode ? Model.FOCUS_HINT_PREVIOUS : Model.FOCUS_HINT_NEXT);
+ updateActionBar();
+ mListener.onFilmModeChanged(enabled);
+ }
+
+ public boolean getFilmMode() {
+ return mFilmMode;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Framework events
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void pause() {
+ mPositionController.skipAnimation();
+ mTileView.freeTextures();
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+ mPictures.get(i).setScreenNail(null);
+ }
+ hideUndoBar();
+ }
+
+ public void resume() {
+ mTileView.prepareTextures();
+ mPositionController.skipToFinalPosition();
+ }
+
+ // move to the camera preview and show controls after resume
+ public void resetToFirstPicture() {
+ mModel.moveTo(0);
+ setFilmMode(false);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Undo Bar
+ ////////////////////////////////////////////////////////////////////////////
+
+ private int mUndoBarState;
+ private static final int UNDO_BAR_SHOW = 1;
+ private static final int UNDO_BAR_TIMEOUT = 2;
+ private static final int UNDO_BAR_TOUCHED = 4;
+ private static final int UNDO_BAR_FULL_CAMERA = 8;
+ private static final int UNDO_BAR_DELETE_LAST = 16;
+
+ // "deleteLast" means if the deletion is on the last remaining picture in
+ // the album.
+ private void showUndoBar(boolean deleteLast) {
+ mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT);
+ mUndoBarState = UNDO_BAR_SHOW;
+ if(deleteLast) mUndoBarState |= UNDO_BAR_DELETE_LAST;
+ mUndoBar.animateVisibility(GLView.VISIBLE);
+ mHandler.sendEmptyMessageDelayed(MSG_UNDO_BAR_TIMEOUT, 3000);
+ if (mListener != null) mListener.onUndoBarVisibilityChanged(true);
+ }
+
+ private void hideUndoBar() {
+ mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT);
+ mListener.onCommitDeleteImage();
+ mUndoBar.animateVisibility(GLView.INVISIBLE);
+ mUndoBarState = 0;
+ mUndoIndexHint = Integer.MAX_VALUE;
+ mListener.onUndoBarVisibilityChanged(false);
+ }
+
+ // Check if the one of the conditions for hiding the undo bar has been
+ // met. The conditions are:
+ //
+ // 1. It has been three seconds since last showing, and (a) the user has
+ // touched, or (b) the deleted picture is the last remaining picture in the
+ // album.
+ //
+ // 2. The camera is shown in full screen.
+ private void checkHideUndoBar(int addition) {
+ mUndoBarState |= addition;
+ if ((mUndoBarState & UNDO_BAR_SHOW) == 0) return;
+ boolean timeout = (mUndoBarState & UNDO_BAR_TIMEOUT) != 0;
+ boolean touched = (mUndoBarState & UNDO_BAR_TOUCHED) != 0;
+ boolean fullCamera = (mUndoBarState & UNDO_BAR_FULL_CAMERA) != 0;
+ boolean deleteLast = (mUndoBarState & UNDO_BAR_DELETE_LAST) != 0;
+ if ((timeout && deleteLast) || fullCamera || touched) {
+ hideUndoBar();
+ }
+ }
+
+ public boolean canUndo() {
+ return (mUndoBarState & UNDO_BAR_SHOW) != 0;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Rendering
+ ////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ if (mFirst) {
+ // Make sure the fields are properly initialized before checking
+ // whether isCamera()
+ mPictures.get(0).reload();
+ }
+ // Check if the camera preview occupies the full screen.
+ boolean full = !mFilmMode && mPictures.get(0).isCamera()
+ && mPositionController.isCenter()
+ && mPositionController.isAtMinimalScale();
+ if (mFirst || full != mFullScreenCamera) {
+ mFullScreenCamera = full;
+ mFirst = false;
+ mListener.onFullScreenChanged(full);
+ if (full) mHandler.sendEmptyMessage(MSG_UNDO_BAR_FULL_CAMERA);
+ }
+
+ // Determine how many photos we need to draw in addition to the center
+ // one.
+ int neighbors;
+ if (mFullScreenCamera) {
+ neighbors = 0;
+ } else {
+ // In page mode, we draw only one previous/next photo. But if we are
+ // doing capture animation, we want to draw all photos.
+ boolean inPageMode = (mPositionController.getFilmRatio() == 0f);
+ boolean inCaptureAnimation =
+ ((mHolding & HOLD_CAPTURE_ANIMATION) != 0);
+ if (inPageMode && !inCaptureAnimation) {
+ neighbors = 1;
+ } else {
+ neighbors = SCREEN_NAIL_MAX;
+ }
+ }
+
+ // Draw photos from back to front
+ for (int i = neighbors; i >= -neighbors; i--) {
+ Rect r = mPositionController.getPosition(i);
+ mPictures.get(i).draw(canvas, r);
+ }
+
+ renderChild(canvas, mEdgeView);
+ renderChild(canvas, mUndoBar);
+
+ mPositionController.advanceAnimation();
+ checkFocusSwitching();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Film mode focus switching
+ ////////////////////////////////////////////////////////////////////////////
+
+ // Runs in GL thread.
+ private void checkFocusSwitching() {
+ if (!mFilmMode) return;
+ if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return;
+ if (switchPosition() != 0) {
+ mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS);
+ }
+ }
+
+ // Runs in main thread.
+ private void switchFocus() {
+ if (mHolding != 0) return;
+ switch (switchPosition()) {
+ case -1:
+ switchToPrevImage();
+ break;
+ case 1:
+ switchToNextImage();
+ break;
+ }
+ }
+
+ // Returns -1 if we should switch focus to the previous picture, +1 if we
+ // should switch to the next, 0 otherwise.
+ private int switchPosition() {
+ Rect curr = mPositionController.getPosition(0);
+ int center = getWidth() / 2;
+
+ if (curr.left > center && mPrevBound < 0) {
+ Rect prev = mPositionController.getPosition(-1);
+ int currDist = curr.left - center;
+ int prevDist = center - prev.right;
+ if (prevDist < currDist) {
+ return -1;
+ }
+ } else if (curr.right < center && mNextBound > 0) {
+ Rect next = mPositionController.getPosition(1);
+ int currDist = center - curr.right;
+ int nextDist = next.left - center;
+ if (nextDist < currDist) {
+ return 1;
+ }
+ }
+
+ return 0;
+ }
+
+ // Switch to the previous or next picture if the hit position is inside
+ // one of their boxes. This runs in main thread.
+ private void switchToHitPicture(int x, int y) {
+ if (mPrevBound < 0) {
+ Rect r = mPositionController.getPosition(-1);
+ if (r.right >= x) {
+ slideToPrevPicture();
+ return;
+ }
+ }
+
+ if (mNextBound > 0) {
+ Rect r = mPositionController.getPosition(1);
+ if (r.left <= x) {
+ slideToNextPicture();
+ return;
+ }
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Page mode focus switching
+ //
+ // We slide image to the next one or the previous one in two cases: 1: If
+ // the user did a fling gesture with enough velocity. 2 If the user has
+ // moved the picture a lot.
+ ////////////////////////////////////////////////////////////////////////////
+
+ private boolean swipeImages(float velocityX, float velocityY) {
+ if (mFilmMode) return false;
+
+ // Avoid swiping images if we're possibly flinging to view the
+ // zoomed in picture vertically.
+ PositionController controller = mPositionController;
+ boolean isMinimal = controller.isAtMinimalScale();
+ int edges = controller.getImageAtEdges();
+ if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX))
+ if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0
+ || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0)
+ return false;
+
+ // If we are at the edge of the current photo and the sweeping velocity
+ // exceeds the threshold, slide to the next / previous image.
+ if (velocityX < -SWIPE_THRESHOLD && (isMinimal
+ || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) {
+ return slideToNextPicture();
+ } else if (velocityX > SWIPE_THRESHOLD && (isMinimal
+ || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) {
+ return slideToPrevPicture();
+ }
+
+ return false;
+ }
+
+ private void snapback() {
+ if ((mHolding & ~HOLD_DELETE) != 0) return;
+ if (mFilmMode || !snapToNeighborImage()) {
+ mPositionController.snapback();
+ }
+ }
+
+ private boolean snapToNeighborImage() {
+ Rect r = mPositionController.getPosition(0);
+ int viewW = getWidth();
+ // Setting the move threshold proportional to the width of the view
+ int moveThreshold = viewW / 5 ;
+ int threshold = moveThreshold + gapToSide(r.width(), viewW);
+
+ // If we have moved the picture a lot, switching.
+ if (viewW - r.right > threshold) {
+ return slideToNextPicture();
+ } else if (r.left > threshold) {
+ return slideToPrevPicture();
+ }
+
+ return false;
+ }
+
+ private boolean slideToNextPicture() {
+ if (mNextBound <= 0) return false;
+ switchToNextImage();
+ mPositionController.startHorizontalSlide();
+ return true;
+ }
+
+ private boolean slideToPrevPicture() {
+ if (mPrevBound >= 0) return false;
+ switchToPrevImage();
+ mPositionController.startHorizontalSlide();
+ return true;
+ }
+
+ private static int gapToSide(int imageWidth, int viewWidth) {
+ return Math.max(0, (viewWidth - imageWidth) / 2);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Focus switching
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void switchToImage(int index) {
+ mModel.moveTo(index);
+ }
+
+ private void switchToNextImage() {
+ mModel.moveTo(mModel.getCurrentIndex() + 1);
+ }
+
+ private void switchToPrevImage() {
+ mModel.moveTo(mModel.getCurrentIndex() - 1);
+ }
+
+ private void switchToFirstImage() {
+ mModel.moveTo(0);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Opening Animation
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void setOpenAnimationRect(Rect rect) {
+ mPositionController.setOpenAnimationRect(rect);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Capture Animation
+ ////////////////////////////////////////////////////////////////////////////
+
+ public boolean switchWithCaptureAnimation(int offset) {
+ GLRoot root = getGLRoot();
+ if(root == null) return false;
+ root.lockRenderThread();
+ try {
+ return switchWithCaptureAnimationLocked(offset);
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ private boolean switchWithCaptureAnimationLocked(int offset) {
+ if (mHolding != 0) return true;
+ if (offset == 1) {
+ if (mNextBound <= 0) return false;
+ // Temporary disable action bar until the capture animation is done.
+ if (!mFilmMode) mListener.onActionBarAllowed(false);
+ switchToNextImage();
+ mPositionController.startCaptureAnimationSlide(-1);
+ } else if (offset == -1) {
+ if (mPrevBound >= 0) return false;
+ if (mFilmMode) setFilmMode(false);
+
+ // If we are too far away from the first image (so that we don't
+ // have all the ScreenNails in-between), we go directly without
+ // animation.
+ if (mModel.getCurrentIndex() > SCREEN_NAIL_MAX) {
+ switchToFirstImage();
+ mPositionController.skipToFinalPosition();
+ return true;
+ }
+
+ switchToFirstImage();
+ mPositionController.startCaptureAnimationSlide(1);
+ } else {
+ return false;
+ }
+ mHolding |= HOLD_CAPTURE_ANIMATION;
+ Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0);
+ mHandler.sendMessageDelayed(m, PositionController.CAPTURE_ANIMATION_TIME);
+ return true;
+ }
+
+ private void captureAnimationDone(int offset) {
+ mHolding &= ~HOLD_CAPTURE_ANIMATION;
+ if (offset == 1 && !mFilmMode) {
+ // Now the capture animation is done, enable the action bar.
+ mListener.onActionBarAllowed(true);
+ mListener.onActionBarWanted();
+ }
+ snapback();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Card deck effect calculation
+ ////////////////////////////////////////////////////////////////////////////
+
+ // Returns the scrolling progress value for an object moving out of a
+ // view. The progress value measures how much the object has moving out of
+ // the view. The object currently displays in [left, right), and the view is
+ // at [0, viewWidth].
+ //
+ // The returned value is negative when the object is moving right, and
+ // positive when the object is moving left. The value goes to -1 or 1 when
+ // the object just moves out of the view completely. The value is 0 if the
+ // object currently fills the view.
+ private static float calculateMoveOutProgress(int left, int right,
+ int viewWidth) {
+ // w = object width
+ // viewWidth = view width
+ int w = right - left;
+
+ // If the object width is smaller than the view width,
+ // |....view....|
+ // |<-->| progress = -1 when left = viewWidth
+ // |<-->| progress = 0 when left = viewWidth / 2 - w / 2
+ // |<-->| progress = 1 when left = -w
+ if (w < viewWidth) {
+ int zx = viewWidth / 2 - w / 2;
+ if (left > zx) {
+ return -(left - zx) / (float) (viewWidth - zx); // progress = (0, -1]
+ } else {
+ return (left - zx) / (float) (-w - zx); // progress = [0, 1]
+ }
+ }
+
+ // If the object width is larger than the view width,
+ // |..view..|
+ // |<--------->| progress = -1 when left = viewWidth
+ // |<--------->| progress = 0 between left = 0
+ // |<--------->| and right = viewWidth
+ // |<--------->| progress = 1 when right = 0
+ if (left > 0) {
+ return -left / (float) viewWidth;
+ }
+
+ if (right < viewWidth) {
+ return (viewWidth - right) / (float) viewWidth;
+ }
+
+ return 0;
+ }
+
+ // Maps a scrolling progress value to the alpha factor in the fading
+ // animation.
+ private float getScrollAlpha(float scrollProgress) {
+ return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation(
+ 1 - Math.abs(scrollProgress)) : 1.0f;
+ }
+
+ // Maps a scrolling progress value to the scaling factor in the fading
+ // animation.
+ private float getScrollScale(float scrollProgress) {
+ float interpolatedProgress = mScaleInterpolator.getInterpolation(
+ Math.abs(scrollProgress));
+ float scale = (1 - interpolatedProgress) +
+ interpolatedProgress * TRANSITION_SCALE_FACTOR;
+ return scale;
+ }
+
+
+ // This interpolator emulates the rate at which the perceived scale of an
+ // object changes as its distance from a camera increases. When this
+ // interpolator is applied to a scale animation on a view, it evokes the
+ // sense that the object is shrinking due to moving away from the camera.
+ private static class ZInterpolator {
+ private float focalLength;
+
+ public ZInterpolator(float foc) {
+ focalLength = foc;
+ }
+
+ public float getInterpolation(float input) {
+ return (1.0f - focalLength / (focalLength + input)) /
+ (1.0f - focalLength / (focalLength + 1.0f));
+ }
+ }
+
+ // Returns an interpolated value for the page/film transition.
+ // When ratio = 0, the result is from.
+ // When ratio = 1, the result is to.
+ private static float interpolate(float ratio, float from, float to) {
+ return from + (to - from) * ratio * ratio;
+ }
+
+ // Returns the alpha factor in film mode if a picture is not in the center.
+ // The 0.03 lower bound is to make the item always visible a bit.
+ private float getOffsetAlpha(float offset) {
+ offset /= 0.5f;
+ float alpha = (offset > 0) ? (1 - offset) : (1 + offset);
+ return Utils.clamp(alpha, 0.03f, 1f);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Simple public utilities
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public Rect getPhotoRect(int index) {
+ return mPositionController.getPosition(index);
+ }
+
+ public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) {
+ Rect location = new Rect();
+ Utils.assertTrue(root.getBoundsOf(this, location));
+
+ Rect fullRect = bounds();
+ PhotoFallbackEffect effect = new PhotoFallbackEffect();
+ for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+ MediaItem item = mModel.getMediaItem(i);
+ if (item == null) continue;
+ ScreenNail sc = mModel.getScreenNail(i);
+ if (!(sc instanceof TiledScreenNail)
+ || ((TiledScreenNail) sc).isShowingPlaceholder()) continue;
+
+ // Now, sc is BitmapScreenNail and is not showing placeholder
+ Rect rect = new Rect(getPhotoRect(i));
+ if (!Rect.intersects(fullRect, rect)) continue;
+ rect.offset(location.left, location.top);
+
+ int width = sc.getWidth();
+ int height = sc.getHeight();
+
+ int rotation = mModel.getImageRotation(i);
+ RawTexture texture;
+ if ((rotation % 180) == 0) {
+ texture = new RawTexture(width, height, true);
+ canvas.beginRenderTarget(texture);
+ canvas.translate(width / 2f, height / 2f);
+ } else {
+ texture = new RawTexture(height, width, true);
+ canvas.beginRenderTarget(texture);
+ canvas.translate(height / 2f, width / 2f);
+ }
+
+ canvas.rotate(rotation, 0, 0, 1);
+ canvas.translate(-width / 2f, -height / 2f);
+ sc.draw(canvas, 0, 0, width, height);
+ canvas.endRenderTarget();
+ effect.addEntry(item.getPath(), rect, texture);
+ }
+ return effect;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PopupList.java b/src/com/android/gallery3d/ui/PopupList.java
new file mode 100644
index 000000000..248f50b25
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PopupList.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2012 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;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class PopupList {
+
+ public static interface OnPopupItemClickListener {
+ public boolean onPopupItemClick(int itemId);
+ }
+
+ public static class Item {
+ public final int id;
+ public String title;
+
+ public Item(int id, String title) {
+ this.id = id;
+ this.title = title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+ }
+
+ private final Context mContext;
+ private final View mAnchorView;
+ private final ArrayList<Item> mItems = new ArrayList<Item>();
+ private PopupWindow mPopupWindow;
+ private ListView mContentList;
+ private OnPopupItemClickListener mOnPopupItemClickListener;
+ private int mPopupOffsetX;
+ private int mPopupOffsetY;
+ private int mPopupWidth;
+ private int mPopupHeight;
+
+ public PopupList(Context context, View anchorView) {
+ mContext = context;
+ mAnchorView = anchorView;
+ }
+
+ public void setOnPopupItemClickListener(OnPopupItemClickListener listener) {
+ mOnPopupItemClickListener = listener;
+ }
+
+ public void addItem(int id, String title) {
+ mItems.add(new Item(id, title));
+ }
+
+ public void clearItems() {
+ mItems.clear();
+ }
+
+ private final PopupWindow.OnDismissListener mOnDismissListener =
+ new PopupWindow.OnDismissListener() {
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onDismiss() {
+ if (mPopupWindow == null) return;
+ mPopupWindow = null;
+ ViewTreeObserver observer = mAnchorView.getViewTreeObserver();
+ if (observer.isAlive()) {
+ // We used the deprecated function for backward compatibility
+ // The new "removeOnGlobalLayoutListener" is introduced in API level 16
+ observer.removeGlobalOnLayoutListener(mOnGLobalLayoutListener);
+ }
+ }
+ };
+
+ private final OnItemClickListener mOnItemClickListener =
+ new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if (mPopupWindow == null) return;
+ mPopupWindow.dismiss();
+ if (mOnPopupItemClickListener != null) {
+ mOnPopupItemClickListener.onPopupItemClick((int) id);
+ }
+ }
+ };
+
+ private final OnGlobalLayoutListener mOnGLobalLayoutListener =
+ new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ if (mPopupWindow == null) return;
+ updatePopupLayoutParams();
+ // Need to update the position of the popup window
+ mPopupWindow.update(mAnchorView,
+ mPopupOffsetX, mPopupOffsetY, mPopupWidth, mPopupHeight);
+ }
+ };
+
+ public void show() {
+ if (mPopupWindow != null) return;
+ mAnchorView.getViewTreeObserver()
+ .addOnGlobalLayoutListener(mOnGLobalLayoutListener);
+ mPopupWindow = createPopupWindow();
+ updatePopupLayoutParams();
+ mPopupWindow.setWidth(mPopupWidth);
+ mPopupWindow.setHeight(mPopupHeight);
+ mPopupWindow.showAsDropDown(mAnchorView, mPopupOffsetX, mPopupOffsetY);
+ }
+
+ private void updatePopupLayoutParams() {
+ ListView content = mContentList;
+ PopupWindow popup = mPopupWindow;
+
+ Rect p = new Rect();
+ popup.getBackground().getPadding(p);
+
+ int maxHeight = mPopupWindow.getMaxAvailableHeight(mAnchorView) - p.top - p.bottom;
+ mContentList.measure(
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST));
+ mPopupWidth = content.getMeasuredWidth() + p.top + p.bottom;
+ mPopupHeight = Math.min(maxHeight, content.getMeasuredHeight() + p.left + p.right);
+ mPopupOffsetX = -p.left;
+ mPopupOffsetY = -p.top;
+ }
+
+ private PopupWindow createPopupWindow() {
+ PopupWindow popup = new PopupWindow(mContext);
+ popup.setOnDismissListener(mOnDismissListener);
+
+ popup.setBackgroundDrawable(mContext.getResources().getDrawable(
+ R.drawable.menu_dropdown_panel_holo_dark));
+
+ mContentList = new ListView(mContext, null,
+ android.R.attr.dropDownListViewStyle);
+ mContentList.setAdapter(new ItemDataAdapter());
+ mContentList.setOnItemClickListener(mOnItemClickListener);
+ popup.setContentView(mContentList);
+ popup.setFocusable(true);
+ popup.setOutsideTouchable(true);
+
+ return popup;
+ }
+
+ public Item findItem(int id) {
+ for (Item item : mItems) {
+ if (item.id == id) return item;
+ }
+ return null;
+ }
+
+ private class ItemDataAdapter extends BaseAdapter {
+ @Override
+ public int getCount() {
+ return mItems.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mItems.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mItems.get(position).id;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = LayoutInflater.from(mContext)
+ .inflate(R.layout.popup_list_item, null);
+ }
+ TextView text = (TextView) convertView.findViewById(android.R.id.text1);
+ text.setText(mItems.get(position).title);
+ return convertView;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java
new file mode 100644
index 000000000..6a4bcea87
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PositionController.java
@@ -0,0 +1,1821 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.Rect;
+import android.util.Log;
+import android.widget.Scroller;
+
+import com.android.gallery3d.app.PhotoPage;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.ui.PhotoView.Size;
+import com.android.gallery3d.util.GalleryUtils;
+import com.android.gallery3d.util.RangeArray;
+import com.android.gallery3d.util.RangeIntArray;
+
+class PositionController {
+ private static final String TAG = "PositionController";
+
+ public static final int IMAGE_AT_LEFT_EDGE = 1;
+ public static final int IMAGE_AT_RIGHT_EDGE = 2;
+ public static final int IMAGE_AT_TOP_EDGE = 4;
+ public static final int IMAGE_AT_BOTTOM_EDGE = 8;
+
+ public static final int CAPTURE_ANIMATION_TIME = 700;
+ public static final int SNAPBACK_ANIMATION_TIME = 600;
+
+ // Special values for animation time.
+ private static final long NO_ANIMATION = -1;
+ private static final long LAST_ANIMATION = -2;
+
+ private static final int ANIM_KIND_NONE = -1;
+ private static final int ANIM_KIND_SCROLL = 0;
+ private static final int ANIM_KIND_SCALE = 1;
+ private static final int ANIM_KIND_SNAPBACK = 2;
+ private static final int ANIM_KIND_SLIDE = 3;
+ private static final int ANIM_KIND_ZOOM = 4;
+ private static final int ANIM_KIND_OPENING = 5;
+ private static final int ANIM_KIND_FLING = 6;
+ private static final int ANIM_KIND_FLING_X = 7;
+ private static final int ANIM_KIND_DELETE = 8;
+ private static final int ANIM_KIND_CAPTURE = 9;
+
+ // Animation time in milliseconds. The order must match ANIM_KIND_* above.
+ //
+ // The values for ANIM_KIND_FLING_X does't matter because we use
+ // mFilmScroller.isFinished() to decide when to stop. We set it to 0 so it's
+ // faster for Animatable.advanceAnimation() to calculate the progress
+ // (always 1).
+ private static final int ANIM_TIME[] = {
+ 0, // ANIM_KIND_SCROLL
+ 0, // ANIM_KIND_SCALE
+ SNAPBACK_ANIMATION_TIME, // ANIM_KIND_SNAPBACK
+ 400, // ANIM_KIND_SLIDE
+ 300, // ANIM_KIND_ZOOM
+ 300, // ANIM_KIND_OPENING
+ 0, // ANIM_KIND_FLING (the duration is calculated dynamically)
+ 0, // ANIM_KIND_FLING_X (see the comment above)
+ 0, // ANIM_KIND_DELETE (the duration is calculated dynamically)
+ CAPTURE_ANIMATION_TIME, // ANIM_KIND_CAPTURE
+ };
+
+ // 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;
+
+ // For user's gestures, we give a temporary extra scaling range which goes
+ // above or below the usual scaling limits.
+ private static final float SCALE_MIN_EXTRA = 0.7f;
+ private static final float SCALE_MAX_EXTRA = 1.4f;
+
+ // Setting this true makes the extra scaling range permanent (until this is
+ // set to false again).
+ private boolean mExtraScalingRange = false;
+
+ // Film Mode v.s. Page Mode: in film mode we show smaller pictures.
+ private boolean mFilmMode = false;
+
+ // These are the limits for width / height of the picture in film mode.
+ private static final float FILM_MODE_PORTRAIT_HEIGHT = 0.48f;
+ private static final float FILM_MODE_PORTRAIT_WIDTH = 0.7f;
+ private static final float FILM_MODE_LANDSCAPE_HEIGHT = 0.7f;
+ private static final float FILM_MODE_LANDSCAPE_WIDTH = 0.7f;
+
+ // In addition to the focused box (index == 0). We also keep information
+ // about this many boxes on each side.
+ private static final int BOX_MAX = PhotoView.SCREEN_NAIL_MAX;
+ private static final int[] CENTER_OUT_INDEX = new int[2 * BOX_MAX + 1];
+
+ private static final int IMAGE_GAP = GalleryUtils.dpToPixel(16);
+ private static final int HORIZONTAL_SLACK = GalleryUtils.dpToPixel(12);
+
+ // These are constants for the delete gesture.
+ private static final int DEFAULT_DELETE_ANIMATION_DURATION = 200; // ms
+ private static final int MAX_DELETE_ANIMATION_DURATION = 400; // ms
+
+ private Listener mListener;
+ private volatile Rect mOpenAnimationRect;
+
+ // Use a large enough value, so we won't see the gray shadow in the beginning.
+ private int mViewW = 1200;
+ private int mViewH = 1200;
+
+ // A scaling gesture is in progress.
+ private boolean mInScale;
+ // The focus point of the scaling gesture, relative to the center of the
+ // picture in bitmap pixels.
+ private float mFocusX, mFocusY;
+
+ // whether there is a previous/next picture.
+ private boolean mHasPrev, mHasNext;
+
+ // This is used by the fling animation (page mode).
+ private FlingScroller mPageScroller;
+
+ // This is used by the fling animation (film mode).
+ private Scroller mFilmScroller;
+
+ // The bound of the stable region that the focused box can stay, see the
+ // comments above calculateStableBound() for details.
+ private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
+
+ // Constrained frame is a rectangle that the focused box should fit into if
+ // it is constrained. It has two effects:
+ //
+ // (1) In page mode, if the focused box is constrained, scaling for the
+ // focused box is adjusted to fit into the constrained frame, instead of the
+ // whole view.
+ //
+ // (2) In page mode, if the focused box is constrained, the mPlatform's
+ // default center (mDefaultX/Y) is moved to the center of the constrained
+ // frame, instead of the view center.
+ //
+ private Rect mConstrainedFrame = new Rect();
+
+ // Whether the focused box is constrained.
+ //
+ // Our current program's first call to moveBox() sets constrained = true, so
+ // we set the initial value of this variable to true, and we will not see
+ // see unwanted transition animation.
+ private boolean mConstrained = true;
+
+ //
+ // ___________________________________________________________
+ // | _____ _____ _____ _____ _____ |
+ // | | | | | | | | | | | |
+ // | | Box | | Box | | Box*| | Box | | Box | |
+ // | |_____|.....|_____|.....|_____|.....|_____|.....|_____| |
+ // | Gap Gap Gap Gap |
+ // |___________________________________________________________|
+ //
+ // <-- Platform -->
+ //
+ // The focused box (Box*) centers at mPlatform's (mCurrentX, mCurrentY)
+
+ private Platform mPlatform = new Platform();
+ private RangeArray<Box> mBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
+ // The gap at the right of a Box i is at index i. The gap at the left of a
+ // Box i is at index i - 1.
+ private RangeArray<Gap> mGaps = new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
+ private FilmRatio mFilmRatio = new FilmRatio();
+
+ // These are only used during moveBox().
+ private RangeArray<Box> mTempBoxes = new RangeArray<Box>(-BOX_MAX, BOX_MAX);
+ private RangeArray<Gap> mTempGaps =
+ new RangeArray<Gap>(-BOX_MAX, BOX_MAX - 1);
+
+ // The output of the PositionController. Available through getPosition().
+ private RangeArray<Rect> mRects = new RangeArray<Rect>(-BOX_MAX, BOX_MAX);
+
+ // The direction of a new picture should appear. New pictures pop from top
+ // if this value is true, or from bottom if this value is false.
+ boolean mPopFromTop;
+
+ public interface Listener {
+ void invalidate();
+ boolean isHoldingDown();
+ boolean isHoldingDelete();
+
+ // EdgeView
+ void onPull(int offset, int direction);
+ void onRelease();
+ void onAbsorb(int velocity, int direction);
+ }
+
+ static {
+ // Initialize the CENTER_OUT_INDEX array.
+ // The array maps 0, 1, 2, 3, 4, ..., 2 * BOX_MAX
+ // to 0, 1, -1, 2, -2, ..., BOX_MAX, -BOX_MAX
+ for (int i = 0; i < CENTER_OUT_INDEX.length; i++) {
+ int j = (i + 1) / 2;
+ if ((i & 1) == 0) j = -j;
+ CENTER_OUT_INDEX[i] = j;
+ }
+ }
+
+ public PositionController(Context context, Listener listener) {
+ mListener = listener;
+ mPageScroller = new FlingScroller();
+ mFilmScroller = new Scroller(context, null, false);
+
+ // Initialize the areas.
+ initPlatform();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ mBoxes.put(i, new Box());
+ initBox(i);
+ mRects.put(i, new Rect());
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ mGaps.put(i, new Gap());
+ initGap(i);
+ }
+ }
+
+ public void setOpenAnimationRect(Rect r) {
+ mOpenAnimationRect = r;
+ }
+
+ public void setViewSize(int viewW, int viewH) {
+ if (viewW == mViewW && viewH == mViewH) return;
+
+ boolean wasMinimal = isAtMinimalScale();
+
+ mViewW = viewW;
+ mViewH = viewH;
+ initPlatform();
+
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ setBoxSize(i, viewW, viewH, true);
+ }
+
+ updateScaleAndGapLimit();
+
+ // If the focused box was at minimal scale, we try to make it the
+ // minimal scale under the new view size.
+ if (wasMinimal) {
+ Box b = mBoxes.get(0);
+ b.mCurrentScale = b.mScaleMin;
+ }
+
+ // If we have the opening animation, do it. Otherwise go directly to the
+ // right position.
+ if (!startOpeningAnimationIfNeeded()) {
+ skipToFinalPosition();
+ }
+ }
+
+ public void setConstrainedFrame(Rect cFrame) {
+ if (mConstrainedFrame.equals(cFrame)) return;
+ mConstrainedFrame.set(cFrame);
+ mPlatform.updateDefaultXY();
+ updateScaleAndGapLimit();
+ snapAndRedraw();
+ }
+
+ public void forceImageSize(int index, Size s) {
+ if (s.width == 0 || s.height == 0) return;
+ Box b = mBoxes.get(index);
+ b.mImageW = s.width;
+ b.mImageH = s.height;
+ return;
+ }
+
+ public void setImageSize(int index, Size s, Rect cFrame) {
+ if (s.width == 0 || s.height == 0) return;
+
+ boolean needUpdate = false;
+ if (cFrame != null && !mConstrainedFrame.equals(cFrame)) {
+ mConstrainedFrame.set(cFrame);
+ mPlatform.updateDefaultXY();
+ needUpdate = true;
+ }
+ needUpdate |= setBoxSize(index, s.width, s.height, false);
+
+ if (!needUpdate) return;
+ updateScaleAndGapLimit();
+ snapAndRedraw();
+ }
+
+ // Returns false if the box size doesn't change.
+ private boolean setBoxSize(int i, int width, int height, boolean isViewSize) {
+ Box b = mBoxes.get(i);
+ boolean wasViewSize = b.mUseViewSize;
+
+ // If we already have an image size, we don't want to use the view size.
+ if (!wasViewSize && isViewSize) return false;
+
+ b.mUseViewSize = isViewSize;
+
+ if (width == b.mImageW && height == b.mImageH) {
+ return false;
+ }
+
+ // The ratio of the old size and the new size.
+ //
+ // If the aspect ratio changes, we don't know if it is because one side
+ // grows or the other side shrinks. Currently we just assume the view
+ // angle of the longer side doesn't change (so the aspect ratio change
+ // is because the view angle of the shorter side changes). This matches
+ // what camera preview does.
+ float ratio = (width > height)
+ ? (float) b.mImageW / width
+ : (float) b.mImageH / height;
+
+ b.mImageW = width;
+ b.mImageH = height;
+
+ // If this is the first time we receive an image size or we are in fullscreen,
+ // we change the scale directly. Otherwise adjust the scales by a ratio,
+ // and snapback will animate the scale into the min/max bounds if necessary.
+ if ((wasViewSize && !isViewSize) || !mFilmMode) {
+ b.mCurrentScale = getMinimalScale(b);
+ b.mAnimationStartTime = NO_ANIMATION;
+ } else {
+ b.mCurrentScale *= ratio;
+ b.mFromScale *= ratio;
+ b.mToScale *= ratio;
+ }
+
+ if (i == 0) {
+ mFocusX /= ratio;
+ mFocusY /= ratio;
+ }
+
+ return true;
+ }
+
+ private boolean startOpeningAnimationIfNeeded() {
+ if (mOpenAnimationRect == null) return false;
+ Box b = mBoxes.get(0);
+ if (b.mUseViewSize) return false;
+
+ // Start animation from the saved rectangle if we have one.
+ Rect r = mOpenAnimationRect;
+ mOpenAnimationRect = null;
+
+ mPlatform.mCurrentX = r.centerX() - mViewW / 2;
+ b.mCurrentY = r.centerY() - mViewH / 2;
+ b.mCurrentScale = Math.max(r.width() / (float) b.mImageW,
+ r.height() / (float) b.mImageH);
+ startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin,
+ ANIM_KIND_OPENING);
+
+ // Animate from large gaps for neighbor boxes to avoid them
+ // shown on the screen during opening animation.
+ for (int i = -1; i < 1; i++) {
+ Gap g = mGaps.get(i);
+ g.mCurrentGap = mViewW;
+ g.doAnimation(g.mDefaultSize, ANIM_KIND_OPENING);
+ }
+
+ return true;
+ }
+
+ public void setFilmMode(boolean enabled) {
+ if (enabled == mFilmMode) return;
+ mFilmMode = enabled;
+
+ mPlatform.updateDefaultXY();
+ updateScaleAndGapLimit();
+ stopAnimation();
+ snapAndRedraw();
+ }
+
+ public void setExtraScalingRange(boolean enabled) {
+ if (mExtraScalingRange == enabled) return;
+ mExtraScalingRange = enabled;
+ if (!enabled) {
+ snapAndRedraw();
+ }
+ }
+
+ // This should be called whenever the scale range of boxes or the default
+ // gap size may change. Currently this can happen due to change of view
+ // size, image size, mFilmMode, mConstrained, and mConstrainedFrame.
+ private void updateScaleAndGapLimit() {
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ Box b = mBoxes.get(i);
+ b.mScaleMin = getMinimalScale(b);
+ b.mScaleMax = getMaximalScale(b);
+ }
+
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ Gap g = mGaps.get(i);
+ g.mDefaultSize = getDefaultGapSize(i);
+ }
+ }
+
+ // Returns the default gap size according the the size of the boxes around
+ // the gap and the current mode.
+ private int getDefaultGapSize(int i) {
+ if (mFilmMode) return IMAGE_GAP;
+ Box a = mBoxes.get(i);
+ Box b = mBoxes.get(i + 1);
+ return IMAGE_GAP + Math.max(gapToSide(a), gapToSide(b));
+ }
+
+ // Here is how we layout the boxes in the page mode.
+ //
+ // previous current next
+ // ___________ ________________ __________
+ // | _______ | | __________ | | ______ |
+ // | | | | | | right->| | | | | |
+ // | | |<-------->|<--left | | | | | |
+ // | |_______| | | | |__________| | | |______| |
+ // |___________| | |________________| |__________|
+ // | <--> gapToSide()
+ // |
+ // IMAGE_GAP + MAX(gapToSide(previous), gapToSide(current))
+ private int gapToSide(Box b) {
+ return (int) ((mViewW - getMinimalScale(b) * b.mImageW) / 2 + 0.5f);
+ }
+
+ // Stop all animations at where they are now.
+ public void stopAnimation() {
+ mPlatform.mAnimationStartTime = NO_ANIMATION;
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ mBoxes.get(i).mAnimationStartTime = NO_ANIMATION;
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ mGaps.get(i).mAnimationStartTime = NO_ANIMATION;
+ }
+ }
+
+ public void skipAnimation() {
+ if (mPlatform.mAnimationStartTime != NO_ANIMATION) {
+ mPlatform.mCurrentX = mPlatform.mToX;
+ mPlatform.mCurrentY = mPlatform.mToY;
+ mPlatform.mAnimationStartTime = NO_ANIMATION;
+ }
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ Box b = mBoxes.get(i);
+ if (b.mAnimationStartTime == NO_ANIMATION) continue;
+ b.mCurrentY = b.mToY;
+ b.mCurrentScale = b.mToScale;
+ b.mAnimationStartTime = NO_ANIMATION;
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ Gap g = mGaps.get(i);
+ if (g.mAnimationStartTime == NO_ANIMATION) continue;
+ g.mCurrentGap = g.mToGap;
+ g.mAnimationStartTime = NO_ANIMATION;
+ }
+ redraw();
+ }
+
+ public void snapback() {
+ snapAndRedraw();
+ }
+
+ public void skipToFinalPosition() {
+ stopAnimation();
+ snapAndRedraw();
+ skipAnimation();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Start an animations for the focused box
+ ////////////////////////////////////////////////////////////////////////////
+
+ public void zoomIn(float tapX, float tapY, float targetScale) {
+ tapX -= mViewW / 2;
+ tapY -= mViewH / 2;
+ Box b = mBoxes.get(0);
+
+ // Convert the tap position to distance to center in bitmap coordinates
+ float tempX = (tapX - mPlatform.mCurrentX) / b.mCurrentScale;
+ float tempY = (tapY - b.mCurrentY) / b.mCurrentScale;
+
+ int x = (int) (-tempX * targetScale + 0.5f);
+ int y = (int) (-tempY * targetScale + 0.5f);
+
+ calculateStableBound(targetScale);
+ int targetX = Utils.clamp(x, mBoundLeft, mBoundRight);
+ int targetY = Utils.clamp(y, mBoundTop, mBoundBottom);
+ targetScale = Utils.clamp(targetScale, b.mScaleMin, b.mScaleMax);
+
+ startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
+ }
+
+ public void resetToFullView() {
+ Box b = mBoxes.get(0);
+ startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_ZOOM);
+ }
+
+ public void beginScale(float focusX, float focusY) {
+ focusX -= mViewW / 2;
+ focusY -= mViewH / 2;
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+ mInScale = true;
+ mFocusX = (int) ((focusX - p.mCurrentX) / b.mCurrentScale + 0.5f);
+ mFocusY = (int) ((focusY - b.mCurrentY) / b.mCurrentScale + 0.5f);
+ }
+
+ // Scales the image by the given factor.
+ // Returns an out-of-range indicator:
+ // 1 if the intended scale is too large for the stable range.
+ // 0 if the intended scale is in the stable range.
+ // -1 if the intended scale is too small for the stable range.
+ public int scaleBy(float s, float focusX, float focusY) {
+ focusX -= mViewW / 2;
+ focusY -= mViewH / 2;
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // We want to keep the focus point (on the bitmap) the same as when we
+ // begin the scale gesture, that is,
+ //
+ // (focusX' - currentX') / scale' = (focusX - currentX) / scale
+ //
+ s = b.clampScale(s * getTargetScale(b));
+ int x = mFilmMode ? p.mCurrentX : (int) (focusX - s * mFocusX + 0.5f);
+ int y = mFilmMode ? b.mCurrentY : (int) (focusY - s * mFocusY + 0.5f);
+ startAnimation(x, y, s, ANIM_KIND_SCALE);
+ if (s < b.mScaleMin) return -1;
+ if (s > b.mScaleMax) return 1;
+ return 0;
+ }
+
+ public void endScale() {
+ mInScale = false;
+ snapAndRedraw();
+ }
+
+ // Slide the focused box to the center of the view.
+ public void startHorizontalSlide() {
+ Box b = mBoxes.get(0);
+ startAnimation(mPlatform.mDefaultX, 0, b.mScaleMin, ANIM_KIND_SLIDE);
+ }
+
+ // Slide the focused box to the center of the view with the capture
+ // animation. In addition to the sliding, the animation will also scale the
+ // the focused box, the specified neighbor box, and the gap between the
+ // two. The specified offset should be 1 or -1.
+ public void startCaptureAnimationSlide(int offset) {
+ Box b = mBoxes.get(0);
+ Box n = mBoxes.get(offset); // the neighbor box
+ Gap g = mGaps.get(offset); // the gap between the two boxes
+
+ mPlatform.doAnimation(mPlatform.mDefaultX, mPlatform.mDefaultY,
+ ANIM_KIND_CAPTURE);
+ b.doAnimation(0, b.mScaleMin, ANIM_KIND_CAPTURE);
+ n.doAnimation(0, n.mScaleMin, ANIM_KIND_CAPTURE);
+ g.doAnimation(g.mDefaultSize, ANIM_KIND_CAPTURE);
+ redraw();
+ }
+
+ // Only allow scrolling when we are not currently in an animation or we
+ // are in some animation with can be interrupted.
+ private boolean canScroll() {
+ Box b = mBoxes.get(0);
+ if (b.mAnimationStartTime == NO_ANIMATION) return true;
+ switch (b.mAnimationKind) {
+ case ANIM_KIND_SCROLL:
+ case ANIM_KIND_FLING:
+ case ANIM_KIND_FLING_X:
+ return true;
+ }
+ return false;
+ }
+
+ public void scrollPage(int dx, int dy) {
+ if (!canScroll()) return;
+
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ calculateStableBound(b.mCurrentScale);
+
+ int x = p.mCurrentX + dx;
+ int y = b.mCurrentY + dy;
+
+ // Vertical direction: If we have space to move in the vertical
+ // direction, we show the edge effect when scrolling reaches the edge.
+ if (mBoundTop != mBoundBottom) {
+ if (y < mBoundTop) {
+ mListener.onPull(mBoundTop - y, EdgeView.BOTTOM);
+ } else if (y > mBoundBottom) {
+ mListener.onPull(y - mBoundBottom, EdgeView.TOP);
+ }
+ }
+
+ y = Utils.clamp(y, mBoundTop, mBoundBottom);
+
+ // Horizontal direction: we show the edge effect when the scrolling
+ // tries to go left of the first image or go right of the last image.
+ if (!mHasPrev && x > mBoundRight) {
+ int pixels = x - mBoundRight;
+ mListener.onPull(pixels, EdgeView.LEFT);
+ x = mBoundRight;
+ } else if (!mHasNext && x < mBoundLeft) {
+ int pixels = mBoundLeft - x;
+ mListener.onPull(pixels, EdgeView.RIGHT);
+ x = mBoundLeft;
+ }
+
+ startAnimation(x, y, b.mCurrentScale, ANIM_KIND_SCROLL);
+ }
+
+ public void scrollFilmX(int dx) {
+ if (!canScroll()) return;
+
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // Only allow scrolling when we are not currently in an animation or we
+ // are in some animation with can be interrupted.
+ if (b.mAnimationStartTime != NO_ANIMATION) {
+ switch (b.mAnimationKind) {
+ case ANIM_KIND_SCROLL:
+ case ANIM_KIND_FLING:
+ case ANIM_KIND_FLING_X:
+ break;
+ default:
+ return;
+ }
+ }
+
+ int x = p.mCurrentX + dx;
+
+ // Horizontal direction: we show the edge effect when the scrolling
+ // tries to go left of the first image or go right of the last image.
+ x -= mPlatform.mDefaultX;
+ if (!mHasPrev && x > 0) {
+ mListener.onPull(x, EdgeView.LEFT);
+ x = 0;
+ } else if (!mHasNext && x < 0) {
+ mListener.onPull(-x, EdgeView.RIGHT);
+ x = 0;
+ }
+ x += mPlatform.mDefaultX;
+ startAnimation(x, b.mCurrentY, b.mCurrentScale, ANIM_KIND_SCROLL);
+ }
+
+ public void scrollFilmY(int boxIndex, int dy) {
+ if (!canScroll()) return;
+
+ Box b = mBoxes.get(boxIndex);
+ int y = b.mCurrentY + dy;
+ b.doAnimation(y, b.mCurrentScale, ANIM_KIND_SCROLL);
+ redraw();
+ }
+
+ public boolean flingPage(int velocityX, int velocityY) {
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // We only want to do fling when the picture is zoomed-in.
+ if (viewWiderThanScaledImage(b.mCurrentScale) &&
+ viewTallerThanScaledImage(b.mCurrentScale)) {
+ return false;
+ }
+
+ // We only allow flinging in the directions where it won't go over the
+ // picture.
+ int edges = getImageAtEdges();
+ if ((velocityX > 0 && (edges & IMAGE_AT_LEFT_EDGE) != 0) ||
+ (velocityX < 0 && (edges & IMAGE_AT_RIGHT_EDGE) != 0)) {
+ velocityX = 0;
+ }
+ if ((velocityY > 0 && (edges & IMAGE_AT_TOP_EDGE) != 0) ||
+ (velocityY < 0 && (edges & IMAGE_AT_BOTTOM_EDGE) != 0)) {
+ velocityY = 0;
+ }
+
+ if (velocityX == 0 && velocityY == 0) return false;
+
+ mPageScroller.fling(p.mCurrentX, b.mCurrentY, velocityX, velocityY,
+ mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
+ int targetX = mPageScroller.getFinalX();
+ int targetY = mPageScroller.getFinalY();
+ ANIM_TIME[ANIM_KIND_FLING] = mPageScroller.getDuration();
+ return startAnimation(targetX, targetY, b.mCurrentScale, ANIM_KIND_FLING);
+ }
+
+ public boolean flingFilmX(int velocityX) {
+ if (velocityX == 0) return false;
+
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+
+ // If we are already at the edge, don't start the fling.
+ int defaultX = p.mDefaultX;
+ if ((!mHasPrev && p.mCurrentX >= defaultX)
+ || (!mHasNext && p.mCurrentX <= defaultX)) {
+ return false;
+ }
+
+ mFilmScroller.fling(p.mCurrentX, 0, velocityX, 0,
+ Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
+ int targetX = mFilmScroller.getFinalX();
+ return startAnimation(
+ targetX, b.mCurrentY, b.mCurrentScale, ANIM_KIND_FLING_X);
+ }
+
+ // Moves the specified box out of screen. If velocityY is 0, a default
+ // velocity is used. Returns the time for the duration, or -1 if we cannot
+ // not do the animation.
+ public int flingFilmY(int boxIndex, int velocityY) {
+ Box b = mBoxes.get(boxIndex);
+
+ // Calculate targetY
+ int h = heightOf(b);
+ int targetY;
+ int FUZZY = 3; // TODO: figure out why this is needed.
+ if (velocityY < 0 || (velocityY == 0 && b.mCurrentY <= 0)) {
+ targetY = -mViewH / 2 - (h + 1) / 2 - FUZZY;
+ } else {
+ targetY = (mViewH + 1) / 2 + h / 2 + FUZZY;
+ }
+
+ // Calculate duration
+ int duration;
+ if (velocityY != 0) {
+ duration = (int) (Math.abs(targetY - b.mCurrentY) * 1000f
+ / Math.abs(velocityY));
+ duration = Math.min(MAX_DELETE_ANIMATION_DURATION, duration);
+ } else {
+ duration = DEFAULT_DELETE_ANIMATION_DURATION;
+ }
+
+ // Start animation
+ ANIM_TIME[ANIM_KIND_DELETE] = duration;
+ if (b.doAnimation(targetY, b.mCurrentScale, ANIM_KIND_DELETE)) {
+ redraw();
+ return duration;
+ }
+ return -1;
+ }
+
+ // Returns the index of the box which contains the given point (x, y)
+ // Returns Integer.MAX_VALUE if there is no hit. There may be more than
+ // one box contains the given point, and we want to give priority to the
+ // one closer to the focused index (0).
+ public int hitTest(int x, int y) {
+ for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+ int j = CENTER_OUT_INDEX[i];
+ Rect r = mRects.get(j);
+ if (r.contains(x, y)) {
+ return j;
+ }
+ }
+
+ return Integer.MAX_VALUE;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Redraw
+ //
+ // If a method changes box positions directly, redraw()
+ // should be called.
+ //
+ // If a method may also cause a snapback to happen, snapAndRedraw() should
+ // be called.
+ //
+ // If a method starts an animation to change the position of focused box,
+ // startAnimation() should be called.
+ //
+ // If time advances to change the box position, advanceAnimation() should
+ // be called.
+ ////////////////////////////////////////////////////////////////////////////
+ private void redraw() {
+ layoutAndSetPosition();
+ mListener.invalidate();
+ }
+
+ private void snapAndRedraw() {
+ mPlatform.startSnapback();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ mBoxes.get(i).startSnapback();
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ mGaps.get(i).startSnapback();
+ }
+ mFilmRatio.startSnapback();
+ redraw();
+ }
+
+ private boolean startAnimation(int targetX, int targetY, float targetScale,
+ int kind) {
+ boolean changed = false;
+ changed |= mPlatform.doAnimation(targetX, mPlatform.mDefaultY, kind);
+ changed |= mBoxes.get(0).doAnimation(targetY, targetScale, kind);
+ if (changed) redraw();
+ return changed;
+ }
+
+ public void advanceAnimation() {
+ boolean changed = false;
+ changed |= mPlatform.advanceAnimation();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ changed |= mBoxes.get(i).advanceAnimation();
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ changed |= mGaps.get(i).advanceAnimation();
+ }
+ changed |= mFilmRatio.advanceAnimation();
+ if (changed) redraw();
+ }
+
+ public boolean inOpeningAnimation() {
+ return (mPlatform.mAnimationKind == ANIM_KIND_OPENING &&
+ mPlatform.mAnimationStartTime != NO_ANIMATION) ||
+ (mBoxes.get(0).mAnimationKind == ANIM_KIND_OPENING &&
+ mBoxes.get(0).mAnimationStartTime != NO_ANIMATION);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Layout
+ ////////////////////////////////////////////////////////////////////////////
+
+ // Returns the display width of this box.
+ private int widthOf(Box b) {
+ return (int) (b.mImageW * b.mCurrentScale + 0.5f);
+ }
+
+ // Returns the display height of this box.
+ private int heightOf(Box b) {
+ return (int) (b.mImageH * b.mCurrentScale + 0.5f);
+ }
+
+ // Returns the display width of this box, using the given scale.
+ private int widthOf(Box b, float scale) {
+ return (int) (b.mImageW * scale + 0.5f);
+ }
+
+ // Returns the display height of this box, using the given scale.
+ private int heightOf(Box b, float scale) {
+ return (int) (b.mImageH * scale + 0.5f);
+ }
+
+ // Convert the information in mPlatform and mBoxes to mRects, so the user
+ // can get the position of each box by getPosition().
+ //
+ // Note we go from center-out because each box's X coordinate
+ // is relative to its anchor box (except the focused box).
+ private void layoutAndSetPosition() {
+ for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+ convertBoxToRect(CENTER_OUT_INDEX[i]);
+ }
+ //dumpState();
+ }
+
+ @SuppressWarnings("unused")
+ private void dumpState() {
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ Log.d(TAG, "Gap " + i + ": " + mGaps.get(i).mCurrentGap);
+ }
+
+ for (int i = 0; i < 2 * BOX_MAX + 1; i++) {
+ dumpRect(CENTER_OUT_INDEX[i]);
+ }
+
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ for (int j = i + 1; j <= BOX_MAX; j++) {
+ if (Rect.intersects(mRects.get(i), mRects.get(j))) {
+ Log.d(TAG, "rect " + i + " and rect " + j + "intersects!");
+ }
+ }
+ }
+ }
+
+ private void dumpRect(int i) {
+ StringBuilder sb = new StringBuilder();
+ Rect r = mRects.get(i);
+ sb.append("Rect " + i + ":");
+ sb.append("(");
+ sb.append(r.centerX());
+ sb.append(",");
+ sb.append(r.centerY());
+ sb.append(") [");
+ sb.append(r.width());
+ sb.append("x");
+ sb.append(r.height());
+ sb.append("]");
+ Log.d(TAG, sb.toString());
+ }
+
+ private void convertBoxToRect(int i) {
+ Box b = mBoxes.get(i);
+ Rect r = mRects.get(i);
+ int y = b.mCurrentY + mPlatform.mCurrentY + mViewH / 2;
+ int w = widthOf(b);
+ int h = heightOf(b);
+ if (i == 0) {
+ int x = mPlatform.mCurrentX + mViewW / 2;
+ r.left = x - w / 2;
+ r.right = r.left + w;
+ } else if (i > 0) {
+ Rect a = mRects.get(i - 1);
+ Gap g = mGaps.get(i - 1);
+ r.left = a.right + g.mCurrentGap;
+ r.right = r.left + w;
+ } else { // i < 0
+ Rect a = mRects.get(i + 1);
+ Gap g = mGaps.get(i);
+ r.right = a.left - g.mCurrentGap;
+ r.left = r.right - w;
+ }
+ r.top = y - h / 2;
+ r.bottom = r.top + h;
+ }
+
+ // Returns the position of a box.
+ public Rect getPosition(int index) {
+ return mRects.get(index);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Box management
+ ////////////////////////////////////////////////////////////////////////////
+
+ // Initialize the platform to be at the view center.
+ private void initPlatform() {
+ mPlatform.updateDefaultXY();
+ mPlatform.mCurrentX = mPlatform.mDefaultX;
+ mPlatform.mCurrentY = mPlatform.mDefaultY;
+ mPlatform.mAnimationStartTime = NO_ANIMATION;
+ }
+
+ // Initialize a box to have the size of the view.
+ private void initBox(int index) {
+ Box b = mBoxes.get(index);
+ b.mImageW = mViewW;
+ b.mImageH = mViewH;
+ b.mUseViewSize = true;
+ b.mScaleMin = getMinimalScale(b);
+ b.mScaleMax = getMaximalScale(b);
+ b.mCurrentY = 0;
+ b.mCurrentScale = b.mScaleMin;
+ b.mAnimationStartTime = NO_ANIMATION;
+ b.mAnimationKind = ANIM_KIND_NONE;
+ }
+
+ // Initialize a box to a given size.
+ private void initBox(int index, Size size) {
+ if (size.width == 0 || size.height == 0) {
+ initBox(index);
+ return;
+ }
+ Box b = mBoxes.get(index);
+ b.mImageW = size.width;
+ b.mImageH = size.height;
+ b.mUseViewSize = false;
+ b.mScaleMin = getMinimalScale(b);
+ b.mScaleMax = getMaximalScale(b);
+ b.mCurrentY = 0;
+ b.mCurrentScale = b.mScaleMin;
+ b.mAnimationStartTime = NO_ANIMATION;
+ b.mAnimationKind = ANIM_KIND_NONE;
+ }
+
+ // Initialize a gap. This can only be called after the boxes around the gap
+ // has been initialized.
+ private void initGap(int index) {
+ Gap g = mGaps.get(index);
+ g.mDefaultSize = getDefaultGapSize(index);
+ g.mCurrentGap = g.mDefaultSize;
+ g.mAnimationStartTime = NO_ANIMATION;
+ }
+
+ private void initGap(int index, int size) {
+ Gap g = mGaps.get(index);
+ g.mDefaultSize = getDefaultGapSize(index);
+ g.mCurrentGap = size;
+ g.mAnimationStartTime = NO_ANIMATION;
+ }
+
+ @SuppressWarnings("unused")
+ private void debugMoveBox(int fromIndex[]) {
+ StringBuilder s = new StringBuilder("moveBox:");
+ for (int i = 0; i < fromIndex.length; i++) {
+ int j = fromIndex[i];
+ if (j == Integer.MAX_VALUE) {
+ s.append(" N");
+ } else {
+ s.append(" ");
+ s.append(fromIndex[i]);
+ }
+ }
+ Log.d(TAG, s.toString());
+ }
+
+ // Move the boxes: it may indicate focus change, box deleted, box appearing,
+ // box reordered, etc.
+ //
+ // Each element in the fromIndex array indicates where each box was in the
+ // old array. If the value is Integer.MAX_VALUE (pictured as N below), it
+ // means the box is new.
+ //
+ // For example:
+ // N N N N N N N -- all new boxes
+ // -3 -2 -1 0 1 2 3 -- nothing changed
+ // -2 -1 0 1 2 3 N -- focus goes to the next box
+ // N -3 -2 -1 0 1 2 -- focus goes to the previous box
+ // -3 -2 -1 1 2 3 N -- the focused box was deleted.
+ //
+ // hasPrev/hasNext indicates if there are previous/next boxes for the
+ // focused box. constrained indicates whether the focused box should be put
+ // into the constrained frame.
+ public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext,
+ boolean constrained, Size[] sizes) {
+ //debugMoveBox(fromIndex);
+ mHasPrev = hasPrev;
+ mHasNext = hasNext;
+
+ RangeIntArray from = new RangeIntArray(fromIndex, -BOX_MAX, BOX_MAX);
+
+ // 1. Get the absolute X coordinates for the boxes.
+ layoutAndSetPosition();
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ Box b = mBoxes.get(i);
+ Rect r = mRects.get(i);
+ b.mAbsoluteX = r.centerX() - mViewW / 2;
+ }
+
+ // 2. copy boxes and gaps to temporary storage.
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ mTempBoxes.put(i, mBoxes.get(i));
+ mBoxes.put(i, null);
+ }
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ mTempGaps.put(i, mGaps.get(i));
+ mGaps.put(i, null);
+ }
+
+ // 3. move back boxes that are used in the new array.
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ int j = from.get(i);
+ if (j == Integer.MAX_VALUE) continue;
+ mBoxes.put(i, mTempBoxes.get(j));
+ mTempBoxes.put(j, null);
+ }
+
+ // 4. move back gaps if both boxes around it are kept together.
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ int j = from.get(i);
+ if (j == Integer.MAX_VALUE) continue;
+ int k = from.get(i + 1);
+ if (k == Integer.MAX_VALUE) continue;
+ if (j + 1 == k) {
+ mGaps.put(i, mTempGaps.get(j));
+ mTempGaps.put(j, null);
+ }
+ }
+
+ // 5. recycle the boxes that are not used in the new array.
+ int k = -BOX_MAX;
+ for (int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ if (mBoxes.get(i) != null) continue;
+ while (mTempBoxes.get(k) == null) {
+ k++;
+ }
+ mBoxes.put(i, mTempBoxes.get(k++));
+ initBox(i, sizes[i + BOX_MAX]);
+ }
+
+ // 6. Now give the recycled box a reasonable absolute X position.
+ //
+ // First try to find the first and the last box which the absolute X
+ // position is known.
+ int first, last;
+ for (first = -BOX_MAX; first <= BOX_MAX; first++) {
+ if (from.get(first) != Integer.MAX_VALUE) break;
+ }
+ for (last = BOX_MAX; last >= -BOX_MAX; last--) {
+ if (from.get(last) != Integer.MAX_VALUE) break;
+ }
+ // If there is no box has known X position at all, make the focused one
+ // as known.
+ if (first > BOX_MAX) {
+ mBoxes.get(0).mAbsoluteX = mPlatform.mCurrentX;
+ first = last = 0;
+ }
+ // Now for those boxes between first and last, assign their position to
+ // align to the previous box or the next box with known position. For
+ // the boxes before first or after last, we will use a new default gap
+ // size below.
+
+ // Align to the previous box
+ for (int i = Math.max(0, first + 1); i < last; i++) {
+ if (from.get(i) != Integer.MAX_VALUE) continue;
+ Box a = mBoxes.get(i - 1);
+ Box b = mBoxes.get(i);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2
+ + getDefaultGapSize(i);
+ if (mPopFromTop) {
+ b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
+ } else {
+ b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
+ }
+ }
+
+ // Align to the next box
+ for (int i = Math.min(-1, last - 1); i > first; i--) {
+ if (from.get(i) != Integer.MAX_VALUE) continue;
+ Box a = mBoxes.get(i + 1);
+ Box b = mBoxes.get(i);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2)
+ - getDefaultGapSize(i);
+ if (mPopFromTop) {
+ b.mCurrentY = -(mViewH / 2 + heightOf(b) / 2);
+ } else {
+ b.mCurrentY = (mViewH / 2 + heightOf(b) / 2);
+ }
+ }
+
+ // 7. recycle the gaps that are not used in the new array.
+ k = -BOX_MAX;
+ for (int i = -BOX_MAX; i < BOX_MAX; i++) {
+ if (mGaps.get(i) != null) continue;
+ while (mTempGaps.get(k) == null) {
+ k++;
+ }
+ mGaps.put(i, mTempGaps.get(k++));
+ Box a = mBoxes.get(i);
+ Box b = mBoxes.get(i + 1);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ if (i >= first && i < last) {
+ int g = b.mAbsoluteX - a.mAbsoluteX - wb / 2 - (wa - wa / 2);
+ initGap(i, g);
+ } else {
+ initGap(i);
+ }
+ }
+
+ // 8. calculate the new absolute X coordinates for those box before
+ // first or after last.
+ for (int i = first - 1; i >= -BOX_MAX; i--) {
+ Box a = mBoxes.get(i + 1);
+ Box b = mBoxes.get(i);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ Gap g = mGaps.get(i);
+ b.mAbsoluteX = a.mAbsoluteX - wa / 2 - (wb - wb / 2) - g.mCurrentGap;
+ }
+
+ for (int i = last + 1; i <= BOX_MAX; i++) {
+ Box a = mBoxes.get(i - 1);
+ Box b = mBoxes.get(i);
+ int wa = widthOf(a);
+ int wb = widthOf(b);
+ Gap g = mGaps.get(i - 1);
+ b.mAbsoluteX = a.mAbsoluteX + (wa - wa / 2) + wb / 2 + g.mCurrentGap;
+ }
+
+ // 9. offset the Platform position
+ int dx = mBoxes.get(0).mAbsoluteX - mPlatform.mCurrentX;
+ mPlatform.mCurrentX += dx;
+ mPlatform.mFromX += dx;
+ mPlatform.mToX += dx;
+ mPlatform.mFlingOffset += dx;
+
+ if (mConstrained != constrained) {
+ mConstrained = constrained;
+ mPlatform.updateDefaultXY();
+ updateScaleAndGapLimit();
+ }
+
+ snapAndRedraw();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Public utilities
+ ////////////////////////////////////////////////////////////////////////////
+
+ public boolean isAtMinimalScale() {
+ Box b = mBoxes.get(0);
+ return isAlmostEqual(b.mCurrentScale, b.mScaleMin);
+ }
+
+ public boolean isCenter() {
+ Box b = mBoxes.get(0);
+ return mPlatform.mCurrentX == mPlatform.mDefaultX
+ && b.mCurrentY == 0;
+ }
+
+ public int getImageWidth() {
+ Box b = mBoxes.get(0);
+ return b.mImageW;
+ }
+
+ public int getImageHeight() {
+ Box b = mBoxes.get(0);
+ return b.mImageH;
+ }
+
+ public float getImageScale() {
+ Box b = mBoxes.get(0);
+ return b.mCurrentScale;
+ }
+
+ public int getImageAtEdges() {
+ Box b = mBoxes.get(0);
+ Platform p = mPlatform;
+ calculateStableBound(b.mCurrentScale);
+ int edges = 0;
+ if (p.mCurrentX <= mBoundLeft) {
+ edges |= IMAGE_AT_RIGHT_EDGE;
+ }
+ if (p.mCurrentX >= mBoundRight) {
+ edges |= IMAGE_AT_LEFT_EDGE;
+ }
+ if (b.mCurrentY <= mBoundTop) {
+ edges |= IMAGE_AT_BOTTOM_EDGE;
+ }
+ if (b.mCurrentY >= mBoundBottom) {
+ edges |= IMAGE_AT_TOP_EDGE;
+ }
+ return edges;
+ }
+
+ public boolean isScrolling() {
+ return mPlatform.mAnimationStartTime != NO_ANIMATION
+ && mPlatform.mCurrentX != mPlatform.mToX;
+ }
+
+ public void stopScrolling() {
+ if (mPlatform.mAnimationStartTime == NO_ANIMATION) return;
+ if (mFilmMode) mFilmScroller.forceFinished(true);
+ mPlatform.mFromX = mPlatform.mToX = mPlatform.mCurrentX;
+ }
+
+ public float getFilmRatio() {
+ return mFilmRatio.mCurrentRatio;
+ }
+
+ public void setPopFromTop(boolean top) {
+ mPopFromTop = top;
+ }
+
+ public boolean hasDeletingBox() {
+ for(int i = -BOX_MAX; i <= BOX_MAX; i++) {
+ if (mBoxes.get(i).mAnimationKind == ANIM_KIND_DELETE) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Private utilities
+ ////////////////////////////////////////////////////////////////////////////
+
+ private float getMinimalScale(Box b) {
+ float wFactor = 1.0f;
+ float hFactor = 1.0f;
+ int viewW, viewH;
+
+ if (!mFilmMode && mConstrained && !mConstrainedFrame.isEmpty()
+ && b == mBoxes.get(0)) {
+ viewW = mConstrainedFrame.width();
+ viewH = mConstrainedFrame.height();
+ } else {
+ viewW = mViewW;
+ viewH = mViewH;
+ }
+
+ if (mFilmMode) {
+ if (mViewH > mViewW) { // portrait
+ wFactor = FILM_MODE_PORTRAIT_WIDTH;
+ hFactor = FILM_MODE_PORTRAIT_HEIGHT;
+ } else { // landscape
+ wFactor = FILM_MODE_LANDSCAPE_WIDTH;
+ hFactor = FILM_MODE_LANDSCAPE_HEIGHT;
+ }
+ }
+
+ float s = Math.min(wFactor * viewW / b.mImageW,
+ hFactor * viewH / b.mImageH);
+ return Math.min(SCALE_LIMIT, s);
+ }
+
+ private float getMaximalScale(Box b) {
+ if (mFilmMode) return getMinimalScale(b);
+ if (mConstrained && !mConstrainedFrame.isEmpty()) return getMinimalScale(b);
+ return SCALE_LIMIT;
+ }
+
+ private static boolean isAlmostEqual(float a, float b) {
+ float diff = a - b;
+ return (diff < 0 ? -diff : diff) < 0.02f;
+ }
+
+ // Calculates the stable region of mPlatform.mCurrentX and
+ // mBoxes.get(0).mCurrentY, where "stable" means
+ //
+ // (1) If the dimension of scaled image >= view dimension, we will not
+ // see black region outside the image (at that dimension).
+ // (2) If the dimension of scaled image < view dimension, we will center
+ // the scaled image.
+ //
+ // We might temporarily go out of this stable during user interaction,
+ // but will "snap back" after user stops interaction.
+ //
+ // The results are stored in mBound{Left/Right/Top/Bottom}.
+ //
+ // An extra parameter "horizontalSlack" (which has the value of 0 usually)
+ // is used to extend the stable region by some pixels on each side
+ // horizontally.
+ private void calculateStableBound(float scale, int horizontalSlack) {
+ Box b = mBoxes.get(0);
+
+ // The width and height of the box in number of view pixels
+ int w = widthOf(b, scale);
+ int h = heightOf(b, scale);
+
+ // When the edge of the view is aligned with the edge of the box
+ mBoundLeft = (mViewW + 1) / 2 - (w + 1) / 2 - horizontalSlack;
+ mBoundRight = w / 2 - mViewW / 2 + horizontalSlack;
+ mBoundTop = (mViewH + 1) / 2 - (h + 1) / 2;
+ mBoundBottom = h / 2 - mViewH / 2;
+
+ // If the scaled height is smaller than the view height,
+ // force it to be in the center.
+ if (viewTallerThanScaledImage(scale)) {
+ mBoundTop = mBoundBottom = 0;
+ }
+
+ // Same for width
+ if (viewWiderThanScaledImage(scale)) {
+ mBoundLeft = mBoundRight = mPlatform.mDefaultX;
+ }
+ }
+
+ private void calculateStableBound(float scale) {
+ calculateStableBound(scale, 0);
+ }
+
+ private boolean viewTallerThanScaledImage(float scale) {
+ return mViewH >= heightOf(mBoxes.get(0), scale);
+ }
+
+ private boolean viewWiderThanScaledImage(float scale) {
+ return mViewW >= widthOf(mBoxes.get(0), scale);
+ }
+
+ private float getTargetScale(Box b) {
+ return b.mAnimationStartTime == NO_ANIMATION
+ ? b.mCurrentScale : b.mToScale;
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Animatable: an thing which can do animation.
+ ////////////////////////////////////////////////////////////////////////////
+ private abstract static class Animatable {
+ public long mAnimationStartTime;
+ public int mAnimationKind;
+ public int mAnimationDuration;
+
+ // This should be overridden in subclass to change the animation values
+ // give the progress value in [0, 1].
+ protected abstract boolean interpolate(float progress);
+ public abstract boolean startSnapback();
+
+ // Returns true if the animation values changes, so things need to be
+ // redrawn.
+ public boolean advanceAnimation() {
+ if (mAnimationStartTime == NO_ANIMATION) {
+ return false;
+ }
+ if (mAnimationStartTime == LAST_ANIMATION) {
+ mAnimationStartTime = NO_ANIMATION;
+ return startSnapback();
+ }
+
+ float progress;
+ if (mAnimationDuration == 0) {
+ progress = 1;
+ } else {
+ long now = AnimationTime.get();
+ progress =
+ (float) (now - mAnimationStartTime) / mAnimationDuration;
+ }
+
+ if (progress >= 1) {
+ progress = 1;
+ } else {
+ progress = applyInterpolationCurve(mAnimationKind, progress);
+ }
+
+ boolean done = interpolate(progress);
+
+ if (done) {
+ mAnimationStartTime = LAST_ANIMATION;
+ }
+
+ return true;
+ }
+
+ private static float applyInterpolationCurve(int kind, float progress) {
+ float f = 1 - progress;
+ switch (kind) {
+ case ANIM_KIND_SCROLL:
+ case ANIM_KIND_FLING:
+ case ANIM_KIND_FLING_X:
+ case ANIM_KIND_DELETE:
+ case ANIM_KIND_CAPTURE:
+ progress = 1 - f; // linear
+ break;
+ case ANIM_KIND_OPENING:
+ case ANIM_KIND_SCALE:
+ progress = 1 - f * f; // quadratic
+ break;
+ case ANIM_KIND_SNAPBACK:
+ case ANIM_KIND_ZOOM:
+ case ANIM_KIND_SLIDE:
+ progress = 1 - f * f * f * f * f; // x^5
+ break;
+ }
+ return progress;
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Platform: captures the global X/Y movement.
+ ////////////////////////////////////////////////////////////////////////////
+ private class Platform extends Animatable {
+ public int mCurrentX, mFromX, mToX, mDefaultX;
+ public int mCurrentY, mFromY, mToY, mDefaultY;
+ public int mFlingOffset;
+
+ @Override
+ public boolean startSnapback() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ if (mAnimationKind == ANIM_KIND_SCROLL
+ && mListener.isHoldingDown()) return false;
+ if (mInScale) return false;
+
+ Box b = mBoxes.get(0);
+ float scaleMin = mExtraScalingRange ?
+ b.mScaleMin * SCALE_MIN_EXTRA : b.mScaleMin;
+ float scaleMax = mExtraScalingRange ?
+ b.mScaleMax * SCALE_MAX_EXTRA : b.mScaleMax;
+ float scale = Utils.clamp(b.mCurrentScale, scaleMin, scaleMax);
+ int x = mCurrentX;
+ int y = mDefaultY;
+ if (mFilmMode) {
+ x = mDefaultX;
+ } else {
+ calculateStableBound(scale, HORIZONTAL_SLACK);
+ // If the picture is zoomed-in, we want to keep the focus point
+ // stay in the same position on screen, so we need to adjust
+ // target mCurrentX (which is the center of the focused
+ // box). The position of the focus point on screen (relative the
+ // the center of the view) is:
+ //
+ // mCurrentX + scale * mFocusX = mCurrentX' + scale' * mFocusX
+ // => mCurrentX' = mCurrentX + (scale - scale') * mFocusX
+ //
+ if (!viewWiderThanScaledImage(scale)) {
+ float scaleDiff = b.mCurrentScale - scale;
+ x += (int) (mFocusX * scaleDiff + 0.5f);
+ }
+ x = Utils.clamp(x, mBoundLeft, mBoundRight);
+ }
+ if (mCurrentX != x || mCurrentY != y) {
+ return doAnimation(x, y, ANIM_KIND_SNAPBACK);
+ }
+ return false;
+ }
+
+ // The updateDefaultXY() should be called whenever these variables
+ // changes: (1) mConstrained (2) mConstrainedFrame (3) mViewW/H (4)
+ // mFilmMode
+ public void updateDefaultXY() {
+ // We don't check mFilmMode and return 0 for mDefaultX. Because
+ // otherwise if we decide to leave film mode because we are
+ // centered, we will immediately back into film mode because we find
+ // we are not centered.
+ if (mConstrained && !mConstrainedFrame.isEmpty()) {
+ mDefaultX = mConstrainedFrame.centerX() - mViewW / 2;
+ mDefaultY = mFilmMode ? 0 :
+ mConstrainedFrame.centerY() - mViewH / 2;
+ } else {
+ mDefaultX = 0;
+ mDefaultY = 0;
+ }
+ }
+
+ // Starts an animation for the platform.
+ private boolean doAnimation(int targetX, int targetY, int kind) {
+ if (mCurrentX == targetX && mCurrentY == targetY) return false;
+ mAnimationKind = kind;
+ mFromX = mCurrentX;
+ mFromY = mCurrentY;
+ mToX = targetX;
+ mToY = targetY;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[kind];
+ mFlingOffset = 0;
+ advanceAnimation();
+ return true;
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (mAnimationKind == ANIM_KIND_FLING) {
+ return interpolateFlingPage(progress);
+ } else if (mAnimationKind == ANIM_KIND_FLING_X) {
+ return interpolateFlingFilm(progress);
+ } else {
+ return interpolateLinear(progress);
+ }
+ }
+
+ private boolean interpolateFlingFilm(float progress) {
+ mFilmScroller.computeScrollOffset();
+ mCurrentX = mFilmScroller.getCurrX() + mFlingOffset;
+
+ int dir = EdgeView.INVALID_DIRECTION;
+ if (mCurrentX < mDefaultX) {
+ if (!mHasNext) {
+ dir = EdgeView.RIGHT;
+ }
+ } else if (mCurrentX > mDefaultX) {
+ if (!mHasPrev) {
+ dir = EdgeView.LEFT;
+ }
+ }
+ if (dir != EdgeView.INVALID_DIRECTION) {
+ // TODO: restore this onAbsorb call
+ //int v = (int) (mFilmScroller.getCurrVelocity() + 0.5f);
+ //mListener.onAbsorb(v, dir);
+ mFilmScroller.forceFinished(true);
+ mCurrentX = mDefaultX;
+ }
+ return mFilmScroller.isFinished();
+ }
+
+ private boolean interpolateFlingPage(float progress) {
+ mPageScroller.computeScrollOffset(progress);
+ Box b = mBoxes.get(0);
+ calculateStableBound(b.mCurrentScale);
+
+ int oldX = mCurrentX;
+ mCurrentX = mPageScroller.getCurrX();
+
+ // Check if we hit the edges; show edge effects if we do.
+ if (oldX > mBoundLeft && mCurrentX == mBoundLeft) {
+ int v = (int) (-mPageScroller.getCurrVelocityX() + 0.5f);
+ mListener.onAbsorb(v, EdgeView.RIGHT);
+ } else if (oldX < mBoundRight && mCurrentX == mBoundRight) {
+ int v = (int) (mPageScroller.getCurrVelocityX() + 0.5f);
+ mListener.onAbsorb(v, EdgeView.LEFT);
+ }
+
+ return progress >= 1;
+ }
+
+ private boolean interpolateLinear(float progress) {
+ // Other animations
+ if (progress >= 1) {
+ mCurrentX = mToX;
+ mCurrentY = mToY;
+ return true;
+ } else {
+ if (mAnimationKind == ANIM_KIND_CAPTURE) {
+ progress = CaptureAnimation.calculateSlide(progress);
+ }
+ mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
+ mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
+ if (mAnimationKind == ANIM_KIND_CAPTURE) {
+ return false;
+ } else {
+ return (mCurrentX == mToX && mCurrentY == mToY);
+ }
+ }
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Box: represents a rectangular area which shows a picture.
+ ////////////////////////////////////////////////////////////////////////////
+ private class Box extends Animatable {
+ // Size of the bitmap
+ public int mImageW, mImageH;
+
+ // This is true if we assume the image size is the same as view size
+ // until we know the actual size of image. This is also used to
+ // determine if there is an image ready to show.
+ public boolean mUseViewSize;
+
+ // The minimum and maximum scale we allow for this box.
+ public float mScaleMin, mScaleMax;
+
+ // The X/Y value indicates where the center of the box is on the view
+ // coordinate. We always keep the mCurrent{X,Y,Scale} sync with the
+ // actual values used currently. Note that the X values are implicitly
+ // defined by Platform and Gaps.
+ public int mCurrentY, mFromY, mToY;
+ public float mCurrentScale, mFromScale, mToScale;
+
+ // The absolute X coordinate of the center of the box. This is only used
+ // during moveBox().
+ public int mAbsoluteX;
+
+ @Override
+ public boolean startSnapback() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ if (mAnimationKind == ANIM_KIND_SCROLL
+ && mListener.isHoldingDown()) return false;
+ if (mAnimationKind == ANIM_KIND_DELETE
+ && mListener.isHoldingDelete()) return false;
+ if (mInScale && this == mBoxes.get(0)) return false;
+
+ int y = mCurrentY;
+ float scale;
+
+ if (this == mBoxes.get(0)) {
+ float scaleMin = mExtraScalingRange ?
+ mScaleMin * SCALE_MIN_EXTRA : mScaleMin;
+ float scaleMax = mExtraScalingRange ?
+ mScaleMax * SCALE_MAX_EXTRA : mScaleMax;
+ scale = Utils.clamp(mCurrentScale, scaleMin, scaleMax);
+ if (mFilmMode) {
+ y = 0;
+ } else {
+ calculateStableBound(scale, HORIZONTAL_SLACK);
+ // If the picture is zoomed-in, we want to keep the focus
+ // point stay in the same position on screen. See the
+ // comment in Platform.startSnapback for details.
+ if (!viewTallerThanScaledImage(scale)) {
+ float scaleDiff = mCurrentScale - scale;
+ y += (int) (mFocusY * scaleDiff + 0.5f);
+ }
+ y = Utils.clamp(y, mBoundTop, mBoundBottom);
+ }
+ } else {
+ y = 0;
+ scale = mScaleMin;
+ }
+
+ if (mCurrentY != y || mCurrentScale != scale) {
+ return doAnimation(y, scale, ANIM_KIND_SNAPBACK);
+ }
+ return false;
+ }
+
+ private boolean doAnimation(int targetY, float targetScale, int kind) {
+ targetScale = clampScale(targetScale);
+
+ if (mCurrentY == targetY && mCurrentScale == targetScale
+ && kind != ANIM_KIND_CAPTURE) {
+ return false;
+ }
+
+ // Now starts an animation for the box.
+ mAnimationKind = kind;
+ mFromY = mCurrentY;
+ mFromScale = mCurrentScale;
+ mToY = targetY;
+ mToScale = targetScale;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[kind];
+ advanceAnimation();
+ return true;
+ }
+
+ // Clamps the input scale to the range that doAnimation() can reach.
+ public float clampScale(float s) {
+ return Utils.clamp(s,
+ SCALE_MIN_EXTRA * mScaleMin,
+ SCALE_MAX_EXTRA * mScaleMax);
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (mAnimationKind == ANIM_KIND_FLING) {
+ return interpolateFlingPage(progress);
+ } else {
+ return interpolateLinear(progress);
+ }
+ }
+
+ private boolean interpolateFlingPage(float progress) {
+ mPageScroller.computeScrollOffset(progress);
+ calculateStableBound(mCurrentScale);
+
+ int oldY = mCurrentY;
+ mCurrentY = mPageScroller.getCurrY();
+
+ // Check if we hit the edges; show edge effects if we do.
+ if (oldY > mBoundTop && mCurrentY == mBoundTop) {
+ int v = (int) (-mPageScroller.getCurrVelocityY() + 0.5f);
+ mListener.onAbsorb(v, EdgeView.BOTTOM);
+ } else if (oldY < mBoundBottom && mCurrentY == mBoundBottom) {
+ int v = (int) (mPageScroller.getCurrVelocityY() + 0.5f);
+ mListener.onAbsorb(v, EdgeView.TOP);
+ }
+
+ return progress >= 1;
+ }
+
+ private boolean interpolateLinear(float progress) {
+ if (progress >= 1) {
+ mCurrentY = mToY;
+ mCurrentScale = mToScale;
+ return true;
+ } else {
+ mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
+ mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
+ if (mAnimationKind == ANIM_KIND_CAPTURE) {
+ float f = CaptureAnimation.calculateScale(progress);
+ mCurrentScale *= f;
+ return false;
+ } else {
+ return (mCurrentY == mToY && mCurrentScale == mToScale);
+ }
+ }
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Gap: represents a rectangular area which is between two boxes.
+ ////////////////////////////////////////////////////////////////////////////
+ private class Gap extends Animatable {
+ // The default gap size between two boxes. The value may vary for
+ // different image size of the boxes and for different modes (page or
+ // film).
+ public int mDefaultSize;
+
+ // The gap size between the two boxes.
+ public int mCurrentGap, mFromGap, mToGap;
+
+ @Override
+ public boolean startSnapback() {
+ if (mAnimationStartTime != NO_ANIMATION) return false;
+ return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK);
+ }
+
+ // Starts an animation for a gap.
+ public boolean doAnimation(int targetSize, int kind) {
+ if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) {
+ return false;
+ }
+ mAnimationKind = kind;
+ mFromGap = mCurrentGap;
+ mToGap = targetSize;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[mAnimationKind];
+ advanceAnimation();
+ return true;
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (progress >= 1) {
+ mCurrentGap = mToGap;
+ return true;
+ } else {
+ mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
+ if (mAnimationKind == ANIM_KIND_CAPTURE) {
+ float f = CaptureAnimation.calculateScale(progress);
+ mCurrentGap = (int) (mCurrentGap * f);
+ return false;
+ } else {
+ return (mCurrentGap == mToGap);
+ }
+ }
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // FilmRatio: represents the progress of film mode change.
+ ////////////////////////////////////////////////////////////////////////////
+ private class FilmRatio extends Animatable {
+ // The film ratio: 1 means switching to film mode is complete, 0 means
+ // switching to page mode is complete.
+ public float mCurrentRatio, mFromRatio, mToRatio;
+
+ @Override
+ public boolean startSnapback() {
+ float target = mFilmMode ? 1f : 0f;
+ if (target == mToRatio) return false;
+ return doAnimation(target, ANIM_KIND_SNAPBACK);
+ }
+
+ // Starts an animation for the film ratio.
+ private boolean doAnimation(float targetRatio, int kind) {
+ mAnimationKind = kind;
+ mFromRatio = mCurrentRatio;
+ mToRatio = targetRatio;
+ mAnimationStartTime = AnimationTime.startTime();
+ mAnimationDuration = ANIM_TIME[mAnimationKind];
+ advanceAnimation();
+ return true;
+ }
+
+ @Override
+ protected boolean interpolate(float progress) {
+ if (progress >= 1) {
+ mCurrentRatio = mToRatio;
+ return true;
+ } else {
+ mCurrentRatio = mFromRatio + progress * (mToRatio - mFromRatio);
+ return (mCurrentRatio == mToRatio);
+ }
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java b/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java
new file mode 100644
index 000000000..ce672f211
--- /dev/null
+++ b/src/com/android/gallery3d/ui/PreparePageFadeoutTexture.java
@@ -0,0 +1,85 @@
+package com.android.gallery3d.ui;
+
+import android.os.ConditionVariable;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.RawTexture;
+import com.android.gallery3d.ui.GLRoot.OnGLIdleListener;
+
+public class PreparePageFadeoutTexture implements OnGLIdleListener {
+ private static final long TIMEOUT = 200;
+ public static final String KEY_FADE_TEXTURE = "fade_texture";
+
+ private RawTexture mTexture;
+ private ConditionVariable mResultReady = new ConditionVariable(false);
+ private boolean mCancelled = false;
+ private GLView mRootPane;
+
+ public PreparePageFadeoutTexture(GLView rootPane) {
+ if (rootPane == null) {
+ mCancelled = true;
+ return;
+ }
+ int w = rootPane.getWidth();
+ int h = rootPane.getHeight();
+ if (w == 0 || h == 0) {
+ mCancelled = true;
+ return;
+ }
+ mTexture = new RawTexture(w, h, true);
+ mRootPane = rootPane;
+ }
+
+ public boolean isCancelled() {
+ return mCancelled;
+ }
+
+ public synchronized RawTexture get() {
+ if (mCancelled) {
+ return null;
+ } else if (mResultReady.block(TIMEOUT)) {
+ return mTexture;
+ } else {
+ mCancelled = true;
+ return null;
+ }
+ }
+
+ @Override
+ public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+ if (!mCancelled) {
+ try {
+ canvas.beginRenderTarget(mTexture);
+ mRootPane.render(canvas);
+ canvas.endRenderTarget();
+ } catch (RuntimeException e) {
+ mTexture = null;
+ }
+ } else {
+ mTexture = null;
+ }
+ mResultReady.open();
+ return false;
+ }
+
+ public static void prepareFadeOutTexture(AbstractGalleryActivity activity,
+ GLView rootPane) {
+ PreparePageFadeoutTexture task = new PreparePageFadeoutTexture(rootPane);
+ if (task.isCancelled()) return;
+ GLRoot root = activity.getGLRoot();
+ RawTexture texture = null;
+ root.unlockRenderThread();
+ try {
+ root.addOnGLIdleListener(task);
+ texture = task.get();
+ } finally {
+ root.lockRenderThread();
+ }
+
+ if (texture == null) {
+ return;
+ }
+ activity.getTransitionStore().put(KEY_FADE_TEXTURE, texture);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ProgressSpinner.java b/src/com/android/gallery3d/ui/ProgressSpinner.java
new file mode 100644
index 000000000..1b31af278
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ProgressSpinner.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.ui;
+
+import android.content.Context;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+
+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 = AnimationTime.get();
+ 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);
+ 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/RelativePosition.java b/src/com/android/gallery3d/ui/RelativePosition.java
new file mode 100644
index 000000000..0f2bfd812
--- /dev/null
+++ b/src/com/android/gallery3d/ui/RelativePosition.java
@@ -0,0 +1,42 @@
+/*
+ * 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 RelativePosition {
+ private float mAbsoluteX;
+ private float mAbsoluteY;
+ private float mReferenceX;
+ private float mReferenceY;
+
+ public void setAbsolutePosition(int absoluteX, int absoluteY) {
+ mAbsoluteX = absoluteX;
+ mAbsoluteY = absoluteY;
+ }
+
+ public void setReferencePosition(int x, int y) {
+ mReferenceX = x;
+ mReferenceY = y;
+ }
+
+ public float getX() {
+ return mAbsoluteX - mReferenceX;
+ }
+
+ public float getY() {
+ return mAbsoluteY - mReferenceY;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ScreenNail.java b/src/com/android/gallery3d/ui/ScreenNail.java
new file mode 100644
index 000000000..965bf0b54
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScreenNail.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2012 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 com.android.gallery3d.glrenderer.GLCanvas;
+
+public interface ScreenNail {
+ public int getWidth();
+ public int getHeight();
+ public void draw(GLCanvas canvas, int x, int y, int width, int height);
+
+ // We do not need to draw this ScreenNail in this frame.
+ public void noDraw();
+
+ // This ScreenNail will not be used anymore. Release related resources.
+ public void recycle();
+
+ // This is only used by TileImageView to back up the tiles not yet loaded.
+ public void draw(GLCanvas canvas, RectF source, RectF dest);
+}
diff --git a/src/com/android/gallery3d/ui/ScrollBarView.java b/src/com/android/gallery3d/ui/ScrollBarView.java
new file mode 100644
index 000000000..34fbcef7a
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScrollBarView.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+import android.util.TypedValue;
+
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.NinePatchTexture;
+
+public class ScrollBarView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ScrollBarView";
+
+ 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 NinePatchTexture mScrollBarTexture;
+
+ public ScrollBarView(Context context, int gripHeight, int gripWidth) {
+ TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(
+ android.R.attr.scrollbarThumbHorizontal, outValue, true);
+ mScrollBarTexture = new NinePatchTexture(
+ context, outValue.resourceId);
+ mGripPosition = 0;
+ mGripWidth = 0;
+ mGivenGripWidth = gripWidth;
+ mGripHeight = gripHeight;
+ }
+
+ @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);
+ }
+
+ @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);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/ScrollerHelper.java b/src/com/android/gallery3d/ui/ScrollerHelper.java
new file mode 100644
index 000000000..aa68d19d9
--- /dev/null
+++ b/src/com/android/gallery3d/ui/ScrollerHelper.java
@@ -0,0 +1,97 @@
+/*
+ * 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.view.ViewConfiguration;
+
+import com.android.gallery3d.common.OverScroller;
+import com.android.gallery3d.common.Utils;
+
+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 float getCurrVelocity() {
+ return mScroller.getCurrVelocity();
+ }
+
+ 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);
+ }
+
+ // Returns the distance that over the scroll limit.
+ public int startScroll(int distance, int min, int max) {
+ int currPosition = mScroller.getCurrX();
+ int finalPosition = mScroller.isFinished() ? currPosition :
+ 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 finalPosition + distance - newPosition;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SelectionManager.java b/src/com/android/gallery3d/ui/SelectionManager.java
new file mode 100644
index 000000000..be6811bc1
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SelectionManager.java
@@ -0,0 +1,251 @@
+/*
+ * 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.AbstractGalleryActivity;
+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 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<Path> mClickedSet;
+ private MediaSet mSourceMediaSet;
+ 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(AbstractGalleryActivity activity, boolean isAlbumSet) {
+ mDataManager = activity.getDataManager();
+ mClickedSet = new HashSet<Path>();
+ 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() {
+ mInverseSelection = true;
+ mClickedSet.clear();
+ enterSelectionMode();
+ 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;
+ 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);
+ }
+
+ private int getTotalCount() {
+ if (mSourceMediaSet == null) return -1;
+
+ if (mTotal < 0) {
+ mTotal = mIsAlbumSet
+ ? mSourceMediaSet.getSubMediaSetCount()
+ : mSourceMediaSet.getMediaItemCount();
+ }
+ return mTotal;
+ }
+
+ public int getSelectedCount() {
+ int count = mClickedSet.size();
+ if (mInverseSelection) {
+ count = getTotalCount() - count;
+ }
+ return count;
+ }
+
+ public void toggle(Path path) {
+ if (mClickedSet.contains(path)) {
+ mClickedSet.remove(path);
+ } else {
+ enterSelectionMode();
+ mClickedSet.add(path);
+ }
+
+ // Convert to inverse selection mode if everything is selected.
+ int count = getSelectedCount();
+ if (count == getTotalCount()) {
+ selectAll();
+ }
+
+ if (mListener != null) mListener.onSelectionChange(path, isItemSelected(path));
+ if (count == 0 && mAutoLeave) {
+ leaveSelectionMode();
+ }
+ }
+
+ private static boolean expandMediaSet(ArrayList<Path> items, MediaSet set, int maxSelection) {
+ int subCount = set.getSubMediaSetCount();
+ for (int i = 0; i < subCount; i++) {
+ if (!expandMediaSet(items, set.getSubMediaSet(i), maxSelection)) {
+ return false;
+ }
+ }
+ int total = set.getMediaItemCount();
+ int batch = 50;
+ int index = 0;
+
+ while (index < total) {
+ int count = index + batch < total
+ ? batch
+ : total - index;
+ ArrayList<MediaItem> list = set.getMediaItem(index, count);
+ if (list != null
+ && list.size() > (maxSelection - items.size())) {
+ return false;
+ }
+ for (MediaItem item : list) {
+ items.add(item.getPath());
+ }
+ index += batch;
+ }
+ return true;
+ }
+
+ public ArrayList<Path> getSelected(boolean expandSet) {
+ return getSelected(expandSet, Integer.MAX_VALUE);
+ }
+
+ public ArrayList<Path> getSelected(boolean expandSet, int maxSelection) {
+ ArrayList<Path> selected = new ArrayList<Path>();
+ if (mIsAlbumSet) {
+ if (mInverseSelection) {
+ int total = getTotalCount();
+ for (int i = 0; i < total; i++) {
+ MediaSet set = mSourceMediaSet.getSubMediaSet(i);
+ Path id = set.getPath();
+ if (!mClickedSet.contains(id)) {
+ if (expandSet) {
+ if (!expandMediaSet(selected, set, maxSelection)) {
+ return null;
+ }
+ } else {
+ selected.add(id);
+ if (selected.size() > maxSelection) {
+ return null;
+ }
+ }
+ }
+ }
+ } else {
+ for (Path id : mClickedSet) {
+ if (expandSet) {
+ if (!expandMediaSet(selected, mDataManager.getMediaSet(id),
+ maxSelection)) {
+ return null;
+ }
+ } else {
+ selected.add(id);
+ if (selected.size() > maxSelection) {
+ return null;
+ }
+ }
+ }
+ }
+ } else {
+ if (mInverseSelection) {
+ int total = getTotalCount();
+ int index = 0;
+ while (index < total) {
+ int count = Math.min(total - index, MediaSet.MEDIAITEM_BATCH_FETCH_COUNT);
+ ArrayList<MediaItem> list = mSourceMediaSet.getMediaItem(index, count);
+ for (MediaItem item : list) {
+ Path id = item.getPath();
+ if (!mClickedSet.contains(id)) {
+ selected.add(id);
+ if (selected.size() > maxSelection) {
+ return null;
+ }
+ }
+ }
+ index += count;
+ }
+ } else {
+ for (Path id : mClickedSet) {
+ selected.add(id);
+ if (selected.size() > maxSelection) {
+ return null;
+ }
+ }
+ }
+ }
+ return selected;
+ }
+
+ public void setSourceMediaSet(MediaSet set) {
+ mSourceMediaSet = set;
+ mTotal = -1;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SelectionMenu.java b/src/com/android/gallery3d/ui/SelectionMenu.java
new file mode 100644
index 000000000..5b0828328
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SelectionMenu.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.content.Context;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.ui.PopupList.OnPopupItemClickListener;
+
+public class SelectionMenu implements OnClickListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SelectionMenu";
+
+ private final Context mContext;
+ private final Button mButton;
+ private final PopupList mPopupList;
+
+ public SelectionMenu(Context context, Button button, OnPopupItemClickListener listener) {
+ mContext = context;
+ mButton = button;
+ mPopupList = new PopupList(context, mButton);
+ mPopupList.addItem(R.id.action_select_all,
+ context.getString(R.string.select_all));
+ mPopupList.setOnPopupItemClickListener(listener);
+ mButton.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ mPopupList.show();
+ }
+
+ public void updateSelectAllMode(boolean inSelectAllMode) {
+ PopupList.Item item = mPopupList.findItem(R.id.action_select_all);
+ if (item != null) {
+ item.setTitle(mContext.getString(
+ inSelectAllMode ? R.string.deselect_all : R.string.select_all));
+ }
+ }
+
+ public void setTitle(CharSequence title) {
+ mButton.setText(title);
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SlideshowView.java b/src/com/android/gallery3d/ui/SlideshowView.java
new file mode 100644
index 000000000..43784232d
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SlideshowView.java
@@ -0,0 +1,163 @@
+/*
+ * 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.PointF;
+
+import com.android.gallery3d.anim.CanvasAnimation;
+import com.android.gallery3d.anim.FloatAnimation;
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+import java.util.Random;
+
+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 animTime = AnimationTime.get();
+ boolean requestRender = mTransitionAnimation.calculate(animTime);
+ float alpha = mPrevTexture == null ? 1f : mTransitionAnimation.get();
+
+ if (mPrevTexture != null && alpha != 1f) {
+ requestRender |= mPrevAnimation.calculate(animTime);
+ 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(animTime);
+ 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();
+ }
+
+ 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((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);
+ 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..bd0ffdc15
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SlotView.java
@@ -0,0 +1,788 @@
+/*
+ * 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.os.Handler;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.animation.DecelerateInterpolator;
+
+import com.android.gallery3d.anim.Animation;
+import com.android.gallery3d.app.AbstractGalleryActivity;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+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 static final int RENDER_MORE_PASS = 1;
+ public static final int RENDER_MORE_FRAME = 2;
+
+ public interface Listener {
+ public void onDown(int index);
+ public void onUp(boolean followedByLongPress);
+ public void onSingleTapUp(int index);
+ public void onLongTap(int index);
+ public void onScrollPositionChanged(int position, int total);
+ }
+
+ public static class SimpleListener implements Listener {
+ @Override public void onDown(int index) {}
+ @Override public void onUp(boolean followedByLongPress) {}
+ @Override public void onSingleTapUp(int index) {}
+ @Override public void onLongTap(int index) {}
+ @Override public void onScrollPositionChanged(int position, int total) {}
+ }
+
+ public static interface SlotRenderer {
+ public void prepareDrawing();
+ public void onVisibleRangeChanged(int visibleStart, int visibleEnd);
+ public void onSlotSizeChanged(int width, int height);
+ public int renderSlot(GLCanvas canvas, int index, int pass, int width, int height);
+ }
+
+ private final GestureDetector mGestureDetector;
+ private final ScrollerHelper mScroller;
+ private final Paper mPaper = new Paper();
+
+ private Listener mListener;
+ private UserInteractionListener mUIListener;
+
+ private boolean mMoreAnimation = false;
+ private SlotAnimation mAnimation = null;
+ private final Layout mLayout = new Layout();
+ private int mStartIndex = INDEX_NONE;
+
+ // whether the down action happened while the view is scrolling.
+ private boolean mDownInScrolling;
+ private int mOverscrollEffect = OVERSCROLL_3D;
+ private final Handler mHandler;
+
+ private SlotRenderer mRenderer;
+
+ private int[] mRequestRenderSlots = new int[16];
+
+ public static final int OVERSCROLL_3D = 0;
+ public static final int OVERSCROLL_SYSTEM = 1;
+ public static final int OVERSCROLL_NONE = 2;
+
+ // to prevent allocating memory
+ private final Rect mTempRect = new Rect();
+
+ public SlotView(AbstractGalleryActivity activity, Spec spec) {
+ mGestureDetector = new GestureDetector(activity, new MyGestureListener());
+ mScroller = new ScrollerHelper(activity);
+ mHandler = new SynchronizedHandler(activity.getGLRoot());
+ setSlotSpec(spec);
+ }
+
+ public void setSlotRenderer(SlotRenderer slotDrawer) {
+ mRenderer = slotDrawer;
+ if (mRenderer != null) {
+ mRenderer.onSlotSizeChanged(mLayout.mSlotWidth, mLayout.mSlotHeight);
+ mRenderer.onVisibleRangeChanged(getVisibleStart(), getVisibleEnd());
+ }
+ }
+
+ public void setCenterIndex(int index) {
+ int slotCount = mLayout.mSlotCount;
+ if (index < 0 || index >= slotCount) {
+ return;
+ }
+ Rect rect = mLayout.getSlotRect(index, mTempRect);
+ 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, mTempRect);
+ 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 setSlotSpec(Spec spec) {
+ mLayout.setSlotSpec(spec);
+ }
+
+ @Override
+ public void addComponent(GLView view) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
+ if (!changeSize) return;
+
+ // Make sure we are still at a resonable scroll position after the size
+ // is changed (like orientation change). We choose to keep the center
+ // visible slot still visible. This is arbitrary but reasonable.
+ int visibleIndex =
+ (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2;
+ mLayout.setSize(r - l, b - t);
+ makeSlotVisible(visibleIndex);
+ if (mOverscrollEffect == OVERSCROLL_3D) {
+ mPaper.setSize(r - l, b - t);
+ }
+ }
+
+ public void startScatteringAnimation(RelativePosition position) {
+ mAnimation = new ScatteringAnimation(position);
+ mAnimation.start();
+ if (mLayout.mSlotCount != 0) invalidate();
+ }
+
+ public void startRisingAnimation() {
+ mAnimation = new RisingAnimation();
+ mAnimation.start();
+ if (mLayout.mSlotCount != 0) invalidate();
+ }
+
+ 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 Rect getSlotRect(int slotIndex) {
+ return mLayout.getSlotRect(slotIndex, new Rect());
+ }
+
+ @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;
+ case MotionEvent.ACTION_UP:
+ mPaper.onRelease();
+ invalidate();
+ 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);
+ }
+
+ private static int[] expandIntArray(int array[], int capacity) {
+ while (array.length < capacity) {
+ array = new int[array.length * 2];
+ }
+ return array;
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ super.render(canvas);
+
+ if (mRenderer == null) return;
+ mRenderer.prepareDrawing();
+
+ long animTime = AnimationTime.get();
+ boolean more = mScroller.advanceAnimation(animTime);
+ more |= mLayout.advanceAnimation(animTime);
+ int oldX = mScrollX;
+ updateScrollPosition(mScroller.getPosition(), false);
+
+ boolean paperActive = false;
+ if (mOverscrollEffect == OVERSCROLL_3D) {
+ // Check if an edge is reached and notify mPaper if so.
+ int newX = mScrollX;
+ int limit = mLayout.getScrollLimit();
+ if (oldX > 0 && newX == 0 || oldX < limit && newX == limit) {
+ float v = mScroller.getCurrVelocity();
+ if (newX == limit) v = -v;
+
+ // I don't know why, but getCurrVelocity() can return NaN.
+ if (!Float.isNaN(v)) {
+ mPaper.edgeReached(v);
+ }
+ }
+ paperActive = mPaper.advanceAnimation();
+ }
+
+ more |= paperActive;
+
+ if (mAnimation != null) {
+ more |= mAnimation.calculate(animTime);
+ }
+
+ canvas.translate(-mScrollX, -mScrollY);
+
+ int requestCount = 0;
+ int requestedSlot[] = expandIntArray(mRequestRenderSlots,
+ mLayout.mVisibleEnd - mLayout.mVisibleStart);
+
+ for (int i = mLayout.mVisibleEnd - 1; i >= mLayout.mVisibleStart; --i) {
+ int r = renderItem(canvas, i, 0, paperActive);
+ if ((r & RENDER_MORE_FRAME) != 0) more = true;
+ if ((r & RENDER_MORE_PASS) != 0) requestedSlot[requestCount++] = i;
+ }
+
+ for (int pass = 1; requestCount != 0; ++pass) {
+ int newCount = 0;
+ for (int i = 0; i < requestCount; ++i) {
+ int r = renderItem(canvas,
+ requestedSlot[i], pass, paperActive);
+ if ((r & RENDER_MORE_FRAME) != 0) more = true;
+ if ((r & RENDER_MORE_PASS) != 0) requestedSlot[newCount++] = i;
+ }
+ requestCount = newCount;
+ }
+
+ canvas.translate(mScrollX, mScrollY);
+
+ if (more) invalidate();
+
+ final UserInteractionListener listener = mUIListener;
+ if (mMoreAnimation && !more && listener != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ listener.onUserInteractionEnd();
+ }
+ });
+ }
+ mMoreAnimation = more;
+ }
+
+ private int renderItem(
+ GLCanvas canvas, int index, int pass, boolean paperActive) {
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA | GLCanvas.SAVE_FLAG_MATRIX);
+ Rect rect = mLayout.getSlotRect(index, mTempRect);
+ if (paperActive) {
+ canvas.multiplyMatrix(mPaper.getTransform(rect, mScrollX), 0);
+ } else {
+ canvas.translate(rect.left, rect.top, 0);
+ }
+ if (mAnimation != null && mAnimation.isActive()) {
+ mAnimation.apply(canvas, index, rect);
+ }
+ int result = mRenderer.renderSlot(
+ canvas, index, pass, rect.right - rect.left, rect.bottom - rect.top);
+ canvas.restore();
+ return result;
+ }
+
+ public static abstract class SlotAnimation extends Animation {
+ protected float mProgress = 0;
+
+ public SlotAnimation() {
+ setInterpolator(new DecelerateInterpolator(4));
+ setDuration(1500);
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mProgress = progress;
+ }
+
+ abstract public void apply(GLCanvas canvas, int slotIndex, Rect target);
+ }
+
+ public static class RisingAnimation extends SlotAnimation {
+ private static final int RISING_DISTANCE = 128;
+
+ @Override
+ public void apply(GLCanvas canvas, int slotIndex, Rect target) {
+ canvas.translate(0, 0, RISING_DISTANCE * (1 - mProgress));
+ }
+ }
+
+ public static class ScatteringAnimation extends SlotAnimation {
+ private int PHOTO_DISTANCE = 1000;
+ private RelativePosition mCenter;
+
+ public ScatteringAnimation(RelativePosition center) {
+ mCenter = center;
+ }
+
+ @Override
+ public void apply(GLCanvas canvas, int slotIndex, Rect target) {
+ canvas.translate(
+ (mCenter.getX() - target.centerX()) * (1 - mProgress),
+ (mCenter.getY() - target.centerY()) * (1 - mProgress),
+ slotIndex * PHOTO_DISTANCE * (1 - mProgress));
+ canvas.setAlpha(mProgress);
+ }
+ }
+
+ // This Spec class is used to specify the size of each slot in the SlotView.
+ // There are two ways to do it:
+ //
+ // (1) Specify slotWidth and slotHeight: they specify the width and height
+ // of each slot. The number of rows and the gap between slots will be
+ // determined automatically.
+ // (2) Specify rowsLand, rowsPort, and slotGap: they specify the number
+ // of rows in landscape/portrait mode and the gap between slots. The
+ // width and height of each slot is determined automatically.
+ //
+ // The initial value of -1 means they are not specified.
+ public static class Spec {
+ public int slotWidth = -1;
+ public int slotHeight = -1;
+ public int slotHeightAdditional = 0;
+
+ public int rowsLand = -1;
+ public int rowsPort = -1;
+ public int slotGap = -1;
+ }
+
+ public class Layout {
+
+ private int mVisibleStart;
+ private int mVisibleEnd;
+
+ private int mSlotCount;
+ private int mSlotWidth;
+ private int mSlotHeight;
+ private int mSlotGap;
+
+ private Spec mSpec;
+
+ private int mWidth;
+ private int mHeight;
+
+ private int mUnitCount;
+ private int mContentLength;
+ private int mScrollPosition;
+
+ private IntegerAnimation mVerticalPadding = new IntegerAnimation();
+ private IntegerAnimation mHorizontalPadding = new IntegerAnimation();
+
+ public void setSlotSpec(Spec spec) {
+ mSpec = spec;
+ }
+
+ public boolean setSlotCount(int slotCount) {
+ if (slotCount == mSlotCount) return false;
+ if (mSlotCount != 0) {
+ mHorizontalPadding.setEnabled(true);
+ mVerticalPadding.setEnabled(true);
+ }
+ mSlotCount = slotCount;
+ int hPadding = mHorizontalPadding.getTarget();
+ int vPadding = mVerticalPadding.getTarget();
+ initLayoutParameters();
+ return vPadding != mVerticalPadding.getTarget()
+ || hPadding != mHorizontalPadding.getTarget();
+ }
+
+ public Rect getSlotRect(int index, Rect rect) {
+ int col, row;
+ if (WIDE) {
+ col = index / mUnitCount;
+ row = index - col * mUnitCount;
+ } else {
+ row = index / mUnitCount;
+ col = index - row * mUnitCount;
+ }
+
+ int x = mHorizontalPadding.get() + col * (mSlotWidth + mSlotGap);
+ int y = mVerticalPadding.get() + row * (mSlotHeight + mSlotGap);
+ rect.set(x, y, x + mSlotWidth, y + mSlotHeight);
+ return rect;
+ }
+
+ public int getSlotWidth() {
+ return mSlotWidth;
+ }
+
+ public int getSlotHeight() {
+ return mSlotHeight;
+ }
+
+ // 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 + mSlotGap) / (minorUnitSize + mSlotGap);
+ if (unitCount == 0) unitCount = 1;
+ mUnitCount = unitCount;
+
+ // We put extra padding above and below the column.
+ int availableUnits = Math.min(mUnitCount, mSlotCount);
+ int usedMinorLength = availableUnits * minorUnitSize +
+ (availableUnits - 1) * mSlotGap;
+ padding[0] = (minorLength - usedMinorLength) / 2;
+
+ // Then calculate how many columns we need for all slots.
+ int count = ((mSlotCount + mUnitCount - 1) / mUnitCount);
+ mContentLength = count * majorUnitSize + (count - 1) * mSlotGap;
+
+ // 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() {
+ // Initialize mSlotWidth and mSlotHeight from mSpec
+ if (mSpec.slotWidth != -1) {
+ mSlotGap = 0;
+ mSlotWidth = mSpec.slotWidth;
+ mSlotHeight = mSpec.slotHeight;
+ } else {
+ int rows = (mWidth > mHeight) ? mSpec.rowsLand : mSpec.rowsPort;
+ mSlotGap = mSpec.slotGap;
+ mSlotHeight = Math.max(1, (mHeight - (rows - 1) * mSlotGap) / rows);
+ mSlotWidth = mSlotHeight - mSpec.slotHeightAdditional;
+ }
+
+ if (mRenderer != null) {
+ mRenderer.onSlotSizeChanged(mSlotWidth, mSlotHeight);
+ }
+
+ int[] padding = new int[2];
+ if (WIDE) {
+ initLayoutParameters(mWidth, mHeight, mSlotWidth, mSlotHeight, padding);
+ mVerticalPadding.startAnimateTo(padding[0]);
+ mHorizontalPadding.startAnimateTo(padding[1]);
+ } else {
+ initLayoutParameters(mHeight, mWidth, mSlotHeight, mSlotWidth, padding);
+ mVerticalPadding.startAnimateTo(padding[1]);
+ mHorizontalPadding.startAnimateTo(padding[0]);
+ }
+ updateVisibleSlotRange();
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ initLayoutParameters();
+ }
+
+ private void updateVisibleSlotRange() {
+ int position = mScrollPosition;
+
+ if (WIDE) {
+ int startCol = position / (mSlotWidth + mSlotGap);
+ int start = Math.max(0, mUnitCount * startCol);
+ int endCol = (position + mWidth + mSlotWidth + mSlotGap - 1) /
+ (mSlotWidth + mSlotGap);
+ int end = Math.min(mSlotCount, mUnitCount * endCol);
+ setVisibleRange(start, end);
+ } else {
+ int startRow = position / (mSlotHeight + mSlotGap);
+ int start = Math.max(0, mUnitCount * startRow);
+ int endRow = (position + mHeight + mSlotHeight + mSlotGap - 1) /
+ (mSlotHeight + mSlotGap);
+ int end = Math.min(mSlotCount, mUnitCount * endRow);
+ 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;
+ }
+ if (mRenderer != null) {
+ mRenderer.onVisibleRangeChanged(mVisibleStart, mVisibleEnd);
+ }
+ }
+
+ public int getVisibleStart() {
+ return mVisibleStart;
+ }
+
+ public int getVisibleEnd() {
+ return mVisibleEnd;
+ }
+
+ public int getSlotIndexByPosition(float x, float y) {
+ int absoluteX = Math.round(x) + (WIDE ? mScrollPosition : 0);
+ int absoluteY = Math.round(y) + (WIDE ? 0 : mScrollPosition);
+
+ absoluteX -= mHorizontalPadding.get();
+ absoluteY -= mVerticalPadding.get();
+
+ if (absoluteX < 0 || absoluteY < 0) {
+ return INDEX_NONE;
+ }
+
+ int columnIdx = absoluteX / (mSlotWidth + mSlotGap);
+ int rowIdx = absoluteY / (mSlotHeight + mSlotGap);
+
+ if (!WIDE && columnIdx >= mUnitCount) {
+ return INDEX_NONE;
+ }
+
+ if (WIDE && rowIdx >= mUnitCount) {
+ return INDEX_NONE;
+ }
+
+ if (absoluteX % (mSlotWidth + mSlotGap) >= mSlotWidth) {
+ return INDEX_NONE;
+ }
+
+ if (absoluteY % (mSlotHeight + mSlotGap) >= mSlotHeight) {
+ 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;
+ }
+
+ public boolean advanceAnimation(long animTime) {
+ // use '|' to make sure both sides will be executed
+ return mVerticalPadding.calculate(animTime) | mHorizontalPadding.calculate(animTime);
+ }
+ }
+
+ private class MyGestureListener implements GestureDetector.OnGestureListener {
+ private boolean isDown;
+
+ // We call the listener's onDown() when our onShowPress() is called and
+ // call the listener's onUp() when we receive any further event.
+ @Override
+ public void onShowPress(MotionEvent e) {
+ GLRoot root = getGLRoot();
+ root.lockRenderThread();
+ try {
+ if (isDown) return;
+ int index = mLayout.getSlotIndexByPosition(e.getX(), e.getY());
+ if (index != INDEX_NONE) {
+ isDown = true;
+ mListener.onDown(index);
+ }
+ } finally {
+ root.unlockRenderThread();
+ }
+ }
+
+ private void cancelDown(boolean byLongPress) {
+ if (!isDown) return;
+ isDown = false;
+ mListener.onUp(byLongPress);
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1,
+ MotionEvent e2, float velocityX, float velocityY) {
+ cancelDown(false);
+ 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) {
+ cancelDown(false);
+ float distance = WIDE ? distanceX : distanceY;
+ int overDistance = mScroller.startScroll(
+ Math.round(distance), 0, mLayout.getScrollLimit());
+ if (mOverscrollEffect == OVERSCROLL_3D && overDistance != 0) {
+ mPaper.overScroll(overDistance);
+ }
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ cancelDown(false);
+ 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) {
+ cancelDown(true);
+ 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;
+ }
+ // Reset the scroll position to avoid scrolling over the updated limit.
+ setScrollPosition(WIDE ? mScrollX : mScrollY);
+ return changed;
+ }
+
+ public int getVisibleStart() {
+ return mLayout.getVisibleStart();
+ }
+
+ public int getVisibleEnd() {
+ return mLayout.getVisibleEnd();
+ }
+
+ public int getScrollX() {
+ return mScrollX;
+ }
+
+ public int getScrollY() {
+ return mScrollY;
+ }
+
+ public Rect getSlotRect(int slotIndex, GLView rootPane) {
+ // Get slot rectangle relative to this root pane.
+ Rect offset = new Rect();
+ rootPane.getBoundsOf(this, offset);
+ Rect r = getSlotRect(slotIndex);
+ r.offset(offset.left - getScrollX(),
+ offset.top - getScrollY());
+ return r;
+ }
+
+ private static class IntegerAnimation extends Animation {
+ private int mTarget;
+ private int mCurrent = 0;
+ private int mFrom = 0;
+ private boolean mEnabled = false;
+
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ public void startAnimateTo(int target) {
+ if (!mEnabled) {
+ mTarget = mCurrent = target;
+ return;
+ }
+ if (target == mTarget) return;
+
+ mFrom = mCurrent;
+ mTarget = target;
+ setDuration(180);
+ start();
+ }
+
+ public int get() {
+ return mCurrent;
+ }
+
+ public int getTarget() {
+ return mTarget;
+ }
+
+ @Override
+ protected void onCalculate(float progress) {
+ mCurrent = Math.round(mFrom + progress * (mTarget - mFrom));
+ if (progress == 1f) mEnabled = false;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
new file mode 100644
index 000000000..18121e63b
--- /dev/null
+++ b/src/com/android/gallery3d/ui/SurfaceTextureScreenNail.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2012 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.annotation.TargetApi;
+import android.graphics.RectF;
+import android.graphics.SurfaceTexture;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.glrenderer.ExtTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+
+@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+public abstract class SurfaceTextureScreenNail implements ScreenNail,
+ SurfaceTexture.OnFrameAvailableListener {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SurfaceTextureScreenNail";
+ // This constant is not available in API level before 15, but it was just an
+ // oversight.
+ private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;
+
+ protected ExtTexture mExtTexture;
+ private SurfaceTexture mSurfaceTexture;
+ private int mWidth, mHeight;
+ private float[] mTransform = new float[16];
+ private boolean mHasTexture = false;
+
+ public SurfaceTextureScreenNail() {
+ }
+
+ public void acquireSurfaceTexture(GLCanvas canvas) {
+ mExtTexture = new ExtTexture(canvas, GL_TEXTURE_EXTERNAL_OES);
+ mExtTexture.setSize(mWidth, mHeight);
+ mSurfaceTexture = new SurfaceTexture(mExtTexture.getId());
+ setDefaultBufferSize(mSurfaceTexture, mWidth, mHeight);
+ mSurfaceTexture.setOnFrameAvailableListener(this);
+ synchronized (this) {
+ mHasTexture = true;
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
+ private static void setDefaultBufferSize(SurfaceTexture st, int width, int height) {
+ if (ApiHelper.HAS_SET_DEFALT_BUFFER_SIZE) {
+ st.setDefaultBufferSize(width, height);
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH)
+ private static void releaseSurfaceTexture(SurfaceTexture st) {
+ st.setOnFrameAvailableListener(null);
+ if (ApiHelper.HAS_RELEASE_SURFACE_TEXTURE) {
+ st.release();
+ }
+ }
+
+ public SurfaceTexture getSurfaceTexture() {
+ return mSurfaceTexture;
+ }
+
+ public void releaseSurfaceTexture() {
+ synchronized (this) {
+ mHasTexture = false;
+ }
+ mExtTexture.recycle();
+ mExtTexture = null;
+ releaseSurfaceTexture(mSurfaceTexture);
+ mSurfaceTexture = null;
+ }
+
+ public void setSize(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ public void resizeTexture() {
+ if (mExtTexture != null) {
+ mExtTexture.setSize(mWidth, mHeight);
+ setDefaultBufferSize(mSurfaceTexture, mWidth, mHeight);
+ }
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+ synchronized (this) {
+ if (!mHasTexture) return;
+ mSurfaceTexture.updateTexImage();
+ mSurfaceTexture.getTransformMatrix(mTransform);
+
+ // Flip vertically.
+ canvas.save(GLCanvas.SAVE_FLAG_MATRIX);
+ int cx = x + width / 2;
+ int cy = y + height / 2;
+ canvas.translate(cx, cy);
+ canvas.scale(1, -1, 1);
+ canvas.translate(-cx, -cy);
+ updateTransformMatrix(mTransform);
+ canvas.drawTexture(mExtTexture, mTransform, x, y, width, height);
+ canvas.restore();
+ }
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, RectF source, RectF dest) {
+ throw new UnsupportedOperationException();
+ }
+
+ protected void updateTransformMatrix(float[] matrix) {}
+
+ @Override
+ abstract public void noDraw();
+
+ @Override
+ abstract public void recycle();
+
+ @Override
+ abstract public void onFrameAvailable(SurfaceTexture surfaceTexture);
+}
diff --git a/src/com/android/gallery3d/ui/SynchronizedHandler.java b/src/com/android/gallery3d/ui/SynchronizedHandler.java
new file mode 100644
index 000000000..ba1035747
--- /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 android.os.Handler;
+import android.os.Message;
+
+import com.android.gallery3d.common.Utils;
+
+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/TileImageView.java b/src/com/android/gallery3d/ui/TileImageView.java
new file mode 100644
index 000000000..3185c7598
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TileImageView.java
@@ -0,0 +1,786 @@
+/*
+ * 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.Bitmap;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.support.v4.util.LongSparseArray;
+import android.util.DisplayMetrics;
+import android.util.FloatMath;
+import android.view.WindowManager;
+
+import com.android.gallery3d.app.GalleryContext;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DecodeUtils;
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+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 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";
+ private static final int UPLOAD_LIMIT = 1;
+
+ // TILE_SIZE must be 2^N
+ private static int sTileSize;
+
+ /*
+ * 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()
+ * --> DECODE_FAIL - by decodeTile()
+ * RECYCLING --> RECYCLED - by decodeTile()
+ * DECODED --> ACTIVATED - (after the decoded bitmap is uploaded)
+ * DECODED --> RECYCLED - by recycleTile()
+ * DECODE_FAIL -> 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_DECODE_FAIL = 0x10;
+ private static final int STATE_RECYCLING = 0x20;
+ private static final int STATE_RECYCLED = 0x40;
+
+ private TileSource mModel;
+ private ScreenNail mScreenNail;
+ 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 mScreenNail 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 LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>();
+
+ // The following three queue is guarded by TileImageView.this
+ private final TileQueue mRecycledQueue = new TileQueue();
+ private final TileQueue mUploadQueue = new TileQueue();
+ private final 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<Void> mTileDecoder;
+ private final ThreadPool mThreadPool;
+ private boolean mBackgroundTileUploaded;
+
+ public static interface TileSource {
+ public int getLevelCount();
+ public ScreenNail getScreenNail();
+ public int getImageWidth();
+ public int getImageHeight();
+
+ // The tile returned by this method can be specified this way: Assuming
+ // the image size is (width, height), first take the intersection of (0,
+ // 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
+ // in extending the region, we found some part of the region are outside
+ // the image, those pixels are filled with black.
+ //
+ // If level > 0, it does the same operation on a down-scaled version of
+ // the original image (down-scaled by a factor of 2^level), but (x, y)
+ // still refers to the coordinate on the original image.
+ //
+ // The method would be called in another thread.
+ public Bitmap getTile(int level, int x, int y, int tileSize);
+ }
+
+ public static boolean isHighResolution(Context context) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager)
+ context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metrics);
+ return metrics.heightPixels > 2048 || metrics.widthPixels > 2048;
+ }
+
+ public TileImageView(GalleryContext context) {
+ mThreadPool = context.getThreadPool();
+ mTileDecoder = mThreadPool.submit(new TileDecoder());
+ if (sTileSize == 0) {
+ if (isHighResolution(context.getAndroidContext())) {
+ sTileSize = 512 ;
+ } else {
+ sTileSize = 256;
+ }
+ }
+ }
+
+ public void setModel(TileSource model) {
+ mModel = model;
+ if (model != null) notifyModelInvalidated();
+ }
+
+ public void setScreenNail(ScreenNail s) {
+ mScreenNail = s;
+ }
+
+ public void notifyModelInvalidated() {
+ invalidateTiles();
+ if (mModel == null) {
+ mScreenNail = null;
+ mImageWidth = 0;
+ mImageHeight = 0;
+ mLevelCount = 0;
+ } else {
+ setScreenNail(mModel.getScreenNail());
+ 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.
+ int n = mActiveTiles.size();
+ for (int i = 0; i < n; i++) {
+ Tile tile = mActiveTiles.valueAt(i);
+ int level = tile.mTileLevel;
+ if (level < fromLevel || level >= endLevel
+ || !range[level - fromLevel].contains(tile.mX, tile.mY)) {
+ mActiveTiles.removeAt(i);
+ i--;
+ n--;
+ recycleTile(tile);
+ }
+ }
+ }
+
+ for (int i = fromLevel; i < endLevel; ++i) {
+ int size = sTileSize << 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
+ int n = mActiveTiles.size();
+ for (int i = 0; i < n; i++) {
+ Tile tile = mActiveTiles.valueAt(i);
+ 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) FloatMath.floor(cX - width / (2f * scale));
+ int top = (int) FloatMath.floor(cY - height / (2f * scale));
+ int right = (int) FloatMath.ceil(left + width / scale);
+ int bottom = (int) FloatMath.ceil(top + height / scale);
+
+ // align the rectangle to tile boundary
+ int size = sTileSize << 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);
+ }
+
+ // Calculate where the center of the image is, in the view coordinates.
+ public void getImageCenter(Point center) {
+ // The width and height of this view.
+ int viewW = getWidth();
+ int viewH = getHeight();
+
+ // The distance between the center of the view to the center of the
+ // bitmap, in bitmap units. (mCenterX and mCenterY are the bitmap
+ // coordinates correspond to the center of view)
+ int distW, distH;
+ if (mRotation % 180 == 0) {
+ distW = mImageWidth / 2 - mCenterX;
+ distH = mImageHeight / 2 - mCenterY;
+ } else {
+ distW = mImageHeight / 2 - mCenterY;
+ distH = mImageWidth / 2 - mCenterX;
+ }
+
+ // Convert to view coordinates. mScale translates from bitmap units to
+ // view units.
+ center.x = Math.round(viewW / 2f + distW * mScale);
+ center.y = Math.round(viewH / 2f + distH * mScale);
+ }
+
+ public boolean setPosition(int centerX, int centerY, float scale, int rotation) {
+ if (mCenterX == centerX && mCenterY == centerY
+ && mScale == scale && mRotation == rotation) 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;
+ }
+
+ int n = mActiveTiles.size();
+ for (int i = 0; i < n; i++) {
+ Tile texture = mActiveTiles.valueAt(i);
+ texture.recycle();
+ }
+ mActiveTiles.clear();
+ mTileRange.set(0, 0, 0, 0);
+
+ synchronized (this) {
+ mUploadQueue.clean();
+ mDecodeQueue.clean();
+ Tile tile = mRecycledQueue.pop();
+ while (tile != null) {
+ tile.recycle();
+ tile = mRecycledQueue.pop();
+ }
+ }
+ setScreenNail(null);
+ }
+
+ public void prepareTextures() {
+ if (mTileDecoder == null) {
+ mTileDecoder = mThreadPool.submit(new TileDecoder());
+ }
+ if (mIsTextureFreed) {
+ layoutTiles(mCenterX, mCenterY, mScale, mRotation);
+ mIsTextureFreed = false;
+ setScreenNail(mModel == null ? null : mModel.getScreenNail());
+ }
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ mUploadQuota = UPLOAD_LIMIT;
+ mRenderComplete = true;
+
+ int level = mLevel;
+ int rotation = mRotation;
+ int flags = 0;
+ if (rotation != 0) flags |= GLCanvas.SAVE_FLAG_MATRIX;
+
+ if (flags != 0) {
+ canvas.save(flags);
+ if (rotation != 0) {
+ int centerX = getWidth() / 2, centerY = getHeight() / 2;
+ canvas.translate(centerX, centerY);
+ canvas.rotate(rotation, 0, 0, 1);
+ canvas.translate(-centerX, -centerY);
+ }
+ }
+ try {
+ if (level != mLevelCount && !isScreenNailAnimating()) {
+ if (mScreenNail != null) {
+ mScreenNail.noDraw();
+ }
+
+ int size = (sTileSize << 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 (mScreenNail != null) {
+ mScreenNail.draw(canvas, mOffsetX, mOffsetY,
+ Math.round(mImageWidth * mScale),
+ Math.round(mImageHeight * mScale));
+ if (isScreenNailAnimating()) {
+ invalidate();
+ }
+ }
+ } finally {
+ if (flags != 0) canvas.restore();
+ }
+
+ if (mRenderComplete) {
+ if (!mBackgroundTileUploaded) uploadBackgroundTiles(canvas);
+ } else {
+ invalidate();
+ }
+ }
+
+ private boolean isScreenNailAnimating() {
+ return (mScreenNail instanceof TiledScreenNail)
+ && ((TiledScreenNail) mScreenNail).isAnimating();
+ }
+
+ private void uploadBackgroundTiles(GLCanvas canvas) {
+ mBackgroundTileUploaded = true;
+ int n = mActiveTiles.size();
+ for (int i = 0; i < n; i++) {
+ Tile tile = mActiveTiles.valueAt(i);
+ if (!tile.isContentValid()) 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;
+ if (tile.mDecodedTile != null) {
+ GalleryBitmapPool.getInstance().put(tile.mDecodedTile);
+ tile.mDecodedTile = null;
+ }
+ mRecycledQueue.push(tile);
+ return false;
+ }
+ tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL;
+ 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;
+ if (tile.mDecodedTile != null) {
+ GalleryBitmapPool.getInstance().put(tile.mDecodedTile);
+ 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 result;
+ }
+
+ private class TileUploader implements GLRoot.OnGLIdleListener {
+ AtomicBoolean mActive = new AtomicBoolean(false);
+
+ @Override
+ public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
+ // Skips uploading if there is a pending rendering request.
+ // Returns true to keep uploading in next rendering loop.
+ if (renderRequested) return true;
+ int quota = UPLOAD_LIMIT;
+ Tile tile = null;
+ while (quota > 0) {
+ synchronized (TileImageView.this) {
+ tile = mUploadQueue.pop();
+ }
+ if (tile == null) break;
+ if (!tile.isContentValid()) {
+ boolean hasBeenLoaded = tile.isLoaded();
+ Utils.assertTrue(tile.mTileState == STATE_DECODED);
+ tile.updateContent(canvas);
+ if (!hasBeenLoaded) tile.draw(canvas, 0, 0);
+ --quota;
+ }
+ }
+ if (tile == null) mActive.set(false);
+ 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, sTileSize, sTileSize);
+
+ Tile tile = getTile(tx, ty, level);
+ if (tile != null) {
+ if (!tile.isContentValid()) {
+ if (tile.mTileState == STATE_DECODED) {
+ if (mUploadQuota > 0) {
+ --mUploadQuota;
+ tile.updateContent(canvas);
+ } else {
+ mRenderComplete = false;
+ }
+ } else if (tile.mTileState != STATE_DECODE_FAIL){
+ mRenderComplete = false;
+ queueForDecode(tile);
+ }
+ }
+ if (drawTile(tile, canvas, source, target)) return;
+ }
+ if (mScreenNail != null) {
+ int size = sTileSize << level;
+ float scaleX = (float) mScreenNail.getWidth() / mImageWidth;
+ float scaleY = (float) mScreenNail.getHeight() / mImageHeight;
+ source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
+ (ty + size) * scaleY);
+ mScreenNail.draw(canvas, source, target);
+ }
+ }
+
+ static boolean drawTile(
+ Tile tile, GLCanvas canvas, RectF source, RectF target) {
+ while (true) {
+ if (tile.isContentValid()) {
+ 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 = (sTileSize + source.left) / 2f;
+ source.right = (sTileSize + source.right) / 2f;
+ }
+ if (tile.mY == parent.mY) {
+ source.top /= 2f;
+ source.bottom /= 2f;
+ } else {
+ source.top = (sTileSize + source.top) / 2f;
+ source.bottom = (sTileSize + source.bottom) / 2f;
+ }
+ tile = parent;
+ }
+ }
+
+ private class Tile extends UploadedTexture {
+ public int mX;
+ public int mY;
+ public int mTileLevel;
+ public Tile mNext;
+ public Bitmap mDecodedTile;
+ public 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) {
+ GalleryBitmapPool.getInstance().put(bitmap);
+ }
+
+ boolean decode() {
+ // Get a tile from the original image. The tile is down-scaled
+ // by (1 << mTilelevel) from a region in the original image.
+ try {
+ mDecodedTile = DecodeUtils.ensureGLCompatibleBitmap(mModel.getTile(
+ mTileLevel, mX, mY, sTileSize));
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode tile", t);
+ }
+ return mDecodedTile != null;
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ Utils.assertTrue(mTileState == STATE_DECODED);
+
+ // We need to override the width and height, so that we won't
+ // draw beyond the boundaries.
+ int rightEdge = ((mImageWidth - mX) >> mTileLevel);
+ int bottomEdge = ((mImageHeight - mY) >> mTileLevel);
+ setSize(Math.min(sTileSize, rightEdge), Math.min(sTileSize, bottomEdge));
+
+ Bitmap bitmap = mDecodedTile;
+ mDecodedTile = null;
+ mTileState = STATE_ACTIVATED;
+ return bitmap;
+ }
+
+ // We override getTextureWidth() and getTextureHeight() here, so the
+ // texture can be re-used for different tiles regardless of the actual
+ // size of the tile (which may be small because it is a tile at the
+ // boundary).
+ @Override
+ public int getTextureWidth() {
+ return sTileSize;
+ }
+
+ @Override
+ public int getTextureHeight() {
+ return sTileSize;
+ }
+
+ 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 = sTileSize << (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 / sTileSize, mY / sTileSize, 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<Void> {
+
+ 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..0c1f66d0c
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TileImageViewAdapter.java
@@ -0,0 +1,200 @@
+/*
+ * 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.annotation.TargetApi;
+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;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.common.Utils;
+import com.android.photos.data.GalleryBitmapPool;
+
+public class TileImageViewAdapter implements TileImageView.TileSource {
+ private static final String TAG = "TileImageViewAdapter";
+ protected ScreenNail mScreenNail;
+ protected boolean mOwnScreenNail;
+ protected BitmapRegionDecoder mRegionDecoder;
+ protected int mImageWidth;
+ protected int mImageHeight;
+ protected int mLevelCount;
+
+ public TileImageViewAdapter() {
+ }
+
+ public synchronized void clear() {
+ mScreenNail = null;
+ mImageWidth = 0;
+ mImageHeight = 0;
+ mLevelCount = 0;
+ mRegionDecoder = null;
+ }
+
+ // Caller is responsible to recycle the ScreenNail
+ public synchronized void setScreenNail(
+ ScreenNail screenNail, int width, int height) {
+ Utils.checkNotNull(screenNail);
+ mScreenNail = screenNail;
+ mImageWidth = width;
+ mImageHeight = height;
+ mRegionDecoder = null;
+ mLevelCount = 0;
+ }
+
+ public synchronized void setRegionDecoder(BitmapRegionDecoder decoder) {
+ mRegionDecoder = Utils.checkNotNull(decoder);
+ mImageWidth = decoder.getWidth();
+ mImageHeight = decoder.getHeight();
+ mLevelCount = calculateLevelCount();
+ }
+
+ private int calculateLevelCount() {
+ return Math.max(0, Utils.ceilLog2(
+ (float) mImageWidth / mScreenNail.getWidth()));
+ }
+
+ // Gets a sub image on a rectangle of the current photo. For example,
+ // getTile(1, 50, 50, 100, 3, pool) means to get the region located
+ // at (50, 50) with sample level 1 (ie, down sampled by 2^1) and the
+ // target tile size (after sampling) 100 with border 3.
+ //
+ // From this spec, we can infer the actual tile size to be
+ // 100 + 3x2 = 106, and the size of the region to be extracted from the
+ // photo to be 200 with border 6.
+ //
+ // As a result, we should decode region (50-6, 50-6, 250+6, 250+6) or
+ // (44, 44, 256, 256) from the original photo and down sample it to 106.
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ @Override
+ public Bitmap getTile(int level, int x, int y, int tileSize) {
+ if (!ApiHelper.HAS_REUSING_BITMAP_IN_BITMAP_REGION_DECODER) {
+ return getTileWithoutReusingBitmap(level, x, y, tileSize);
+ }
+
+ int t = tileSize << level;
+
+ Rect wantRegion = new Rect(x, y, x + t, y + t);
+
+ boolean needClear;
+ BitmapRegionDecoder regionDecoder = null;
+
+ synchronized (this) {
+ regionDecoder = mRegionDecoder;
+ if (regionDecoder == null) return null;
+
+ // We need to clear a reused bitmap, if wantRegion is not fully
+ // within the image.
+ needClear = !new Rect(0, 0, mImageWidth, mImageHeight)
+ .contains(wantRegion);
+ }
+
+ Bitmap bitmap = GalleryBitmapPool.getInstance().get(tileSize, tileSize);
+ if (bitmap != null) {
+ if (needClear) bitmap.eraseColor(0);
+ } else {
+ bitmap = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
+ }
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Config.ARGB_8888;
+ options.inPreferQualityOverSpeed = true;
+ options.inSampleSize = (1 << level);
+ options.inBitmap = bitmap;
+
+ try {
+ // In CropImage, we may call the decodeRegion() concurrently.
+ synchronized (regionDecoder) {
+ bitmap = regionDecoder.decodeRegion(wantRegion, options);
+ }
+ } finally {
+ if (options.inBitmap != bitmap && options.inBitmap != null) {
+ GalleryBitmapPool.getInstance().put(options.inBitmap);
+ options.inBitmap = null;
+ }
+ }
+
+ if (bitmap == null) {
+ Log.w(TAG, "fail in decoding region");
+ }
+ return bitmap;
+ }
+
+ private Bitmap getTileWithoutReusingBitmap(
+ int level, int x, int y, int tileSize) {
+ int t = tileSize << level;
+ Rect wantRegion = new Rect(x, y, x + t, y + t);
+
+ BitmapRegionDecoder regionDecoder;
+ Rect overlapRegion;
+
+ synchronized (this) {
+ regionDecoder = mRegionDecoder;
+ if (regionDecoder == null) return null;
+ overlapRegion = new Rect(0, 0, mImageWidth, mImageHeight);
+ Utils.assertTrue(overlapRegion.intersect(wantRegion));
+ }
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Config.ARGB_8888;
+ options.inPreferQualityOverSpeed = true;
+ options.inSampleSize = (1 << level);
+ Bitmap bitmap = null;
+
+ // In CropImage, we may call the decodeRegion() concurrently.
+ synchronized (regionDecoder) {
+ bitmap = regionDecoder.decodeRegion(overlapRegion, options);
+ }
+
+ if (bitmap == null) {
+ Log.w(TAG, "fail in decoding region");
+ }
+
+ if (wantRegion.equals(overlapRegion)) return bitmap;
+
+ Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
+ Canvas canvas = new Canvas(result);
+ canvas.drawBitmap(bitmap,
+ (overlapRegion.left - wantRegion.left) >> level,
+ (overlapRegion.top - wantRegion.top) >> level, null);
+ return result;
+ }
+
+
+ @Override
+ public ScreenNail getScreenNail() {
+ return mScreenNail;
+ }
+
+ @Override
+ public int getImageHeight() {
+ return mImageHeight;
+ }
+
+ @Override
+ public int getImageWidth() {
+ return mImageWidth;
+ }
+
+ @Override
+ public int getLevelCount() {
+ return mLevelCount;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/TiledScreenNail.java b/src/com/android/gallery3d/ui/TiledScreenNail.java
new file mode 100644
index 000000000..860e230bb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/TiledScreenNail.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2012 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.RectF;
+
+import com.android.gallery3d.common.Utils;
+import com.android.photos.data.GalleryBitmapPool;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.TiledTexture;
+
+// This is a ScreenNail wraps a Bitmap. There are some extra functions:
+//
+// - If we need to draw before the bitmap is available, we draw a rectange of
+// placeholder color (gray).
+//
+// - When the the bitmap is available, and we have drawn the placeholder color
+// before, we will do a fade-in animation.
+public class TiledScreenNail implements ScreenNail {
+ @SuppressWarnings("unused")
+ private static final String TAG = "TiledScreenNail";
+
+ // The duration of the fading animation in milliseconds
+ private static final int DURATION = 180;
+
+ private static int sMaxSide = 640;
+
+ // These are special values for mAnimationStartTime
+ private static final long ANIMATION_NOT_NEEDED = -1;
+ private static final long ANIMATION_NEEDED = -2;
+ private static final long ANIMATION_DONE = -3;
+
+ private int mWidth;
+ private int mHeight;
+ private long mAnimationStartTime = ANIMATION_NOT_NEEDED;
+
+ private Bitmap mBitmap;
+ private TiledTexture mTexture;
+
+ public TiledScreenNail(Bitmap bitmap) {
+ mWidth = bitmap.getWidth();
+ mHeight = bitmap.getHeight();
+ mBitmap = bitmap;
+ mTexture = new TiledTexture(bitmap);
+ }
+
+ public TiledScreenNail(int width, int height) {
+ setSize(width, height);
+ }
+
+ // This gets overridden by bitmap_screennail_placeholder
+ // in GalleryUtils.initialize
+ private static int mPlaceholderColor = 0xFF222222;
+ private static boolean mDrawPlaceholder = true;
+
+ public static void setPlaceholderColor(int color) {
+ mPlaceholderColor = color;
+ }
+
+ private void setSize(int width, int height) {
+ if (width == 0 || height == 0) {
+ width = sMaxSide;
+ height = sMaxSide * 3 / 4;
+ }
+ float scale = Math.min(1, (float) sMaxSide / Math.max(width, height));
+ mWidth = Math.round(scale * width);
+ mHeight = Math.round(scale * height);
+ }
+
+ // Combines the two ScreenNails.
+ // Returns the used one and recycle the unused one.
+ public ScreenNail combine(ScreenNail other) {
+ if (other == null) {
+ return this;
+ }
+
+ if (!(other instanceof TiledScreenNail)) {
+ recycle();
+ return other;
+ }
+
+ // Now both are TiledScreenNail. Move over the information about width,
+ // height, and Bitmap, then recycle the other.
+ TiledScreenNail newer = (TiledScreenNail) other;
+ mWidth = newer.mWidth;
+ mHeight = newer.mHeight;
+ if (newer.mTexture != null) {
+ if (mBitmap != null) GalleryBitmapPool.getInstance().put(mBitmap);
+ if (mTexture != null) mTexture.recycle();
+ mBitmap = newer.mBitmap;
+ mTexture = newer.mTexture;
+ newer.mBitmap = null;
+ newer.mTexture = null;
+ }
+ newer.recycle();
+ return this;
+ }
+
+ public void updatePlaceholderSize(int width, int height) {
+ if (mBitmap != null) return;
+ if (width == 0 || height == 0) return;
+ setSize(width, height);
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public void noDraw() {
+ }
+
+ @Override
+ public void recycle() {
+ if (mTexture != null) {
+ mTexture.recycle();
+ mTexture = null;
+ }
+ if (mBitmap != null) {
+ GalleryBitmapPool.getInstance().put(mBitmap);
+ mBitmap = null;
+ }
+ }
+
+ public static void disableDrawPlaceholder() {
+ mDrawPlaceholder = false;
+ }
+
+ public static void enableDrawPlaceholder() {
+ mDrawPlaceholder = true;
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, int x, int y, int width, int height) {
+ if (mTexture == null || !mTexture.isReady()) {
+ if (mAnimationStartTime == ANIMATION_NOT_NEEDED) {
+ mAnimationStartTime = ANIMATION_NEEDED;
+ }
+ if(mDrawPlaceholder) {
+ canvas.fillRect(x, y, width, height, mPlaceholderColor);
+ }
+ return;
+ }
+
+ if (mAnimationStartTime == ANIMATION_NEEDED) {
+ mAnimationStartTime = AnimationTime.get();
+ }
+
+ if (isAnimating()) {
+ mTexture.drawMixed(canvas, mPlaceholderColor, getRatio(), x, y,
+ width, height);
+ } else {
+ mTexture.draw(canvas, x, y, width, height);
+ }
+ }
+
+ @Override
+ public void draw(GLCanvas canvas, RectF source, RectF dest) {
+ if (mTexture == null || !mTexture.isReady()) {
+ canvas.fillRect(dest.left, dest.top, dest.width(), dest.height(),
+ mPlaceholderColor);
+ return;
+ }
+
+ mTexture.draw(canvas, source, dest);
+ }
+
+ public boolean isAnimating() {
+ // The TiledTexture may not be uploaded completely yet.
+ // In that case, we count it as animating state and we will draw
+ // the placeholder in TileImageView.
+ if (mTexture == null || !mTexture.isReady()) return true;
+ if (mAnimationStartTime < 0) return false;
+ if (AnimationTime.get() - mAnimationStartTime >= DURATION) {
+ mAnimationStartTime = ANIMATION_DONE;
+ return false;
+ }
+ return true;
+ }
+
+ private float getRatio() {
+ float r = (float) (AnimationTime.get() - mAnimationStartTime) / DURATION;
+ return Utils.clamp(1.0f - r, 0.0f, 1.0f);
+ }
+
+ public boolean isShowingPlaceholder() {
+ return (mBitmap == null) || isAnimating();
+ }
+
+ public TiledTexture getTexture() {
+ return mTexture;
+ }
+
+ public static void setMaxSide(int size) {
+ sMaxSide = size;
+ }
+}
diff --git a/src/com/android/gallery3d/ui/UndoBarView.java b/src/com/android/gallery3d/ui/UndoBarView.java
new file mode 100644
index 000000000..42f12ae72
--- /dev/null
+++ b/src/com/android/gallery3d/ui/UndoBarView.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2012 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.view.MotionEvent;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.NinePatchTexture;
+import com.android.gallery3d.glrenderer.ResourceTexture;
+import com.android.gallery3d.glrenderer.StringTexture;
+import com.android.gallery3d.util.GalleryUtils;
+
+public class UndoBarView extends GLView {
+ @SuppressWarnings("unused")
+ private static final String TAG = "UndoBarView";
+
+ private static final int WHITE = 0xFFFFFFFF;
+ private static final int GRAY = 0xFFAAAAAA;
+
+ private final NinePatchTexture mPanel;
+ private final StringTexture mUndoText;
+ private final StringTexture mDeletedText;
+ private final ResourceTexture mUndoIcon;
+ private final int mBarHeight;
+ private final int mBarMargin;
+ private final int mUndoTextMargin;
+ private final int mIconSize;
+ private final int mIconMargin;
+ private final int mSeparatorTopMargin;
+ private final int mSeparatorBottomMargin;
+ private final int mSeparatorRightMargin;
+ private final int mSeparatorWidth;
+ private final int mDeletedTextMargin;
+ private final int mClickRegion;
+
+ private OnClickListener mOnClickListener;
+ private boolean mDownOnButton;
+
+ // This is the layout of UndoBarView. The unit is dp.
+ //
+ // +-+----+----------------+-+--+----+-+------+--+-+
+ // 48 | | | Deleted | | | <- | | UNDO | | |
+ // +-+----+----------------+-+--+----+-+------+--+-+
+ // 4 16 1 12 32 8 16 4
+ public UndoBarView(Context context) {
+ mBarHeight = GalleryUtils.dpToPixel(48);
+ mBarMargin = GalleryUtils.dpToPixel(4);
+ mUndoTextMargin = GalleryUtils.dpToPixel(16);
+ mIconMargin = GalleryUtils.dpToPixel(8);
+ mIconSize = GalleryUtils.dpToPixel(32);
+ mSeparatorRightMargin = GalleryUtils.dpToPixel(12);
+ mSeparatorTopMargin = GalleryUtils.dpToPixel(10);
+ mSeparatorBottomMargin = GalleryUtils.dpToPixel(10);
+ mSeparatorWidth = GalleryUtils.dpToPixel(1);
+ mDeletedTextMargin = GalleryUtils.dpToPixel(16);
+
+ mPanel = new NinePatchTexture(context, R.drawable.panel_undo_holo);
+ mUndoText = StringTexture.newInstance(context.getString(R.string.undo),
+ GalleryUtils.dpToPixel(12), GRAY, 0, true);
+ mDeletedText = StringTexture.newInstance(
+ context.getString(R.string.deleted),
+ GalleryUtils.dpToPixel(16), WHITE);
+ mUndoIcon = new ResourceTexture(
+ context, R.drawable.ic_menu_revert_holo_dark);
+ mClickRegion = mBarMargin + mUndoTextMargin + mUndoText.getWidth()
+ + mIconMargin + mIconSize + mSeparatorRightMargin;
+ }
+
+ public void setOnClickListener(OnClickListener listener) {
+ mOnClickListener = listener;
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ setMeasuredSize(0 /* unused */, mBarHeight);
+ }
+
+ @Override
+ protected void render(GLCanvas canvas) {
+ super.render(canvas);
+ advanceAnimation();
+
+ canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
+ canvas.multiplyAlpha(mAlpha);
+
+ int w = getWidth();
+ int h = getHeight();
+ mPanel.draw(canvas, mBarMargin, 0, w - mBarMargin * 2, mBarHeight);
+
+ int x = w - mBarMargin;
+ int y;
+
+ x -= mUndoTextMargin + mUndoText.getWidth();
+ y = (mBarHeight - mUndoText.getHeight()) / 2;
+ mUndoText.draw(canvas, x, y);
+
+ x -= mIconMargin + mIconSize;
+ y = (mBarHeight - mIconSize) / 2;
+ mUndoIcon.draw(canvas, x, y, mIconSize, mIconSize);
+
+ x -= mSeparatorRightMargin + mSeparatorWidth;
+ y = mSeparatorTopMargin;
+ canvas.fillRect(x, y, mSeparatorWidth,
+ mBarHeight - mSeparatorTopMargin - mSeparatorBottomMargin, GRAY);
+
+ x = mBarMargin + mDeletedTextMargin;
+ y = (mBarHeight - mDeletedText.getHeight()) / 2;
+ mDeletedText.draw(canvas, x, y);
+
+ canvas.restore();
+ }
+
+ @Override
+ protected boolean onTouch(MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDownOnButton = inUndoButton(event);
+ break;
+ case MotionEvent.ACTION_UP:
+ if (mDownOnButton) {
+ if (mOnClickListener != null && inUndoButton(event)) {
+ mOnClickListener.onClick(this);
+ }
+ mDownOnButton = false;
+ }
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ mDownOnButton = false;
+ break;
+ }
+ return true;
+ }
+
+ // Check if the event is on the right of the separator
+ private boolean inUndoButton(MotionEvent event) {
+ float x = event.getX();
+ float y = event.getY();
+ int w = getWidth();
+ int h = getHeight();
+ return (x >= w - mClickRegion && x < w && y >= 0 && y < h);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Alpha Animation
+ ////////////////////////////////////////////////////////////////////////////
+
+ private static final long NO_ANIMATION = -1;
+ private static long ANIM_TIME = 200;
+ private long mAnimationStartTime = NO_ANIMATION;
+ private float mFromAlpha, mToAlpha;
+ private float mAlpha;
+
+ private static float getTargetAlpha(int visibility) {
+ return (visibility == VISIBLE) ? 1f : 0f;
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ mAlpha = getTargetAlpha(visibility);
+ mAnimationStartTime = NO_ANIMATION;
+ super.setVisibility(visibility);
+ invalidate();
+ }
+
+ public void animateVisibility(int visibility) {
+ float target = getTargetAlpha(visibility);
+ if (mAnimationStartTime == NO_ANIMATION && mAlpha == target) return;
+ if (mAnimationStartTime != NO_ANIMATION && mToAlpha == target) return;
+
+ mFromAlpha = mAlpha;
+ mToAlpha = target;
+ mAnimationStartTime = AnimationTime.startTime();
+
+ super.setVisibility(VISIBLE);
+ invalidate();
+ }
+
+ private void advanceAnimation() {
+ if (mAnimationStartTime == NO_ANIMATION) return;
+
+ float delta = (float) (AnimationTime.get() - mAnimationStartTime) /
+ ANIM_TIME;
+ mAlpha = mFromAlpha + ((mToAlpha > mFromAlpha) ? delta : -delta);
+ mAlpha = Utils.clamp(mAlpha, 0f, 1f);
+
+ if (mAlpha == mToAlpha) {
+ mAnimationStartTime = NO_ANIMATION;
+ if (mAlpha == 0) {
+ super.setVisibility(INVISIBLE);
+ }
+ }
+ invalidate();
+ }
+}
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/ui/WakeLockHoldingProgressListener.java b/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java
new file mode 100644
index 000000000..ee61d8edb
--- /dev/null
+++ b/src/com/android/gallery3d/ui/WakeLockHoldingProgressListener.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2012 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.app.Activity;
+import android.content.Context;
+import android.os.PowerManager;
+
+import com.android.gallery3d.app.AbstractGalleryActivity;
+
+public class WakeLockHoldingProgressListener implements MenuExecutor.ProgressListener {
+ static private final String DEFAULT_WAKE_LOCK_LABEL = "Gallery Progress Listener";
+ private AbstractGalleryActivity mActivity;
+ private PowerManager.WakeLock mWakeLock;
+
+ public WakeLockHoldingProgressListener(AbstractGalleryActivity galleryActivity) {
+ this(galleryActivity, DEFAULT_WAKE_LOCK_LABEL);
+ }
+
+ public WakeLockHoldingProgressListener(AbstractGalleryActivity galleryActivity, String label) {
+ mActivity = galleryActivity;
+ PowerManager pm =
+ (PowerManager) ((Activity) mActivity).getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, label);
+ }
+
+ @Override
+ public void onProgressComplete(int result) {
+ mWakeLock.release();
+ }
+
+ @Override
+ public void onProgressStart() {
+ mWakeLock.acquire();
+ }
+
+ protected AbstractGalleryActivity getActivity() {
+ return mActivity;
+ }
+
+ @Override
+ public void onProgressUpdate(int index) {
+ }
+
+ @Override
+ public void onConfirmDialogDismissed(boolean confirmed) {
+ }
+
+ @Override
+ public void onConfirmDialogShown() {
+ }
+}
diff --git a/src/com/android/gallery3d/util/AccessibilityUtils.java b/src/com/android/gallery3d/util/AccessibilityUtils.java
new file mode 100644
index 000000000..9df8e4ece
--- /dev/null
+++ b/src/com/android/gallery3d/util/AccessibilityUtils.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2012 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.content.Context;
+import android.support.v4.view.accessibility.AccessibilityRecordCompat;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.gallery3d.common.ApiHelper;
+
+/**
+ * AccessibilityUtils provides functions needed in accessibility mode. All the functions
+ * in this class are made compatible with gingerbread and later API's
+*/
+public class AccessibilityUtils {
+ public static void makeAnnouncement(View view, CharSequence announcement) {
+ if (view == null)
+ return;
+ if (ApiHelper.HAS_ANNOUNCE_FOR_ACCESSIBILITY) {
+ view.announceForAccessibility(announcement);
+ } else {
+ // For API 15 and earlier, we need to construct an accessibility event
+ Context ctx = view.getContext();
+ AccessibilityManager am = (AccessibilityManager) ctx.getSystemService(
+ Context.ACCESSIBILITY_SERVICE);
+ if (!am.isEnabled()) return;
+ AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
+ AccessibilityRecordCompat arc = new AccessibilityRecordCompat(event);
+ arc.setSource(view);
+ event.setClassName(view.getClass().getName());
+ event.setPackageName(view.getContext().getPackageName());
+ event.setEnabled(view.isEnabled());
+ event.getText().add(announcement);
+ am.sendAccessibilityEvent(event);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/util/BucketNames.java b/src/com/android/gallery3d/util/BucketNames.java
new file mode 100644
index 000000000..990dc8224
--- /dev/null
+++ b/src/com/android/gallery3d/util/BucketNames.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012 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;
+
+/**
+ * Bucket names for buckets that are created and used in the Gallery.
+ */
+public class BucketNames {
+
+ public static final String CAMERA = "DCIM/Camera";
+ public static final String IMPORTED = "Imported";
+ public static final String DOWNLOAD = "download";
+ public static final String EDITED_ONLINE_PHOTOS = "EditedOnlinePhotos";
+ public static final String SCREENSHOTS = "Pictures/Screenshots";
+}
diff --git a/src/com/android/gallery3d/util/CacheManager.java b/src/com/android/gallery3d/util/CacheManager.java
new file mode 100644
index 000000000..ba466f79b
--- /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 android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.android.gallery3d.common.BlobCache;
+
+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<String, BlobCache> sCacheMap =
+ new HashMap<String, BlobCache>();
+ 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/GalleryUtils.java b/src/com/android/gallery3d/util/GalleryUtils.java
new file mode 100644
index 000000000..9245e2c5f
--- /dev/null
+++ b/src/com/android/gallery3d/util/GalleryUtils.java
@@ -0,0 +1,404 @@
+/*
+ * 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.annotation.TargetApi;
+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.content.res.Resources;
+import android.graphics.Color;
+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 com.android.gallery3d.R;
+import com.android.gallery3d.app.Gallery;
+import com.android.gallery3d.app.PackagesMonitor;
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.ui.TiledScreenNail;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+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 CAMERA_LAUNCHER_NAME = "com.android.camera.CameraLauncher";
+
+ public static final String MIME_TYPE_IMAGE = "image/*";
+ public static final String MIME_TYPE_VIDEO = "video/*";
+ public static final String MIME_TYPE_PANORAMA360 = "application/vnd.google.panorama360+jpg";
+ public static final String MIME_TYPE_ALL = "*/*";
+
+ private static final String DIR_TYPE_IMAGE = "vnd.android.cursor.dir/image";
+ private static final String DIR_TYPE_VIDEO = "vnd.android.cursor.dir/video";
+
+ 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 float sPixelDensity = -1f;
+ private static boolean sCameraAvailableInitialized = false;
+ private static boolean sCameraAvailable;
+
+ public static void initialize(Context context) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager)
+ context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metrics);
+ sPixelDensity = metrics.density;
+ Resources r = context.getResources();
+ TiledScreenNail.setPlaceholderColor(r.getColor(
+ R.color.bitmap_screennail_placeholder));
+ initializeThumbnailSizes(metrics, r);
+ }
+
+ private static void initializeThumbnailSizes(DisplayMetrics metrics, Resources r) {
+ int maxPixels = Math.max(metrics.heightPixels, metrics.widthPixels);
+
+ // For screen-nails, we never need to completely fill the screen
+ MediaItem.setThumbnailSizes(maxPixels / 2, maxPixels / 5);
+ TiledScreenNail.setMaxSide(maxPixels / 2);
+ }
+
+ public static float[] intColorToFloatARGBArray(int from) {
+ return new float[] {
+ Color.alpha(from) / 255f,
+ Color.red(from) / 255f,
+ Color.green(from) / 255f,
+ Color.blue(from) / 255f
+ };
+ }
+
+ 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() {
+ @Override
+ 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<ResolveInfo> 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 isAnyCameraAvailable(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<ResolveInfo> 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 isCameraAvailable(Context context) {
+ if (sCameraAvailableInitialized) return sCameraAvailable;
+ PackageManager pm = context.getPackageManager();
+ ComponentName name = new ComponentName(context, CAMERA_LAUNCHER_NAME);
+ int state = pm.getComponentEnabledSetting(name);
+ sCameraAvailableInitialized = true;
+ sCameraAvailable =
+ (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)
+ || (state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+ return sCameraAvailable;
+ }
+
+ public static void startCameraActivity(Context context) {
+ Intent intent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA)
+ .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
+ public static void startGalleryActivity(Context context) {
+ Intent intent = new Intent(context, Gallery.class)
+ .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
+ 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 String formatLatitudeLongitude(String format, double latitude,
+ double longitude) {
+ // We need to specify the locale otherwise it may go wrong in some language
+ // (e.g. Locale.FRENCH)
+ return String.format(Locale.ENGLISH, format, latitude, longitude);
+ }
+
+ 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 = formatLatitudeLongitude("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 = formatLatitudeLongitude("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();
+ }
+
+ // Return the local path that matches the given bucketId. If no match is
+ // found, return null
+ public static String searchDirForPath(File dir, int bucketId) {
+ File[] files = dir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ String path = file.getAbsolutePath();
+ if (GalleryUtils.getBucketId(path) == bucketId) {
+ return path;
+ } else {
+ path = searchDirForPath(file, bucketId);
+ if (path != null) return path;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ // 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;
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ public static int determineTypeBits(Context context, Intent intent) {
+ int typeBits = 0;
+ String type = intent.resolveType(context);
+
+ if (MIME_TYPE_ALL.equals(type)) {
+ typeBits = DataManager.INCLUDE_ALL;
+ } else if (MIME_TYPE_IMAGE.equals(type) ||
+ DIR_TYPE_IMAGE.equals(type)) {
+ typeBits = DataManager.INCLUDE_IMAGE;
+ } else if (MIME_TYPE_VIDEO.equals(type) ||
+ DIR_TYPE_VIDEO.equals(type)) {
+ typeBits = DataManager.INCLUDE_VIDEO;
+ } else {
+ typeBits = DataManager.INCLUDE_ALL;
+ }
+
+ if (ApiHelper.HAS_INTENT_EXTRA_LOCAL_ONLY) {
+ if (intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false)) {
+ typeBits |= DataManager.INCLUDE_LOCAL_ONLY;
+ }
+ }
+
+ 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 boolean isPanorama(MediaItem item) {
+ if (item == null) return false;
+ int w = item.getWidth();
+ int h = item.getHeight();
+ return (h > 0 && w / h >= 2);
+ }
+}
diff --git a/src/com/android/gallery3d/util/Holder.java b/src/com/android/gallery3d/util/Holder.java
new file mode 100644
index 000000000..0ce914c1d
--- /dev/null
+++ b/src/com/android/gallery3d/util/Holder.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012 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 Holder<T> {
+ private T mObject;
+
+ public void set(T object) {
+ mObject = object;
+ }
+
+ public T get() {
+ return mObject;
+ }
+}
diff --git a/src/com/android/gallery3d/util/IdentityCache.java b/src/com/android/gallery3d/util/IdentityCache.java
new file mode 100644
index 000000000..3edc424a3
--- /dev/null
+++ b/src/com/android/gallery3d/util/IdentityCache.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.util;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Set;
+
+public class IdentityCache<K, V> {
+
+ private final HashMap<K, Entry<K, V>> mWeakMap =
+ new HashMap<K, Entry<K, V>>();
+ private ReferenceQueue<V> mQueue = new ReferenceQueue<V>();
+
+ public IdentityCache() {
+ }
+
+ private static class Entry<K, V> extends WeakReference<V> {
+ K mKey;
+
+ public Entry(K key, V value, ReferenceQueue<V> queue) {
+ super(value, queue);
+ mKey = key;
+ }
+ }
+
+ private void cleanUpWeakMap() {
+ Entry<K, V> entry = (Entry<K, V>) mQueue.poll();
+ while (entry != null) {
+ mWeakMap.remove(entry.mKey);
+ entry = (Entry<K, V>) mQueue.poll();
+ }
+ }
+
+ public synchronized V put(K key, V value) {
+ cleanUpWeakMap();
+ Entry<K, V> entry = mWeakMap.put(
+ key, new Entry<K, V>(key, value, mQueue));
+ return entry == null ? null : entry.get();
+ }
+
+ public synchronized V get(K key) {
+ cleanUpWeakMap();
+ Entry<K, V> entry = mWeakMap.get(key);
+ return entry == null ? null : entry.get();
+ }
+
+ // This is currently unused.
+ /*
+ public synchronized void clear() {
+ mWeakMap.clear();
+ mQueue = new ReferenceQueue<V>();
+ }
+ */
+
+ // This is for debugging only
+ public synchronized ArrayList<K> keys() {
+ Set<K> set = mWeakMap.keySet();
+ ArrayList<K> result = new ArrayList<K>(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..2c4dc2c83
--- /dev/null
+++ b/src/com/android/gallery3d/util/IntArray.java
@@ -0,0 +1,60 @@
+/*
+ * 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 removeLast() {
+ mSize--;
+ return mData[mSize];
+ }
+
+ public int size() {
+ return mSize;
+ }
+
+ // For testing only
+ 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/JobLimiter.java b/src/com/android/gallery3d/util/JobLimiter.java
new file mode 100644
index 000000000..42b754153
--- /dev/null
+++ b/src/com/android/gallery3d/util/JobLimiter.java
@@ -0,0 +1,159 @@
+/*
+ * 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.util;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+
+import java.util.LinkedList;
+
+// Limit the number of concurrent jobs that has been submitted into a ThreadPool
+@SuppressWarnings("rawtypes")
+public class JobLimiter implements FutureListener {
+ private static final String TAG = "JobLimiter";
+
+ // State Transition:
+ // INIT -> DONE, CANCELLED
+ // DONE -> CANCELLED
+ private static final int STATE_INIT = 0;
+ private static final int STATE_DONE = 1;
+ private static final int STATE_CANCELLED = 2;
+
+ private final LinkedList<JobWrapper<?>> mJobs = new LinkedList<JobWrapper<?>>();
+ private final ThreadPool mPool;
+ private int mLimit;
+
+ private static class JobWrapper<T> implements Future<T>, Job<T> {
+ private int mState = STATE_INIT;
+ private Job<T> mJob;
+ private Future<T> mDelegate;
+ private FutureListener<T> mListener;
+ private T mResult;
+
+ public JobWrapper(Job<T> job, FutureListener<T> listener) {
+ mJob = job;
+ mListener = listener;
+ }
+
+ public synchronized void setFuture(Future<T> future) {
+ if (mState != STATE_INIT) return;
+ mDelegate = future;
+ }
+
+ @Override
+ public void cancel() {
+ FutureListener<T> listener = null;
+ synchronized (this) {
+ if (mState != STATE_DONE) {
+ listener = mListener;
+ mJob = null;
+ mListener = null;
+ if (mDelegate != null) {
+ mDelegate.cancel();
+ mDelegate = null;
+ }
+ }
+ mState = STATE_CANCELLED;
+ mResult = null;
+ notifyAll();
+ }
+ if (listener != null) listener.onFutureDone(this);
+ }
+
+ @Override
+ public synchronized boolean isCancelled() {
+ return mState == STATE_CANCELLED;
+ }
+
+ @Override
+ public boolean isDone() {
+ // Both CANCELLED AND DONE is considered as done
+ return mState != STATE_INIT;
+ }
+
+ @Override
+ public synchronized T get() {
+ while (mState == STATE_INIT) {
+ // handle the interrupted exception of wait()
+ Utils.waitWithoutInterrupt(this);
+ }
+ return mResult;
+ }
+
+ @Override
+ public void waitDone() {
+ get();
+ }
+
+ @Override
+ public T run(JobContext jc) {
+ Job<T> job = null;
+ synchronized (this) {
+ if (mState == STATE_CANCELLED) return null;
+ job = mJob;
+ }
+ T result = null;
+ try {
+ result = job.run(jc);
+ } catch (Throwable t) {
+ Log.w(TAG, "error executing job: " + job, t);
+ }
+ FutureListener<T> listener = null;
+ synchronized (this) {
+ if (mState == STATE_CANCELLED) return null;
+ mState = STATE_DONE;
+ listener = mListener;
+ mListener = null;
+ mJob = null;
+ mResult = result;
+ notifyAll();
+ }
+ if (listener != null) listener.onFutureDone(this);
+ return result;
+ }
+ }
+
+ public JobLimiter(ThreadPool pool, int limit) {
+ mPool = Utils.checkNotNull(pool);
+ mLimit = limit;
+ }
+
+ public synchronized <T> Future<T> submit(Job<T> job, FutureListener<T> listener) {
+ JobWrapper<T> future = new JobWrapper<T>(Utils.checkNotNull(job), listener);
+ mJobs.addLast(future);
+ submitTasksIfAllowed();
+ return future;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void submitTasksIfAllowed() {
+ while (mLimit > 0 && !mJobs.isEmpty()) {
+ JobWrapper wrapper = mJobs.removeFirst();
+ if (!wrapper.isCancelled()) {
+ --mLimit;
+ wrapper.setFuture(mPool.submit(wrapper, this));
+ }
+ }
+ }
+
+ @Override
+ public synchronized void onFutureDone(Future future) {
+ ++mLimit;
+ submitTasksIfAllowed();
+ }
+}
diff --git a/src/com/android/gallery3d/util/LinkedNode.java b/src/com/android/gallery3d/util/LinkedNode.java
new file mode 100644
index 000000000..4cfc3cded
--- /dev/null
+++ b/src/com/android/gallery3d/util/LinkedNode.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.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<T extends LinkedNode> {
+ private LinkedNode mHead = new LinkedNode();
+
+ 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 <T extends LinkedNode> List<T> newList() {
+ return new List<T>();
+ }
+}
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..043800561
--- /dev/null
+++ b/src/com/android/gallery3d/util/MediaSetUtils.java
@@ -0,0 +1,66 @@
+/*
+ * 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.Environment;
+
+import com.android.gallery3d.data.LocalAlbum;
+import com.android.gallery3d.data.LocalMergeAlbum;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.Path;
+
+import java.util.Comparator;
+
+public class MediaSetUtils {
+ public static final Comparator<MediaSet> NAME_COMPARATOR = new NameComparator();
+
+ public static final int CAMERA_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() + "/"
+ + BucketNames.CAMERA);
+ public static final int DOWNLOAD_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() + "/"
+ + BucketNames.DOWNLOAD);
+ public static final int EDITED_ONLINE_PHOTOS_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() + "/"
+ + BucketNames.EDITED_ONLINE_PHOTOS);
+ public static final int IMPORTED_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() + "/"
+ + BucketNames.IMPORTED);
+ public static final int SNAPSHOT_BUCKET_ID = GalleryUtils.getBucketId(
+ Environment.getExternalStorageDirectory().toString() +
+ "/" + BucketNames.SCREENSHOTS);
+
+ 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<MediaSet> {
+ @Override
+ 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/MotionEventHelper.java b/src/com/android/gallery3d/util/MotionEventHelper.java
new file mode 100644
index 000000000..715f7fa69
--- /dev/null
+++ b/src/com/android/gallery3d/util/MotionEventHelper.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 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.annotation.TargetApi;
+import android.graphics.Matrix;
+import android.util.FloatMath;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+
+import com.android.gallery3d.common.ApiHelper;
+
+public final class MotionEventHelper {
+ private MotionEventHelper() {}
+
+ public static MotionEvent transformEvent(MotionEvent e, Matrix m) {
+ // We try to use the new transform method if possible because it uses
+ // less memory.
+ if (ApiHelper.HAS_MOTION_EVENT_TRANSFORM) {
+ return transformEventNew(e, m);
+ } else {
+ return transformEventOld(e, m);
+ }
+ }
+
+ @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
+ private static MotionEvent transformEventNew(MotionEvent e, Matrix m) {
+ MotionEvent newEvent = MotionEvent.obtain(e);
+ newEvent.transform(m);
+ return newEvent;
+ }
+
+ // This is copied from Input.cpp in the android framework.
+ private static MotionEvent transformEventOld(MotionEvent e, Matrix m) {
+ long downTime = e.getDownTime();
+ long eventTime = e.getEventTime();
+ int action = e.getAction();
+ int pointerCount = e.getPointerCount();
+ int[] pointerIds = getPointerIds(e);
+ PointerCoords[] pointerCoords = getPointerCoords(e);
+ int metaState = e.getMetaState();
+ float xPrecision = e.getXPrecision();
+ float yPrecision = e.getYPrecision();
+ int deviceId = e.getDeviceId();
+ int edgeFlags = e.getEdgeFlags();
+ int source = e.getSource();
+ int flags = e.getFlags();
+
+ // Copy the x and y coordinates into an array, map them, and copy back.
+ float[] xy = new float[pointerCoords.length * 2];
+ for (int i = 0; i < pointerCount;i++) {
+ xy[2 * i] = pointerCoords[i].x;
+ xy[2 * i + 1] = pointerCoords[i].y;
+ }
+ m.mapPoints(xy);
+ for (int i = 0; i < pointerCount;i++) {
+ pointerCoords[i].x = xy[2 * i];
+ pointerCoords[i].y = xy[2 * i + 1];
+ pointerCoords[i].orientation = transformAngle(
+ m, pointerCoords[i].orientation);
+ }
+
+ MotionEvent n = MotionEvent.obtain(downTime, eventTime, action,
+ pointerCount, pointerIds, pointerCoords, metaState, xPrecision,
+ yPrecision, deviceId, edgeFlags, source, flags);
+
+ return n;
+ }
+
+ private static int[] getPointerIds(MotionEvent e) {
+ int n = e.getPointerCount();
+ int[] r = new int[n];
+ for (int i = 0; i < n; i++) {
+ r[i] = e.getPointerId(i);
+ }
+ return r;
+ }
+
+ private static PointerCoords[] getPointerCoords(MotionEvent e) {
+ int n = e.getPointerCount();
+ PointerCoords[] r = new PointerCoords[n];
+ for (int i = 0; i < n; i++) {
+ r[i] = new PointerCoords();
+ e.getPointerCoords(i, r[i]);
+ }
+ return r;
+ }
+
+ private static float transformAngle(Matrix m, float angleRadians) {
+ // Construct and transform a vector oriented at the specified clockwise
+ // angle from vertical. Coordinate system: down is increasing Y, right is
+ // increasing X.
+ float[] v = new float[2];
+ v[0] = FloatMath.sin(angleRadians);
+ v[1] = -FloatMath.cos(angleRadians);
+ m.mapVectors(v);
+
+ // Derive the transformed vector's clockwise angle from vertical.
+ float result = (float) Math.atan2(v[0], -v[1]);
+ if (result < -Math.PI / 2) {
+ result += Math.PI;
+ } else if (result > Math.PI / 2) {
+ result -= Math.PI;
+ }
+ return result;
+ }
+}
diff --git a/src/com/android/gallery3d/util/Profile.java b/src/com/android/gallery3d/util/Profile.java
new file mode 100644
index 000000000..7ed72c90e
--- /dev/null
+++ b/src/com/android/gallery3d/util/Profile.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2012 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.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+// The Profile class is used to collect profiling information for a thread. It
+// samples stack traces for a thread periodically. enable() and disable() is
+// used to enable and disable profiling for the calling thread. The profiling
+// information can then be dumped to a file using the dumpToFile() method.
+//
+// The disableAll() method can be used to disable profiling for all threads and
+// can be called in onPause() to ensure all profiling is disabled when an
+// activity is paused.
+public class Profile {
+ @SuppressWarnings("unused")
+ private static final String TAG = "Profile";
+ private static final int NS_PER_MS = 1000000;
+
+ // This is a watchdog entry for one thread.
+ // For every cycleTime period, we dump the stack of the thread.
+ private static class WatchEntry {
+ Thread thread;
+
+ // Both are in milliseconds
+ int cycleTime;
+ int wakeTime;
+
+ boolean isHolding;
+ ArrayList<String[]> holdingStacks = new ArrayList<String[]>();
+ }
+
+ // This is a watchdog thread which dumps stacks of other threads periodically.
+ private static Watchdog sWatchdog = new Watchdog();
+
+ private static class Watchdog {
+ private ArrayList<WatchEntry> mList = new ArrayList<WatchEntry>();
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private Runnable mProcessRunnable = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (Watchdog.this) {
+ processList();
+ }
+ }
+ };
+ private Random mRandom = new Random();
+ private ProfileData mProfileData = new ProfileData();
+
+ public Watchdog() {
+ mHandlerThread = new HandlerThread("Watchdog Handler",
+ Process.THREAD_PRIORITY_FOREGROUND);
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ }
+
+ public synchronized void addWatchEntry(Thread thread, int cycleTime) {
+ WatchEntry e = new WatchEntry();
+ e.thread = thread;
+ e.cycleTime = cycleTime;
+ int firstDelay = 1 + mRandom.nextInt(cycleTime);
+ e.wakeTime = (int) (System.nanoTime() / NS_PER_MS) + firstDelay;
+ mList.add(e);
+ processList();
+ }
+
+ public synchronized void removeWatchEntry(Thread thread) {
+ for (int i = 0; i < mList.size(); i++) {
+ if (mList.get(i).thread == thread) {
+ mList.remove(i);
+ break;
+ }
+ }
+ processList();
+ }
+
+ public synchronized void removeAllWatchEntries() {
+ mList.clear();
+ processList();
+ }
+
+ private void processList() {
+ mHandler.removeCallbacks(mProcessRunnable);
+ if (mList.size() == 0) return;
+
+ int currentTime = (int) (System.nanoTime() / NS_PER_MS);
+ int nextWakeTime = 0;
+
+ for (WatchEntry entry : mList) {
+ if (currentTime > entry.wakeTime) {
+ entry.wakeTime += entry.cycleTime;
+ Thread thread = entry.thread;
+ sampleStack(entry);
+ }
+
+ if (entry.wakeTime > nextWakeTime) {
+ nextWakeTime = entry.wakeTime;
+ }
+ }
+
+ long delay = nextWakeTime - currentTime;
+ mHandler.postDelayed(mProcessRunnable, delay);
+ }
+
+ private void sampleStack(WatchEntry entry) {
+ Thread thread = entry.thread;
+ StackTraceElement[] stack = thread.getStackTrace();
+ String[] lines = new String[stack.length];
+ for (int i = 0; i < stack.length; i++) {
+ lines[i] = stack[i].toString();
+ }
+ if (entry.isHolding) {
+ entry.holdingStacks.add(lines);
+ } else {
+ mProfileData.addSample(lines);
+ }
+ }
+
+ private WatchEntry findEntry(Thread thread) {
+ for (int i = 0; i < mList.size(); i++) {
+ WatchEntry entry = mList.get(i);
+ if (entry.thread == thread) return entry;
+ }
+ return null;
+ }
+
+ public synchronized void dumpToFile(String filename) {
+ mProfileData.dumpToFile(filename);
+ }
+
+ public synchronized void reset() {
+ mProfileData.reset();
+ }
+
+ public synchronized void hold(Thread t) {
+ WatchEntry entry = findEntry(t);
+
+ // This can happen if the profiling is disabled (probably from
+ // another thread). Same check is applied in commit() and drop()
+ // below.
+ if (entry == null) return;
+
+ entry.isHolding = true;
+ }
+
+ public synchronized void commit(Thread t) {
+ WatchEntry entry = findEntry(t);
+ if (entry == null) return;
+ ArrayList<String[]> stacks = entry.holdingStacks;
+ for (int i = 0; i < stacks.size(); i++) {
+ mProfileData.addSample(stacks.get(i));
+ }
+ entry.isHolding = false;
+ entry.holdingStacks.clear();
+ }
+
+ public synchronized void drop(Thread t) {
+ WatchEntry entry = findEntry(t);
+ if (entry == null) return;
+ entry.isHolding = false;
+ entry.holdingStacks.clear();
+ }
+ }
+
+ // Enable profiling for the calling thread. Periodically (every cycleTimeInMs
+ // milliseconds) sample the stack trace of the calling thread.
+ public static void enable(int cycleTimeInMs) {
+ Thread t = Thread.currentThread();
+ sWatchdog.addWatchEntry(t, cycleTimeInMs);
+ }
+
+ // Disable profiling for the calling thread.
+ public static void disable() {
+ sWatchdog.removeWatchEntry(Thread.currentThread());
+ }
+
+ // Disable profiling for all threads.
+ public static void disableAll() {
+ sWatchdog.removeAllWatchEntries();
+ }
+
+ // Dump the profiling data to a file.
+ public static void dumpToFile(String filename) {
+ sWatchdog.dumpToFile(filename);
+ }
+
+ // Reset the collected profiling data.
+ public static void reset() {
+ sWatchdog.reset();
+ }
+
+ // Hold the future samples coming from current thread until commit() or
+ // drop() is called, and those samples are recorded or ignored as a result.
+ // This must called after enable() to be effective.
+ public static void hold() {
+ sWatchdog.hold(Thread.currentThread());
+ }
+
+ public static void commit() {
+ sWatchdog.commit(Thread.currentThread());
+ }
+
+ public static void drop() {
+ sWatchdog.drop(Thread.currentThread());
+ }
+}
diff --git a/src/com/android/gallery3d/util/ProfileData.java b/src/com/android/gallery3d/util/ProfileData.java
new file mode 100644
index 000000000..a1bb8e1e4
--- /dev/null
+++ b/src/com/android/gallery3d/util/ProfileData.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2012 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.util.Log;
+
+import com.android.gallery3d.common.Utils;
+
+import java.io.DataOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+// ProfileData keeps profiling samples in a tree structure.
+// The addSample() method adds a sample. The dumpToFile() method saves the data
+// to a file. The reset() method clears all samples.
+public class ProfileData {
+ @SuppressWarnings("unused")
+ private static final String TAG = "ProfileData";
+
+ private static class Node {
+ public int id; // this is the name of this node, mapped from mNameToId
+ public Node parent;
+ public int sampleCount;
+ public ArrayList<Node> children;
+ public Node(Node parent, int id) {
+ this.parent = parent;
+ this.id = id;
+ }
+ }
+
+ private Node mRoot;
+ private int mNextId;
+ private HashMap<String, Integer> mNameToId;
+ private DataOutputStream mOut;
+ private byte mScratch[] = new byte[4]; // scratch space for writeInt()
+
+ public ProfileData() {
+ mRoot = new Node(null, -1); // The id of the root node is unused.
+ mNameToId = new HashMap<String, Integer>();
+ }
+
+ public void reset() {
+ mRoot = new Node(null, -1);
+ mNameToId.clear();
+ mNextId = 0;
+ }
+
+ private int nameToId(String name) {
+ Integer id = mNameToId.get(name);
+ if (id == null) {
+ id = ++mNextId; // The tool doesn't want id=0, so we start from 1.
+ mNameToId.put(name, id);
+ }
+ return id;
+ }
+
+ public void addSample(String[] stack) {
+ int[] ids = new int[stack.length];
+ for (int i = 0; i < stack.length; i++) {
+ ids[i] = nameToId(stack[i]);
+ }
+
+ Node node = mRoot;
+ for (int i = stack.length - 1; i >= 0; i--) {
+ if (node.children == null) {
+ node.children = new ArrayList<Node>();
+ }
+
+ int id = ids[i];
+ ArrayList<Node> children = node.children;
+ int j;
+ for (j = 0; j < children.size(); j++) {
+ if (children.get(j).id == id) break;
+ }
+ if (j == children.size()) {
+ children.add(new Node(node, id));
+ }
+
+ node = children.get(j);
+ }
+
+ node.sampleCount++;
+ }
+
+ public void dumpToFile(String filename) {
+ try {
+ mOut = new DataOutputStream(new FileOutputStream(filename));
+ // Start record
+ writeInt(0);
+ writeInt(3);
+ writeInt(1);
+ writeInt(20000); // Sampling period: 20ms
+ writeInt(0);
+
+ // Samples
+ writeAllStacks(mRoot, 0);
+
+ // End record
+ writeInt(0);
+ writeInt(1);
+ writeInt(0);
+ writeAllSymbols();
+ } catch (IOException ex) {
+ Log.w("Failed to dump to file", ex);
+ } finally {
+ Utils.closeSilently(mOut);
+ }
+ }
+
+ // Writes out one stack, consisting of N+2 words:
+ // first word: sample count
+ // second word: depth of the stack (N)
+ // N words: each word is the id of one address in the stack
+ private void writeOneStack(Node node, int depth) throws IOException {
+ writeInt(node.sampleCount);
+ writeInt(depth);
+ while (depth-- > 0) {
+ writeInt(node.id);
+ node = node.parent;
+ }
+ }
+
+ private void writeAllStacks(Node node, int depth) throws IOException {
+ if (node.sampleCount > 0) {
+ writeOneStack(node, depth);
+ }
+
+ ArrayList<Node> children = node.children;
+ if (children != null) {
+ for (int i = 0; i < children.size(); i++) {
+ writeAllStacks(children.get(i), depth + 1);
+ }
+ }
+ }
+
+ // Writes out the symbol table. Each line is like:
+ // 0x17e java.util.ArrayList.isEmpty(ArrayList.java:319)
+ private void writeAllSymbols() throws IOException {
+ for (Entry<String, Integer> entry : mNameToId.entrySet()) {
+ mOut.writeBytes(String.format("0x%x %s\n", entry.getValue(), entry.getKey()));
+ }
+ }
+
+ private void writeInt(int v) throws IOException {
+ mScratch[0] = (byte) v;
+ mScratch[1] = (byte) (v >> 8);
+ mScratch[2] = (byte) (v >> 16);
+ mScratch[3] = (byte) (v >> 24);
+ mOut.write(mScratch);
+ }
+}
diff --git a/src/com/android/gallery3d/util/RangeArray.java b/src/com/android/gallery3d/util/RangeArray.java
new file mode 100644
index 000000000..8e61348a3
--- /dev/null
+++ b/src/com/android/gallery3d/util/RangeArray.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2012 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 is an array whose index ranges from min to max (inclusive).
+public class RangeArray<T> {
+ private T[] mData;
+ private int mOffset;
+
+ public RangeArray(int min, int max) {
+ mData = (T[]) new Object[max - min + 1];
+ mOffset = min;
+ }
+
+ // Wraps around an existing array
+ public RangeArray(T[] src, int min, int max) {
+ if (max - min + 1 != src.length) {
+ throw new AssertionError();
+ }
+ mData = src;
+ mOffset = min;
+ }
+
+ public void put(int i, T object) {
+ mData[i - mOffset] = object;
+ }
+
+ public T get(int i) {
+ return mData[i - mOffset];
+ }
+
+ public int indexOf(T object) {
+ for (int i = 0; i < mData.length; i++) {
+ if (mData[i] == object) return i + mOffset;
+ }
+ return Integer.MAX_VALUE;
+ }
+}
diff --git a/src/com/android/gallery3d/util/RangeBoolArray.java b/src/com/android/gallery3d/util/RangeBoolArray.java
new file mode 100644
index 000000000..035fc40a4
--- /dev/null
+++ b/src/com/android/gallery3d/util/RangeBoolArray.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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 is an array whose index ranges from min to max (inclusive).
+public class RangeBoolArray {
+ private boolean[] mData;
+ private int mOffset;
+
+ public RangeBoolArray(int min, int max) {
+ mData = new boolean[max - min + 1];
+ mOffset = min;
+ }
+
+ // Wraps around an existing array
+ public RangeBoolArray(boolean[] src, int min, int max) {
+ mData = src;
+ mOffset = min;
+ }
+
+ public void put(int i, boolean object) {
+ mData[i - mOffset] = object;
+ }
+
+ public boolean get(int i) {
+ return mData[i - mOffset];
+ }
+
+ public int indexOf(boolean object) {
+ for (int i = 0; i < mData.length; i++) {
+ if (mData[i] == object) return i + mOffset;
+ }
+ return Integer.MAX_VALUE;
+ }
+}
diff --git a/src/com/android/gallery3d/util/RangeIntArray.java b/src/com/android/gallery3d/util/RangeIntArray.java
new file mode 100644
index 000000000..9dbb99fac
--- /dev/null
+++ b/src/com/android/gallery3d/util/RangeIntArray.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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 is an array whose index ranges from min to max (inclusive).
+public class RangeIntArray {
+ private int[] mData;
+ private int mOffset;
+
+ public RangeIntArray(int min, int max) {
+ mData = new int[max - min + 1];
+ mOffset = min;
+ }
+
+ // Wraps around an existing array
+ public RangeIntArray(int[] src, int min, int max) {
+ mData = src;
+ mOffset = min;
+ }
+
+ public void put(int i, int object) {
+ mData[i - mOffset] = object;
+ }
+
+ public int get(int i) {
+ return mData[i - mOffset];
+ }
+
+ public int indexOf(int object) {
+ for (int i = 0; i < mData.length; i++) {
+ if (mData[i] == object) return i + mOffset;
+ }
+ return Integer.MAX_VALUE;
+ }
+}
diff --git a/src/com/android/gallery3d/util/ReverseGeocoder.java b/src/com/android/gallery3d/util/ReverseGeocoder.java
new file mode 100644
index 000000000..a8b26d9b5
--- /dev/null
+++ b/src/com/android/gallery3d/util/ReverseGeocoder.java
@@ -0,0 +1,418 @@
+/*
+ * 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.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 com.android.gallery3d.common.BlobCache;
+
+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 {
+ @SuppressWarnings("unused")
+ 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<String> 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<Address> 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/SaveVideoFileInfo.java b/src/com/android/gallery3d/util/SaveVideoFileInfo.java
new file mode 100644
index 000000000..c7e5e8568
--- /dev/null
+++ b/src/com/android/gallery3d/util/SaveVideoFileInfo.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2012 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.io.File;
+
+public class SaveVideoFileInfo {
+ public File mFile = null;
+ public String mFileName = null;
+ // This the full directory path.
+ public File mDirectory = null;
+ // This is just the folder's name.
+ public String mFolderName = null;
+
+}
diff --git a/src/com/android/gallery3d/util/SaveVideoFileUtils.java b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
new file mode 100644
index 000000000..10c41de90
--- /dev/null
+++ b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2012 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.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore.Video;
+import android.provider.MediaStore.Video.VideoColumns;
+
+import com.android.gallery3d.filtershow.tools.SaveImage.ContentResolverQueryCallback;
+
+import java.io.File;
+import java.sql.Date;
+import java.text.SimpleDateFormat;
+
+public class SaveVideoFileUtils {
+ // This function can decide which folder to save the video file, and generate
+ // the needed information for the video file including filename.
+ public static SaveVideoFileInfo getDstMp4FileInfo(String fileNameFormat,
+ ContentResolver contentResolver, Uri uri, String defaultFolderName) {
+ SaveVideoFileInfo dstFileInfo = new SaveVideoFileInfo();
+ // Use the default save directory if the source directory cannot be
+ // saved.
+ dstFileInfo.mDirectory = getSaveDirectory(contentResolver, uri);
+ if ((dstFileInfo.mDirectory == null) || !dstFileInfo.mDirectory.canWrite()) {
+ dstFileInfo.mDirectory = new File(Environment.getExternalStorageDirectory(),
+ BucketNames.DOWNLOAD);
+ dstFileInfo.mFolderName = defaultFolderName;
+ } else {
+ dstFileInfo.mFolderName = dstFileInfo.mDirectory.getName();
+ }
+ dstFileInfo.mFileName = new SimpleDateFormat(fileNameFormat).format(
+ new Date(System.currentTimeMillis()));
+
+ dstFileInfo.mFile = new File(dstFileInfo.mDirectory, dstFileInfo.mFileName + ".mp4");
+ return dstFileInfo;
+ }
+
+ private static void querySource(ContentResolver contentResolver, Uri uri,
+ String[] projection, ContentResolverQueryCallback callback) {
+ Cursor cursor = null;
+ try {
+ cursor = contentResolver.query(uri, projection, null, null, null);
+ if ((cursor != null) && cursor.moveToNext()) {
+ callback.onCursorResult(cursor);
+ }
+ } catch (Exception e) {
+ // Ignore error for lacking the data column from the source.
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ private static File getSaveDirectory(ContentResolver contentResolver, Uri uri) {
+ final File[] dir = new File[1];
+ querySource(contentResolver, uri,
+ new String[] { VideoColumns.DATA },
+ new ContentResolverQueryCallback() {
+ @Override
+ public void onCursorResult(Cursor cursor) {
+ dir[0] = new File(cursor.getString(0)).getParentFile();
+ }
+ });
+ return dir[0];
+ }
+
+
+ /**
+ * Insert the content (saved file) with proper video properties.
+ */
+ public static Uri insertContent(SaveVideoFileInfo mDstFileInfo,
+ ContentResolver contentResolver, Uri uri ) {
+ long nowInMs = System.currentTimeMillis();
+ long nowInSec = nowInMs / 1000;
+ final ContentValues values = new ContentValues(13);
+ values.put(Video.Media.TITLE, mDstFileInfo.mFileName);
+ values.put(Video.Media.DISPLAY_NAME, mDstFileInfo.mFile.getName());
+ values.put(Video.Media.MIME_TYPE, "video/mp4");
+ values.put(Video.Media.DATE_TAKEN, nowInMs);
+ values.put(Video.Media.DATE_MODIFIED, nowInSec);
+ values.put(Video.Media.DATE_ADDED, nowInSec);
+ values.put(Video.Media.DATA, mDstFileInfo.mFile.getAbsolutePath());
+ values.put(Video.Media.SIZE, mDstFileInfo.mFile.length());
+ int durationMs = retriveVideoDurationMs(mDstFileInfo.mFile.getPath());
+ values.put(Video.Media.DURATION, durationMs);
+ // Copy the data taken and location info from src.
+ String[] projection = new String[] {
+ VideoColumns.DATE_TAKEN,
+ VideoColumns.LATITUDE,
+ VideoColumns.LONGITUDE,
+ VideoColumns.RESOLUTION,
+ };
+
+ // Copy some info from the source file.
+ querySource(contentResolver, uri, projection,
+ new ContentResolverQueryCallback() {
+ @Override
+ public void onCursorResult(Cursor cursor) {
+ long timeTaken = cursor.getLong(0);
+ if (timeTaken > 0) {
+ values.put(Video.Media.DATE_TAKEN, timeTaken);
+ }
+ double latitude = cursor.getDouble(1);
+ double longitude = cursor.getDouble(2);
+ // TODO: Change || to && after the default location
+ // issue is
+ // fixed.
+ if ((latitude != 0f) || (longitude != 0f)) {
+ values.put(Video.Media.LATITUDE, latitude);
+ values.put(Video.Media.LONGITUDE, longitude);
+ }
+ values.put(Video.Media.RESOLUTION, cursor.getString(3));
+
+ }
+ });
+
+ return contentResolver.insert(Video.Media.EXTERNAL_CONTENT_URI, values);
+ }
+
+ public static int retriveVideoDurationMs(String path) {
+ int durationMs = 0;
+ // Calculate the duration of the destination file.
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
+ retriever.setDataSource(path);
+ String duration = retriever.extractMetadata(
+ MediaMetadataRetriever.METADATA_KEY_DURATION);
+ if (duration != null) {
+ durationMs = Integer.parseInt(duration);
+ }
+ retriever.release();
+ return durationMs;
+ }
+
+}
diff --git a/src/com/android/gallery3d/util/UpdateHelper.java b/src/com/android/gallery3d/util/UpdateHelper.java
new file mode 100644
index 000000000..f76705d06
--- /dev/null
+++ b/src/com/android/gallery3d/util/UpdateHelper.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.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 <T> 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/photos/AlbumActivity.java b/src/com/android/photos/AlbumActivity.java
new file mode 100644
index 000000000..c616b998b
--- /dev/null
+++ b/src/com/android/photos/AlbumActivity.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class AlbumActivity extends Activity implements MultiChoiceManager.Provider {
+
+ public static final String KEY_ALBUM_URI = AlbumFragment.KEY_ALBUM_URI;
+ public static final String KEY_ALBUM_TITLE = AlbumFragment.KEY_ALBUM_TITLE;
+
+ private MultiChoiceManager mMultiChoiceManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Bundle intentExtras = getIntent().getExtras();
+ mMultiChoiceManager = new MultiChoiceManager(this);
+ if (savedInstanceState == null) {
+ AlbumFragment albumFragment = new AlbumFragment();
+ mMultiChoiceManager.setDelegate(albumFragment);
+ albumFragment.setArguments(intentExtras);
+ getFragmentManager().beginTransaction().add(android.R.id.content,
+ albumFragment).commit();
+ }
+ getActionBar().setTitle(intentExtras.getString(KEY_ALBUM_TITLE));
+ }
+
+ @Override
+ public MultiChoiceManager getMultiChoiceManager() {
+ return mMultiChoiceManager;
+ }
+}
diff --git a/src/com/android/photos/AlbumFragment.java b/src/com/android/photos/AlbumFragment.java
new file mode 100644
index 000000000..406fd2a29
--- /dev/null
+++ b/src/com/android/photos/AlbumFragment.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.Gallery;
+import com.android.photos.adapters.PhotoThumbnailAdapter;
+import com.android.photos.data.PhotoSetLoader;
+import com.android.photos.shims.LoaderCompatShim;
+import com.android.photos.shims.MediaItemsLoader;
+import com.android.photos.views.HeaderGridView;
+
+import java.util.ArrayList;
+
+public class AlbumFragment extends MultiSelectGridFragment implements LoaderCallbacks<Cursor> {
+
+ protected static final String KEY_ALBUM_URI = "AlbumUri";
+ protected static final String KEY_ALBUM_TITLE = "AlbumTitle";
+ private static final int LOADER_ALBUM = 1;
+
+ private LoaderCompatShim<Cursor> mLoaderCompatShim;
+ private PhotoThumbnailAdapter mAdapter;
+ private String mAlbumPath;
+ private String mAlbumTitle;
+ private View mHeaderView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Context context = getActivity();
+ mAdapter = new PhotoThumbnailAdapter(context);
+ Bundle args = getArguments();
+ if (args != null) {
+ mAlbumPath = args.getString(KEY_ALBUM_URI, null);
+ mAlbumTitle = args.getString(KEY_ALBUM_TITLE, null);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ getLoaderManager().initLoader(LOADER_ALBUM, null, this);
+ return inflater.inflate(R.layout.album_content, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ // TODO: Remove once UI stabilizes
+ getGridView().setColumnWidth(MediaItemsLoader.getThumbnailSize());
+ }
+
+ private void updateHeaderView() {
+ if (mHeaderView == null) {
+ mHeaderView = LayoutInflater.from(getActivity())
+ .inflate(R.layout.album_header, getGridView(), false);
+ ((HeaderGridView) getGridView()).addHeaderView(mHeaderView, null, false);
+
+ // TODO remove this when the data model stabilizes
+ mHeaderView.setMinimumHeight(200);
+ }
+ ImageView iv = (ImageView) mHeaderView.findViewById(R.id.album_header_image);
+ TextView title = (TextView) mHeaderView.findViewById(R.id.album_header_title);
+ TextView subtitle = (TextView) mHeaderView.findViewById(R.id.album_header_subtitle);
+ title.setText(mAlbumTitle);
+ int count = mAdapter.getCount();
+ subtitle.setText(getActivity().getResources().getQuantityString(
+ R.plurals.number_of_photos, count, count));
+ if (count > 0) {
+ iv.setImageDrawable(mLoaderCompatShim.drawableForItem(mAdapter.getItem(0), null));
+ }
+ }
+
+ @Override
+ public void onGridItemClick(GridView g, View v, int position, long id) {
+ if (mLoaderCompatShim == null) {
+ // Not fully initialized yet, discard
+ return;
+ }
+ Cursor item = (Cursor) getItemAtPosition(position);
+ Uri uri = mLoaderCompatShim.uriForItem(item);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.setClass(getActivity(), Gallery.class);
+ startActivity(intent);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ // TODO: Switch to PhotoSetLoader
+ MediaItemsLoader loader = new MediaItemsLoader(getActivity(), mAlbumPath);
+ mLoaderCompatShim = loader;
+ mAdapter.setDrawableFactory(mLoaderCompatShim);
+ return loader;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader,
+ Cursor data) {
+ mAdapter.swapCursor(data);
+ updateHeaderView();
+ setAdapter(mAdapter);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ }
+
+ @Override
+ public int getItemMediaType(Object item) {
+ return ((Cursor) item).getInt(PhotoSetLoader.INDEX_MEDIA_TYPE);
+ }
+
+ @Override
+ public int getItemSupportedOperations(Object item) {
+ return ((Cursor) item).getInt(PhotoSetLoader.INDEX_SUPPORTED_OPERATIONS);
+ }
+
+ private ArrayList<Uri> mSubItemUriTemp = new ArrayList<Uri>(1);
+ @Override
+ public ArrayList<Uri> getSubItemUrisForItem(Object item) {
+ mSubItemUriTemp.clear();
+ mSubItemUriTemp.add(mLoaderCompatShim.uriForItem((Cursor) item));
+ return mSubItemUriTemp;
+ }
+
+ @Override
+ public void deleteItemWithPath(Object itemPath) {
+ mLoaderCompatShim.deleteItemWithPath(itemPath);
+ }
+
+ @Override
+ public Uri getItemUri(Object item) {
+ return mLoaderCompatShim.uriForItem((Cursor) item);
+ }
+
+ @Override
+ public Object getPathForItem(Object item) {
+ return mLoaderCompatShim.getPathForItem((Cursor) item);
+ }
+}
diff --git a/src/com/android/photos/AlbumSetFragment.java b/src/com/android/photos/AlbumSetFragment.java
new file mode 100644
index 000000000..bc5289ee1
--- /dev/null
+++ b/src/com/android/photos/AlbumSetFragment.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.MediaStore.Files.FileColumns;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.GridView;
+
+import com.android.gallery3d.R;
+import com.android.photos.adapters.AlbumSetCursorAdapter;
+import com.android.photos.data.AlbumSetLoader;
+import com.android.photos.shims.LoaderCompatShim;
+import com.android.photos.shims.MediaSetLoader;
+
+import java.util.ArrayList;
+
+
+public class AlbumSetFragment extends MultiSelectGridFragment implements LoaderCallbacks<Cursor> {
+
+ private AlbumSetCursorAdapter mAdapter;
+ private LoaderCompatShim<Cursor> mLoaderCompatShim;
+
+ private static final int LOADER_ALBUMSET = 1;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Context context = getActivity();
+ mAdapter = new AlbumSetCursorAdapter(context);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View root = super.onCreateView(inflater, container, savedInstanceState);
+ getLoaderManager().initLoader(LOADER_ALBUMSET, null, this);
+ return root;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getGridView().setColumnWidth(getActivity().getResources()
+ .getDimensionPixelSize(R.dimen.album_set_item_width));
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ // TODO: Switch to AlbumSetLoader
+ MediaSetLoader loader = new MediaSetLoader(getActivity());
+ mAdapter.setDrawableFactory(loader);
+ mLoaderCompatShim = loader;
+ return loader;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader,
+ Cursor data) {
+ mAdapter.swapCursor(data);
+ setAdapter(mAdapter);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ }
+
+ @Override
+ public void onGridItemClick(GridView g, View v, int position, long id) {
+ if (mLoaderCompatShim == null) {
+ // Not fully initialized yet, discard
+ return;
+ }
+ Cursor item = (Cursor) getItemAtPosition(position);
+ Context context = getActivity();
+ Intent intent = new Intent(context, AlbumActivity.class);
+ intent.putExtra(AlbumActivity.KEY_ALBUM_URI,
+ mLoaderCompatShim.getPathForItem(item).toString());
+ intent.putExtra(AlbumActivity.KEY_ALBUM_TITLE,
+ item.getString(AlbumSetLoader.INDEX_TITLE));
+ context.startActivity(intent);
+ }
+
+ @Override
+ public int getItemMediaType(Object item) {
+ return FileColumns.MEDIA_TYPE_NONE;
+ }
+
+ @Override
+ public int getItemSupportedOperations(Object item) {
+ return ((Cursor) item).getInt(AlbumSetLoader.INDEX_SUPPORTED_OPERATIONS);
+ }
+
+ @Override
+ public ArrayList<Uri> getSubItemUrisForItem(Object item) {
+ return mLoaderCompatShim.urisForSubItems((Cursor) item);
+ }
+
+ @Override
+ public void deleteItemWithPath(Object itemPath) {
+ mLoaderCompatShim.deleteItemWithPath(itemPath);
+ }
+
+ @Override
+ public Uri getItemUri(Object item) {
+ return mLoaderCompatShim.uriForItem((Cursor) item);
+ }
+
+ @Override
+ public Object getPathForItem(Object item) {
+ return mLoaderCompatShim.getPathForItem((Cursor) item);
+ }
+}
diff --git a/src/com/android/photos/BitmapRegionTileSource.java b/src/com/android/photos/BitmapRegionTileSource.java
new file mode 100644
index 000000000..d7d52f67a
--- /dev/null
+++ b/src/com/android/photos/BitmapRegionTileSource.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.content.Context;
+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;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.util.Log;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.photos.views.TiledImageRenderer;
+
+import java.io.IOException;
+
+/**
+ * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
+ * {@link BitmapRegionDecoder} to wrap a local file
+ */
+public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
+
+ private static final String TAG = "BitmapRegionTileSource";
+
+ private static final boolean REUSE_BITMAP =
+ Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
+ private static final int MAX_PREVIEW_SIZE = 1024;
+
+ BitmapRegionDecoder mDecoder;
+ int mWidth;
+ int mHeight;
+ int mTileSize;
+ private BasicTexture mPreview;
+ private final int mRotation;
+
+ // For use only by getTile
+ private Rect mWantRegion = new Rect();
+ private Rect mOverlapRegion = new Rect();
+ private BitmapFactory.Options mOptions;
+ private Canvas mCanvas;
+
+ public BitmapRegionTileSource(Context context, String path, int previewSize, int rotation) {
+ mTileSize = TiledImageRenderer.suggestedTileSize(context);
+ mRotation = rotation;
+ try {
+ mDecoder = BitmapRegionDecoder.newInstance(path, true);
+ mWidth = mDecoder.getWidth();
+ mHeight = mDecoder.getHeight();
+ } catch (IOException e) {
+ Log.w("BitmapRegionTileSource", "ctor failed", e);
+ }
+ mOptions = new BitmapFactory.Options();
+ mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ mOptions.inPreferQualityOverSpeed = true;
+ mOptions.inTempStorage = new byte[16 * 1024];
+ if (previewSize != 0) {
+ previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
+ // Although this is the same size as the Bitmap that is likely already
+ // loaded, the lifecycle is different and interactions are on a different
+ // thread. Thus to simplify, this source will decode its own bitmap.
+ int sampleSize = (int) Math.ceil(Math.max(
+ mWidth / (float) previewSize, mHeight / (float) previewSize));
+ mOptions.inSampleSize = Math.max(sampleSize, 1);
+ Bitmap preview = mDecoder.decodeRegion(
+ new Rect(0, 0, mWidth, mHeight), mOptions);
+ if (preview.getWidth() <= MAX_PREVIEW_SIZE && preview.getHeight() <= MAX_PREVIEW_SIZE) {
+ mPreview = new BitmapTexture(preview);
+ } else {
+ Log.w(TAG, String.format(
+ "Failed to create preview of apropriate size! "
+ + " in: %dx%d, sample: %d, out: %dx%d",
+ mWidth, mHeight, sampleSize,
+ preview.getWidth(), preview.getHeight()));
+ }
+ }
+ }
+
+ @Override
+ public int getTileSize() {
+ return mTileSize;
+ }
+
+ @Override
+ public int getImageWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getImageHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public BasicTexture getPreview() {
+ return mPreview;
+ }
+
+ @Override
+ public int getRotation() {
+ return mRotation;
+ }
+
+ @Override
+ public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
+ int tileSize = getTileSize();
+ if (!REUSE_BITMAP) {
+ return getTileWithoutReusingBitmap(level, x, y, tileSize);
+ }
+
+ int t = tileSize << level;
+ mWantRegion.set(x, y, x + t, y + t);
+
+ if (bitmap == null) {
+ bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888);
+ }
+
+ mOptions.inSampleSize = (1 << level);
+ mOptions.inBitmap = bitmap;
+
+ try {
+ bitmap = mDecoder.decodeRegion(mWantRegion, mOptions);
+ } finally {
+ if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) {
+ mOptions.inBitmap = null;
+ }
+ }
+
+ if (bitmap == null) {
+ Log.w("BitmapRegionTileSource", "fail in decoding region");
+ }
+ return bitmap;
+ }
+
+ private Bitmap getTileWithoutReusingBitmap(
+ int level, int x, int y, int tileSize) {
+
+ int t = tileSize << level;
+ mWantRegion.set(x, y, x + t, y + t);
+
+ mOverlapRegion.set(0, 0, mWidth, mHeight);
+
+ mOptions.inSampleSize = (1 << level);
+ Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
+
+ if (bitmap == null) {
+ Log.w(TAG, "fail in decoding region");
+ }
+
+ if (mWantRegion.equals(mOverlapRegion)) {
+ return bitmap;
+ }
+
+ Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
+ if (mCanvas == null) {
+ mCanvas = new Canvas();
+ }
+ mCanvas.setBitmap(result);
+ mCanvas.drawBitmap(bitmap,
+ (mOverlapRegion.left - mWantRegion.left) >> level,
+ (mOverlapRegion.top - mWantRegion.top) >> level, null);
+ mCanvas.setBitmap(null);
+ return result;
+ }
+}
diff --git a/src/com/android/photos/FullscreenViewer.java b/src/com/android/photos/FullscreenViewer.java
new file mode 100644
index 000000000..a3761395e
--- /dev/null
+++ b/src/com/android/photos/FullscreenViewer.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.Activity;
+import android.os.Bundle;
+import com.android.photos.views.TiledImageView;
+
+
+public class FullscreenViewer extends Activity {
+
+ private TiledImageView mTextureView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ String path = getIntent().getData().toString();
+ mTextureView = new TiledImageView(this);
+ mTextureView.setTileSource(new BitmapRegionTileSource(this, path, 0, 0), null);
+ setContentView(mTextureView);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mTextureView.destroy();
+ }
+
+}
diff --git a/src/com/android/photos/GalleryActivity.java b/src/com/android/photos/GalleryActivity.java
new file mode 100644
index 000000000..710767d77
--- /dev/null
+++ b/src/com/android/photos/GalleryActivity.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.ActionBar;
+import android.app.ActionBar.Tab;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.ViewGroup;
+
+import com.android.camera.CameraActivity;
+import com.android.gallery3d.R;
+
+import java.util.ArrayList;
+
+public class GalleryActivity extends Activity implements MultiChoiceManager.Provider {
+
+ private MultiChoiceManager mMultiChoiceManager;
+ private ViewPager mViewPager;
+ private TabsAdapter mTabsAdapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mMultiChoiceManager = new MultiChoiceManager(this);
+ mViewPager = new ViewPager(this);
+ mViewPager.setId(R.id.viewpager);
+ setContentView(mViewPager);
+
+ ActionBar ab = getActionBar();
+ ab.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ ab.setDisplayShowHomeEnabled(false);
+ ab.setDisplayShowTitleEnabled(false);
+
+ mTabsAdapter = new TabsAdapter(this, mViewPager);
+ mTabsAdapter.addTab(ab.newTab().setText(R.string.tab_photos),
+ PhotoSetFragment.class, null);
+ mTabsAdapter.addTab(ab.newTab().setText(R.string.tab_albums),
+ AlbumSetFragment.class, null);
+
+ if (savedInstanceState != null) {
+ ab.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt("tab", getActionBar().getSelectedNavigationIndex());
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.gallery, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_camera:
+ Intent intent = new Intent(this, CameraActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ public static class TabsAdapter extends FragmentPagerAdapter implements
+ ActionBar.TabListener, ViewPager.OnPageChangeListener {
+
+ private final GalleryActivity mActivity;
+ private final ActionBar mActionBar;
+ private final ViewPager mViewPager;
+ private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
+
+ static final class TabInfo {
+
+ private final Class<?> clss;
+ private final Bundle args;
+
+ TabInfo(Class<?> _class, Bundle _args) {
+ clss = _class;
+ args = _args;
+ }
+ }
+
+ public TabsAdapter(GalleryActivity activity, ViewPager pager) {
+ super(activity.getFragmentManager());
+ mActivity = activity;
+ mActionBar = activity.getActionBar();
+ mViewPager = pager;
+ mViewPager.setAdapter(this);
+ mViewPager.setOnPageChangeListener(this);
+ }
+
+ public void addTab(ActionBar.Tab tab, Class<?> clss, Bundle args) {
+ TabInfo info = new TabInfo(clss, args);
+ tab.setTag(info);
+ tab.setTabListener(this);
+ mTabs.add(info);
+ mActionBar.addTab(tab);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return mTabs.size();
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ TabInfo info = mTabs.get(position);
+ return Fragment.instantiate(mActivity, info.clss.getName(),
+ info.args);
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset,
+ int positionOffsetPixels) {
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ mActionBar.setSelectedNavigationItem(position);
+ }
+
+ @Override
+ public void setPrimaryItem(ViewGroup container, int position, Object object) {
+ super.setPrimaryItem(container, position, object);
+ mActivity.mMultiChoiceManager.setDelegate((MultiChoiceManager.Delegate) object);
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ }
+
+ @Override
+ public void onTabSelected(Tab tab, FragmentTransaction ft) {
+ Object tag = tab.getTag();
+ for (int i = 0; i < mTabs.size(); i++) {
+ if (mTabs.get(i) == tag) {
+ mViewPager.setCurrentItem(i);
+ }
+ }
+ }
+
+ @Override
+ public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+ }
+
+ @Override
+ public void onTabReselected(Tab tab, FragmentTransaction ft) {
+ }
+ }
+
+ @Override
+ public MultiChoiceManager getMultiChoiceManager() {
+ return mMultiChoiceManager;
+ }
+}
diff --git a/src/com/android/photos/MultiChoiceManager.java b/src/com/android/photos/MultiChoiceManager.java
new file mode 100644
index 000000000..49519ca63
--- /dev/null
+++ b/src/com/android/photos/MultiChoiceManager.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.MediaStore.Files.FileColumns;
+import android.util.SparseBooleanArray;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.AbsListView.MultiChoiceModeListener;
+import android.widget.ShareActionProvider;
+import android.widget.ShareActionProvider.OnShareTargetSelectedListener;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.app.TrimVideo;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.crop.CropActivity;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MultiChoiceManager implements MultiChoiceModeListener,
+ OnShareTargetSelectedListener, SelectionManager.SelectedUriSource {
+
+ public interface Provider {
+ public MultiChoiceManager getMultiChoiceManager();
+ }
+
+ public interface Delegate {
+ public SparseBooleanArray getSelectedItemPositions();
+ public int getSelectedItemCount();
+ public int getItemMediaType(Object item);
+ public int getItemSupportedOperations(Object item);
+ public ArrayList<Uri> getSubItemUrisForItem(Object item);
+ public Uri getItemUri(Object item);
+ public Object getItemAtPosition(int position);
+ public Object getPathForItemAtPosition(int position);
+ public void deleteItemWithPath(Object itemPath);
+ }
+
+ private SelectionManager mSelectionManager;
+ private ShareActionProvider mShareActionProvider;
+ private ActionMode mActionMode;
+ private Context mContext;
+ private Delegate mDelegate;
+
+ private ArrayList<Uri> mSelectedShareableUrisArray = new ArrayList<Uri>();
+
+ public MultiChoiceManager(Activity activity) {
+ mContext = activity;
+ mSelectionManager = new SelectionManager(activity);
+ }
+
+ public void setDelegate(Delegate delegate) {
+ if (mDelegate == delegate) {
+ return;
+ }
+ if (mActionMode != null) {
+ mActionMode.finish();
+ }
+ mDelegate = delegate;
+ }
+
+ @Override
+ public ArrayList<Uri> getSelectedShareableUris() {
+ return mSelectedShareableUrisArray;
+ }
+
+ private void updateSelectedTitle(ActionMode mode) {
+ int count = mDelegate.getSelectedItemCount();
+ mode.setTitle(mContext.getResources().getQuantityString(
+ R.plurals.number_of_items_selected, count, count));
+ }
+
+ private String getItemMimetype(Object item) {
+ int type = mDelegate.getItemMediaType(item);
+ if (type == FileColumns.MEDIA_TYPE_IMAGE) {
+ return GalleryUtils.MIME_TYPE_IMAGE;
+ } else if (type == FileColumns.MEDIA_TYPE_VIDEO) {
+ return GalleryUtils.MIME_TYPE_VIDEO;
+ } else {
+ return GalleryUtils.MIME_TYPE_ALL;
+ }
+ }
+
+ @Override
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
+ boolean checked) {
+ updateSelectedTitle(mode);
+ Object item = mDelegate.getItemAtPosition(position);
+
+ int supported = mDelegate.getItemSupportedOperations(item);
+
+ if ((supported & MediaObject.SUPPORT_SHARE) > 0) {
+ ArrayList<Uri> subItems = mDelegate.getSubItemUrisForItem(item);
+ if (checked) {
+ mSelectedShareableUrisArray.addAll(subItems);
+ } else {
+ mSelectedShareableUrisArray.removeAll(subItems);
+ }
+ }
+
+ mSelectionManager.onItemSelectedStateChanged(mShareActionProvider,
+ mDelegate.getItemMediaType(item),
+ supported,
+ checked);
+ updateActionItemVisibilities(mode.getMenu(),
+ mSelectionManager.getSupportedOperations());
+ }
+
+ private void updateActionItemVisibilities(Menu menu, int supportedOperations) {
+ MenuItem editItem = menu.findItem(R.id.menu_edit);
+ MenuItem deleteItem = menu.findItem(R.id.menu_delete);
+ MenuItem shareItem = menu.findItem(R.id.menu_share);
+ MenuItem cropItem = menu.findItem(R.id.menu_crop);
+ MenuItem trimItem = menu.findItem(R.id.menu_trim);
+ MenuItem muteItem = menu.findItem(R.id.menu_mute);
+ MenuItem setAsItem = menu.findItem(R.id.menu_set_as);
+
+ editItem.setVisible((supportedOperations & MediaObject.SUPPORT_EDIT) > 0);
+ deleteItem.setVisible((supportedOperations & MediaObject.SUPPORT_DELETE) > 0);
+ shareItem.setVisible((supportedOperations & MediaObject.SUPPORT_SHARE) > 0);
+ cropItem.setVisible((supportedOperations & MediaObject.SUPPORT_CROP) > 0);
+ trimItem.setVisible((supportedOperations & MediaObject.SUPPORT_TRIM) > 0);
+ muteItem.setVisible((supportedOperations & MediaObject.SUPPORT_MUTE) > 0);
+ setAsItem.setVisible((supportedOperations & MediaObject.SUPPORT_SETAS) > 0);
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ mSelectionManager.setSelectedUriSource(this);
+ mActionMode = mode;
+ MenuInflater inflater = mode.getMenuInflater();
+ inflater.inflate(R.menu.gallery_multiselect, menu);
+ MenuItem menuItem = menu.findItem(R.id.menu_share);
+ mShareActionProvider = (ShareActionProvider) menuItem.getActionProvider();
+ mShareActionProvider.setOnShareTargetSelectedListener(this);
+ updateSelectedTitle(mode);
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ // onDestroyActionMode gets called when the share target was selected,
+ // but apparently before the ArrayList is serialized in the intent
+ // so we can't clear the old one here.
+ mSelectedShareableUrisArray = new ArrayList<Uri>();
+ mSelectionManager.onClearSelection();
+ mSelectionManager.setSelectedUriSource(null);
+ mShareActionProvider = null;
+ mActionMode = null;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ updateSelectedTitle(mode);
+ return false;
+ }
+
+ @Override
+ public boolean onShareTargetSelected(ShareActionProvider provider, Intent intent) {
+ mActionMode.finish();
+ return false;
+ }
+
+ private static class BulkDeleteTask extends AsyncTask<Void, Void, Void> {
+ private Delegate mDelegate;
+ private List<Object> mPaths;
+
+ public BulkDeleteTask(Delegate delegate, List<Object> paths) {
+ mDelegate = delegate;
+ mPaths = paths;
+ }
+
+ @Override
+ protected Void doInBackground(Void... ignored) {
+ for (Object path : mPaths) {
+ mDelegate.deleteItemWithPath(path);
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ int actionItemId = item.getItemId();
+ switch (actionItemId) {
+ case R.id.menu_delete:
+ BulkDeleteTask deleteTask = new BulkDeleteTask(mDelegate,
+ getPathsForSelectedItems());
+ deleteTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ mode.finish();
+ return true;
+ case R.id.menu_edit:
+ case R.id.menu_crop:
+ case R.id.menu_trim:
+ case R.id.menu_mute:
+ case R.id.menu_set_as:
+ singleItemAction(getSelectedItem(), actionItemId);
+ mode.finish();
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void singleItemAction(Object item, int actionItemId) {
+ Intent intent = new Intent();
+ String mime = getItemMimetype(item);
+ Uri uri = mDelegate.getItemUri(item);
+ switch (actionItemId) {
+ case R.id.menu_edit:
+ intent.setDataAndType(uri, mime)
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ .setAction(Intent.ACTION_EDIT);
+ mContext.startActivity(Intent.createChooser(intent, null));
+ return;
+ case R.id.menu_crop:
+ intent.setDataAndType(uri, mime)
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ .setAction(CropActivity.CROP_ACTION)
+ .setClass(mContext, FilterShowActivity.class);
+ mContext.startActivity(intent);
+ return;
+ case R.id.menu_trim:
+ intent.setData(uri)
+ .setClass(mContext, TrimVideo.class);
+ mContext.startActivity(intent);
+ return;
+ case R.id.menu_mute:
+ /* TODO need a way to get the file path of an item
+ MuteVideo muteVideo = new MuteVideo(filePath,
+ uri, (Activity) mContext);
+ muteVideo.muteInBackground();
+ */
+ return;
+ case R.id.menu_set_as:
+ intent.setDataAndType(uri, mime)
+ .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ .setAction(Intent.ACTION_ATTACH_DATA)
+ .putExtra("mimeType", mime);
+ mContext.startActivity(Intent.createChooser(
+ intent, mContext.getString(R.string.set_as)));
+ return;
+ default:
+ return;
+ }
+ }
+
+ private List<Object> getPathsForSelectedItems() {
+ List<Object> paths = new ArrayList<Object>();
+ SparseBooleanArray selected = mDelegate.getSelectedItemPositions();
+ for (int i = 0; i < selected.size(); i++) {
+ if (selected.valueAt(i)) {
+ paths.add(mDelegate.getPathForItemAtPosition(i));
+ }
+ }
+ return paths;
+ }
+
+ public Object getSelectedItem() {
+ if (mDelegate.getSelectedItemCount() != 1) {
+ return null;
+ }
+ SparseBooleanArray selected = mDelegate.getSelectedItemPositions();
+ for (int i = 0; i < selected.size(); i++) {
+ if (selected.valueAt(i)) {
+ return mDelegate.getItemAtPosition(selected.keyAt(i));
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/photos/MultiSelectGridFragment.java b/src/com/android/photos/MultiSelectGridFragment.java
new file mode 100644
index 000000000..dda9fe443
--- /dev/null
+++ b/src/com/android/photos/MultiSelectGridFragment.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.SparseBooleanArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.widget.AdapterView;
+import android.widget.GridView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+
+public abstract class MultiSelectGridFragment extends Fragment
+ implements MultiChoiceManager.Delegate, AdapterView.OnItemClickListener {
+
+ final private Handler mHandler = new Handler();
+
+ final private Runnable mRequestFocus = new Runnable() {
+ @Override
+ public void run() {
+ mGrid.focusableViewAvailable(mGrid);
+ }
+ };
+
+ ListAdapter mAdapter;
+ GridView mGrid;
+ TextView mEmptyView;
+ View mProgressContainer;
+ View mGridContainer;
+ CharSequence mEmptyText;
+ boolean mGridShown;
+ MultiChoiceManager.Provider mHost;
+
+ public MultiSelectGridFragment() {
+ }
+
+ /**
+ * Provide default implementation to return a simple grid view. Subclasses
+ * can override to replace with their own layout. If doing so, the returned
+ * view hierarchy <em>must</em> have a GridView whose id is
+ * {@link android.R.id#grid android.R.id.list} and can optionally have a
+ * sibling text view id {@link android.R.id#empty android.R.id.empty} that
+ * is to be shown when the grid is empty.
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.multigrid_content, container, false);
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ mHost = (MultiChoiceManager.Provider) activity;
+ if (mGrid != null) {
+ mGrid.setMultiChoiceModeListener(mHost.getMultiChoiceManager());
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mHost = null;
+ }
+
+ /**
+ * Attach to grid view once the view hierarchy has been created.
+ */
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ensureGrid();
+ }
+
+ /**
+ * Detach from grid view.
+ */
+ @Override
+ public void onDestroyView() {
+ mHandler.removeCallbacks(mRequestFocus);
+ mGrid = null;
+ mGridShown = false;
+ mEmptyView = null;
+ mProgressContainer = mGridContainer = null;
+ super.onDestroyView();
+ }
+
+ /**
+ * This method will be called when an item in the grid is selected.
+ * Subclasses should override. Subclasses can call
+ * getGridView().getItemAtPosition(position) if they need to access the data
+ * associated with the selected item.
+ *
+ * @param g The GridView where the click happened
+ * @param v The view that was clicked within the GridView
+ * @param position The position of the view in the grid
+ * @param id The id of the item that was clicked
+ */
+ public void onGridItemClick(GridView g, View v, int position, long id) {
+ }
+
+ /**
+ * Provide the cursor for the grid view.
+ */
+ public void setAdapter(ListAdapter adapter) {
+ boolean hadAdapter = mAdapter != null;
+ mAdapter = adapter;
+ if (mGrid != null) {
+ mGrid.setAdapter(adapter);
+ if (!mGridShown && !hadAdapter) {
+ // The grid was hidden, and previously didn't have an
+ // adapter. It is now time to show it.
+ setGridShown(true, getView().getWindowToken() != null);
+ }
+ }
+ }
+
+ /**
+ * Set the currently selected grid item to the specified position with the
+ * adapter's data
+ *
+ * @param position
+ */
+ public void setSelection(int position) {
+ ensureGrid();
+ mGrid.setSelection(position);
+ }
+
+ /**
+ * Get the position of the currently selected grid item.
+ */
+ public int getSelectedItemPosition() {
+ ensureGrid();
+ return mGrid.getSelectedItemPosition();
+ }
+
+ /**
+ * Get the cursor row ID of the currently selected grid item.
+ */
+ public long getSelectedItemId() {
+ ensureGrid();
+ return mGrid.getSelectedItemId();
+ }
+
+ /**
+ * Get the activity's grid view widget.
+ */
+ public GridView getGridView() {
+ ensureGrid();
+ return mGrid;
+ }
+
+ /**
+ * The default content for a MultiSelectGridFragment has a TextView that can
+ * be shown when the grid is empty. If you would like to have it shown, call
+ * this method to supply the text it should use.
+ */
+ public void setEmptyText(CharSequence text) {
+ ensureGrid();
+ if (mEmptyView == null) {
+ return;
+ }
+ mEmptyView.setText(text);
+ if (mEmptyText == null) {
+ mGrid.setEmptyView(mEmptyView);
+ }
+ mEmptyText = text;
+ }
+
+ /**
+ * Control whether the grid is being displayed. You can make it not
+ * displayed if you are waiting for the initial data to show in it. During
+ * this time an indeterminate progress indicator will be shown instead.
+ * <p>
+ * Applications do not normally need to use this themselves. The default
+ * behavior of MultiSelectGridFragment is to start with the grid not being
+ * shown, only showing it once an adapter is given with
+ * {@link #setAdapter(ListAdapter)}. If the grid at that point had not been
+ * shown, when it does get shown it will be do without the user ever seeing
+ * the hidden state.
+ *
+ * @param shown If true, the grid view is shown; if false, the progress
+ * indicator. The initial value is true.
+ */
+ public void setGridShown(boolean shown) {
+ setGridShown(shown, true);
+ }
+
+ /**
+ * Like {@link #setGridShown(boolean)}, but no animation is used when
+ * transitioning from the previous state.
+ */
+ public void setGridShownNoAnimation(boolean shown) {
+ setGridShown(shown, false);
+ }
+
+ /**
+ * Control whether the grid is being displayed. You can make it not
+ * displayed if you are waiting for the initial data to show in it. During
+ * this time an indeterminate progress indicator will be shown instead.
+ *
+ * @param shown If true, the grid view is shown; if false, the progress
+ * indicator. The initial value is true.
+ * @param animate If true, an animation will be used to transition to the
+ * new state.
+ */
+ private void setGridShown(boolean shown, boolean animate) {
+ ensureGrid();
+ if (mProgressContainer == null) {
+ throw new IllegalStateException("Can't be used with a custom content view");
+ }
+ if (mGridShown == shown) {
+ return;
+ }
+ mGridShown = shown;
+ if (shown) {
+ if (animate) {
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+ getActivity(), android.R.anim.fade_out));
+ mGridContainer.startAnimation(AnimationUtils.loadAnimation(
+ getActivity(), android.R.anim.fade_in));
+ } else {
+ mProgressContainer.clearAnimation();
+ mGridContainer.clearAnimation();
+ }
+ mProgressContainer.setVisibility(View.GONE);
+ mGridContainer.setVisibility(View.VISIBLE);
+ } else {
+ if (animate) {
+ mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
+ getActivity(), android.R.anim.fade_in));
+ mGridContainer.startAnimation(AnimationUtils.loadAnimation(
+ getActivity(), android.R.anim.fade_out));
+ } else {
+ mProgressContainer.clearAnimation();
+ mGridContainer.clearAnimation();
+ }
+ mProgressContainer.setVisibility(View.VISIBLE);
+ mGridContainer.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Get the ListAdapter associated with this activity's GridView.
+ */
+ public ListAdapter getAdapter() {
+ return mGrid.getAdapter();
+ }
+
+ private void ensureGrid() {
+ if (mGrid != null) {
+ return;
+ }
+ View root = getView();
+ if (root == null) {
+ throw new IllegalStateException("Content view not yet created");
+ }
+ if (root instanceof GridView) {
+ mGrid = (GridView) root;
+ } else {
+ View empty = root.findViewById(android.R.id.empty);
+ if (empty != null && empty instanceof TextView) {
+ mEmptyView = (TextView) empty;
+ }
+ mProgressContainer = root.findViewById(R.id.progressContainer);
+ mGridContainer = root.findViewById(R.id.gridContainer);
+ View rawGridView = root.findViewById(android.R.id.list);
+ if (!(rawGridView instanceof GridView)) {
+ throw new RuntimeException(
+ "Content has view with id attribute 'android.R.id.list' "
+ + "that is not a GridView class");
+ }
+ mGrid = (GridView) rawGridView;
+ if (mGrid == null) {
+ throw new RuntimeException(
+ "Your content must have a GridView whose id attribute is " +
+ "'android.R.id.list'");
+ }
+ if (mEmptyView != null) {
+ mGrid.setEmptyView(mEmptyView);
+ }
+ }
+ mGridShown = true;
+ mGrid.setOnItemClickListener(this);
+ mGrid.setMultiChoiceModeListener(mHost.getMultiChoiceManager());
+ if (mAdapter != null) {
+ ListAdapter adapter = mAdapter;
+ mAdapter = null;
+ setAdapter(adapter);
+ } else {
+ // We are starting without an adapter, so assume we won't
+ // have our data right away and start with the progress indicator.
+ if (mProgressContainer != null) {
+ setGridShown(false, false);
+ }
+ }
+ mHandler.post(mRequestFocus);
+ }
+
+ @Override
+ public Object getItemAtPosition(int position) {
+ return getAdapter().getItem(position);
+ }
+
+ @Override
+ public Object getPathForItemAtPosition(int position) {
+ return getPathForItem(getItemAtPosition(position));
+ }
+
+ @Override
+ public SparseBooleanArray getSelectedItemPositions() {
+ return mGrid.getCheckedItemPositions();
+ }
+
+ @Override
+ public int getSelectedItemCount() {
+ return mGrid.getCheckedItemCount();
+ }
+
+ public abstract Object getPathForItem(Object item);
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+ onGridItemClick((GridView) parent, v, position, id);
+ }
+}
diff --git a/src/com/android/photos/PhotoFragment.java b/src/com/android/photos/PhotoFragment.java
new file mode 100644
index 000000000..3be6313f2
--- /dev/null
+++ b/src/com/android/photos/PhotoFragment.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.Fragment;
+
+
+public class PhotoFragment extends Fragment {
+
+}
diff --git a/src/com/android/photos/PhotoSetFragment.java b/src/com/android/photos/PhotoSetFragment.java
new file mode 100644
index 000000000..961fd0bf2
--- /dev/null
+++ b/src/com/android/photos/PhotoSetFragment.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.GridView;
+
+import com.android.gallery3d.app.Gallery;
+import com.android.photos.adapters.PhotoThumbnailAdapter;
+import com.android.photos.data.PhotoSetLoader;
+import com.android.photos.shims.LoaderCompatShim;
+import com.android.photos.shims.MediaItemsLoader;
+
+import java.util.ArrayList;
+
+public class PhotoSetFragment extends MultiSelectGridFragment implements LoaderCallbacks<Cursor> {
+
+ private static final int LOADER_PHOTOSET = 1;
+
+ private LoaderCompatShim<Cursor> mLoaderCompatShim;
+ private PhotoThumbnailAdapter mAdapter;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Context context = getActivity();
+ mAdapter = new PhotoThumbnailAdapter(context);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View root = super.onCreateView(inflater, container, savedInstanceState);
+ getLoaderManager().initLoader(LOADER_PHOTOSET, null, this);
+ return root;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ // TODO: Remove once UI stabilizes
+ getGridView().setColumnWidth(MediaItemsLoader.getThumbnailSize());
+ }
+
+ @Override
+ public void onGridItemClick(GridView g, View v, int position, long id) {
+ if (mLoaderCompatShim == null) {
+ // Not fully initialized yet, discard
+ return;
+ }
+ Cursor item = (Cursor) getItemAtPosition(position);
+ Uri uri = mLoaderCompatShim.uriForItem(item);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.setClass(getActivity(), Gallery.class);
+ startActivity(intent);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ // TODO: Switch to PhotoSetLoader
+ MediaItemsLoader loader = new MediaItemsLoader(getActivity());
+ mLoaderCompatShim = loader;
+ mAdapter.setDrawableFactory(mLoaderCompatShim);
+ return loader;
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader,
+ Cursor data) {
+ mAdapter.swapCursor(data);
+ setAdapter(mAdapter);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ }
+
+ @Override
+ public int getItemMediaType(Object item) {
+ return ((Cursor) item).getInt(PhotoSetLoader.INDEX_MEDIA_TYPE);
+ }
+
+ @Override
+ public int getItemSupportedOperations(Object item) {
+ return ((Cursor) item).getInt(PhotoSetLoader.INDEX_SUPPORTED_OPERATIONS);
+ }
+
+ private ArrayList<Uri> mSubItemUriTemp = new ArrayList<Uri>(1);
+ @Override
+ public ArrayList<Uri> getSubItemUrisForItem(Object item) {
+ mSubItemUriTemp.clear();
+ mSubItemUriTemp.add(mLoaderCompatShim.uriForItem((Cursor) item));
+ return mSubItemUriTemp;
+ }
+
+ @Override
+ public void deleteItemWithPath(Object itemPath) {
+ mLoaderCompatShim.deleteItemWithPath(itemPath);
+ }
+
+ @Override
+ public Uri getItemUri(Object item) {
+ return mLoaderCompatShim.uriForItem((Cursor) item);
+ }
+
+ @Override
+ public Object getPathForItem(Object item) {
+ return mLoaderCompatShim.getPathForItem((Cursor) item);
+ }
+}
diff --git a/src/com/android/photos/SelectionManager.java b/src/com/android/photos/SelectionManager.java
new file mode 100644
index 000000000..9bfb9be75
--- /dev/null
+++ b/src/com/android/photos/SelectionManager.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2013 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.photos;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcAdapter.CreateBeamUrisCallback;
+import android.nfc.NfcEvent;
+import android.provider.MediaStore.Files.FileColumns;
+import android.widget.ShareActionProvider;
+
+import com.android.gallery3d.common.ApiHelper;
+import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.util.GalleryUtils;
+
+import java.util.ArrayList;
+
+public class SelectionManager {
+ private Activity mActivity;
+ private NfcAdapter mNfcAdapter;
+ private SelectedUriSource mUriSource;
+ private Intent mShareIntent = new Intent();
+
+ public interface SelectedUriSource {
+ public ArrayList<Uri> getSelectedShareableUris();
+ }
+
+ public SelectionManager(Activity activity) {
+ mActivity = activity;
+ if (ApiHelper.AT_LEAST_16) {
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(mActivity);
+ mNfcAdapter.setBeamPushUrisCallback(new CreateBeamUrisCallback() {
+ @Override
+ public Uri[] createBeamUris(NfcEvent arg0) {
+ // This will have been preceded by a call to onItemSelectedStateChange
+ if (mCachedShareableUris == null) return null;
+ return mCachedShareableUris.toArray(
+ new Uri[mCachedShareableUris.size()]);
+ }
+ }, mActivity);
+ }
+ }
+
+ public void setSelectedUriSource(SelectedUriSource source) {
+ mUriSource = source;
+ }
+
+ private int mSelectedTotalCount = 0;
+ private int mSelectedShareableCount = 0;
+ private int mSelectedShareableImageCount = 0;
+ private int mSelectedShareableVideoCount = 0;
+ private int mSelectedDeletableCount = 0;
+ private int mSelectedEditableCount = 0;
+ private int mSelectedCroppableCount = 0;
+ private int mSelectedSetableCount = 0;
+ private int mSelectedTrimmableCount = 0;
+ private int mSelectedMuteableCount = 0;
+
+ private ArrayList<Uri> mCachedShareableUris = null;
+
+ public void onItemSelectedStateChanged(ShareActionProvider share,
+ int itemType, int itemSupportedOperations, boolean selected) {
+ int increment = selected ? 1 : -1;
+
+ mSelectedTotalCount += increment;
+ mCachedShareableUris = null;
+
+ if ((itemSupportedOperations & MediaObject.SUPPORT_DELETE) > 0) {
+ mSelectedDeletableCount += increment;
+ }
+ if ((itemSupportedOperations & MediaObject.SUPPORT_EDIT) > 0) {
+ mSelectedEditableCount += increment;
+ }
+ if ((itemSupportedOperations & MediaObject.SUPPORT_CROP) > 0) {
+ mSelectedCroppableCount += increment;
+ }
+ if ((itemSupportedOperations & MediaObject.SUPPORT_SETAS) > 0) {
+ mSelectedSetableCount += increment;
+ }
+ if ((itemSupportedOperations & MediaObject.SUPPORT_TRIM) > 0) {
+ mSelectedTrimmableCount += increment;
+ }
+ if ((itemSupportedOperations & MediaObject.SUPPORT_MUTE) > 0) {
+ mSelectedMuteableCount += increment;
+ }
+ if ((itemSupportedOperations & MediaObject.SUPPORT_SHARE) > 0) {
+ mSelectedShareableCount += increment;
+ if (itemType == FileColumns.MEDIA_TYPE_IMAGE) {
+ mSelectedShareableImageCount += increment;
+ } else if (itemType == FileColumns.MEDIA_TYPE_VIDEO) {
+ mSelectedShareableVideoCount += increment;
+ }
+ }
+
+ mShareIntent.removeExtra(Intent.EXTRA_STREAM);
+ if (mSelectedShareableCount == 0) {
+ mShareIntent.setAction(null).setType(null);
+ } else if (mSelectedShareableCount >= 1) {
+ mCachedShareableUris = mUriSource.getSelectedShareableUris();
+ if (mCachedShareableUris.size() == 0) {
+ mShareIntent.setAction(null).setType(null);
+ } else {
+ if (mSelectedShareableImageCount == mSelectedShareableCount) {
+ mShareIntent.setType(GalleryUtils.MIME_TYPE_IMAGE);
+ } else if (mSelectedShareableVideoCount == mSelectedShareableCount) {
+ mShareIntent.setType(GalleryUtils.MIME_TYPE_VIDEO);
+ } else {
+ mShareIntent.setType(GalleryUtils.MIME_TYPE_ALL);
+ }
+ if (mCachedShareableUris.size() == 1) {
+ mShareIntent.setAction(Intent.ACTION_SEND);
+ mShareIntent.putExtra(Intent.EXTRA_STREAM, mCachedShareableUris.get(0));
+ } else {
+ mShareIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
+ mShareIntent.putExtra(Intent.EXTRA_STREAM, mCachedShareableUris);
+ }
+ }
+ }
+ share.setShareIntent(mShareIntent);
+ }
+
+ public int getSupportedOperations() {
+ if (mSelectedTotalCount == 0) {
+ return 0;
+ }
+ int supported = 0;
+ if (mSelectedTotalCount == 1) {
+ if (mSelectedCroppableCount == 1) {
+ supported |= MediaObject.SUPPORT_CROP;
+ }
+ if (mSelectedEditableCount == 1) {
+ supported |= MediaObject.SUPPORT_EDIT;
+ }
+ if (mSelectedSetableCount == 1) {
+ supported |= MediaObject.SUPPORT_SETAS;
+ }
+ if (mSelectedTrimmableCount == 1) {
+ supported |= MediaObject.SUPPORT_TRIM;
+ }
+ if (mSelectedMuteableCount == 1) {
+ supported |= MediaObject.SUPPORT_MUTE;
+ }
+ }
+ if (mSelectedDeletableCount == mSelectedTotalCount) {
+ supported |= MediaObject.SUPPORT_DELETE;
+ }
+ if (mSelectedShareableCount > 0) {
+ supported |= MediaObject.SUPPORT_SHARE;
+ }
+ return supported;
+ }
+
+ public void onClearSelection() {
+ mSelectedTotalCount = 0;
+ mSelectedShareableCount = 0;
+ mSelectedShareableImageCount = 0;
+ mSelectedShareableVideoCount = 0;
+ mSelectedDeletableCount = 0;
+ mSelectedEditableCount = 0;
+ mSelectedCroppableCount = 0;
+ mSelectedSetableCount = 0;
+ mSelectedTrimmableCount = 0;
+ mSelectedMuteableCount = 0;
+ mCachedShareableUris = null;
+ mShareIntent.removeExtra(Intent.EXTRA_STREAM);
+ mShareIntent.setAction(null).setType(null);
+ }
+}
diff --git a/src/com/android/photos/adapters/AlbumSetCursorAdapter.java b/src/com/android/photos/adapters/AlbumSetCursorAdapter.java
new file mode 100644
index 000000000..ab99cde70
--- /dev/null
+++ b/src/com/android/photos/adapters/AlbumSetCursorAdapter.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 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.photos.adapters;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.text.format.DateFormat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.gallery3d.R;
+import com.android.photos.data.AlbumSetLoader;
+import com.android.photos.shims.LoaderCompatShim;
+
+import java.util.Date;
+
+public class AlbumSetCursorAdapter extends CursorAdapter {
+
+ private LoaderCompatShim<Cursor> mDrawableFactory;
+
+ public void setDrawableFactory(LoaderCompatShim<Cursor> factory) {
+ mDrawableFactory = factory;
+ }
+
+ public AlbumSetCursorAdapter(Context context) {
+ super(context, null, false);
+ }
+
+ @Override
+ public void bindView(View v, Context context, Cursor cursor) {
+ TextView titleTextView = (TextView) v.findViewById(
+ R.id.album_set_item_title);
+ titleTextView.setText(cursor.getString(AlbumSetLoader.INDEX_TITLE));
+
+ TextView countTextView = (TextView) v.findViewById(
+ R.id.album_set_item_count);
+ int count = cursor.getInt(AlbumSetLoader.INDEX_COUNT);
+ countTextView.setText(context.getResources().getQuantityString(
+ R.plurals.number_of_photos, count, count));
+
+ ImageView thumbImageView = (ImageView) v.findViewById(
+ R.id.album_set_item_image);
+ Drawable recycle = thumbImageView.getDrawable();
+ Drawable drawable = mDrawableFactory.drawableForItem(cursor, recycle);
+ if (recycle != drawable) {
+ thumbImageView.setImageDrawable(drawable);
+ }
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return LayoutInflater.from(context).inflate(
+ R.layout.album_set_item, parent, false);
+ }
+}
diff --git a/src/com/android/photos/adapters/PhotoThumbnailAdapter.java b/src/com/android/photos/adapters/PhotoThumbnailAdapter.java
new file mode 100644
index 000000000..1190b8c85
--- /dev/null
+++ b/src/com/android/photos/adapters/PhotoThumbnailAdapter.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 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.photos.adapters;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+
+import com.android.gallery3d.R;
+import com.android.photos.data.PhotoSetLoader;
+import com.android.photos.shims.LoaderCompatShim;
+import com.android.photos.views.GalleryThumbnailView.GalleryThumbnailAdapter;
+
+
+public class PhotoThumbnailAdapter extends CursorAdapter implements GalleryThumbnailAdapter {
+ private LayoutInflater mInflater;
+ private LoaderCompatShim<Cursor> mDrawableFactory;
+
+ public PhotoThumbnailAdapter(Context context) {
+ super(context, null, false);
+ mInflater = LayoutInflater.from(context);
+ }
+
+ public void setDrawableFactory(LoaderCompatShim<Cursor> factory) {
+ mDrawableFactory = factory;
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ ImageView iv = (ImageView) view.findViewById(R.id.thumbnail);
+ Drawable recycle = iv.getDrawable();
+ Drawable drawable = mDrawableFactory.drawableForItem(cursor, recycle);
+ if (recycle != drawable) {
+ iv.setImageDrawable(drawable);
+ }
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View view = mInflater.inflate(R.layout.photo_set_item, parent, false);
+ return view;
+ }
+
+ @Override
+ public float getIntrinsicAspectRatio(int position) {
+ Cursor cursor = getItem(position);
+ float width = cursor.getInt(PhotoSetLoader.INDEX_WIDTH);
+ float height = cursor.getInt(PhotoSetLoader.INDEX_HEIGHT);
+ return width / height;
+ }
+
+ @Override
+ public Cursor getItem(int position) {
+ return (Cursor) super.getItem(position);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/photos/data/AlbumSetLoader.java b/src/com/android/photos/data/AlbumSetLoader.java
new file mode 100644
index 000000000..940473255
--- /dev/null
+++ b/src/com/android/photos/data/AlbumSetLoader.java
@@ -0,0 +1,54 @@
+package com.android.photos.data;
+
+import android.database.MatrixCursor;
+
+
+public class AlbumSetLoader {
+ public static final int INDEX_ID = 0;
+ public static final int INDEX_TITLE = 1;
+ public static final int INDEX_TIMESTAMP = 2;
+ public static final int INDEX_THUMBNAIL_URI = 3;
+ public static final int INDEX_THUMBNAIL_WIDTH = 4;
+ public static final int INDEX_THUMBNAIL_HEIGHT = 5;
+ public static final int INDEX_COUNT_PENDING_UPLOAD = 6;
+ public static final int INDEX_COUNT = 7;
+ public static final int INDEX_SUPPORTED_OPERATIONS = 8;
+
+ public static final String[] PROJECTION = {
+ "_id",
+ "title",
+ "timestamp",
+ "thumb_uri",
+ "thumb_width",
+ "thumb_height",
+ "count_pending_upload",
+ "_count",
+ "supported_operations"
+ };
+ public static final MatrixCursor MOCK = createRandomCursor(30);
+
+ private static MatrixCursor createRandomCursor(int count) {
+ MatrixCursor c = new MatrixCursor(PROJECTION, count);
+ for (int i = 0; i < count; i++) {
+ c.addRow(createRandomRow());
+ }
+ return c;
+ }
+
+ private static Object[] createRandomRow() {
+ double random = Math.random();
+ int id = (int) (500 * random);
+ Object[] row = {
+ id,
+ "Fun times " + id,
+ (long) (System.currentTimeMillis() * random),
+ null,
+ 0,
+ 0,
+ (random < .3 ? 1 : 0),
+ 1,
+ 0
+ };
+ return row;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/photos/data/BitmapDecoder.java b/src/com/android/photos/data/BitmapDecoder.java
new file mode 100644
index 000000000..0671e73ca
--- /dev/null
+++ b/src/com/android/photos/data/BitmapDecoder.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+import android.util.Log;
+import android.util.Pools.Pool;
+import android.util.Pools.SynchronizedPool;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * BitmapDecoder keeps a pool of temporary storage to reuse for decoding
+ * bitmaps. It also simplifies the multi-stage decoding required to efficiently
+ * use GalleryBitmapPool. The static methods decode and decodeFile can be used
+ * to decode a bitmap from GalleryBitmapPool. The bitmap may be returned
+ * directly to GalleryBitmapPool or use the put method here when the bitmap is
+ * ready to be recycled.
+ */
+public class BitmapDecoder {
+ private static final String TAG = BitmapDecoder.class.getSimpleName();
+ private static final int POOL_SIZE = 4;
+ private static final int TEMP_STORAGE_SIZE_BYTES = 16 * 1024;
+ private static final int HEADER_MAX_SIZE = 128 * 1024;
+ private static final int NO_SCALING = -1;
+
+ private static final Pool<BitmapFactory.Options> sOptions =
+ new SynchronizedPool<BitmapFactory.Options>(POOL_SIZE);
+
+ private interface Decoder<T> {
+ Bitmap decode(T input, BitmapFactory.Options options);
+
+ boolean decodeBounds(T input, BitmapFactory.Options options);
+ }
+
+ private static abstract class OnlyDecode<T> implements Decoder<T> {
+ @Override
+ public boolean decodeBounds(T input, BitmapFactory.Options options) {
+ decode(input, options);
+ return true;
+ }
+ }
+
+ private static final Decoder<InputStream> sStreamDecoder = new Decoder<InputStream>() {
+ @Override
+ public Bitmap decode(InputStream is, Options options) {
+ return BitmapFactory.decodeStream(is, null, options);
+ }
+
+ @Override
+ public boolean decodeBounds(InputStream is, Options options) {
+ is.mark(HEADER_MAX_SIZE);
+ BitmapFactory.decodeStream(is, null, options);
+ try {
+ is.reset();
+ return true;
+ } catch (IOException e) {
+ Log.e(TAG, "Could not decode stream to bitmap", e);
+ return false;
+ }
+ }
+ };
+
+ private static final Decoder<String> sFileDecoder = new OnlyDecode<String>() {
+ @Override
+ public Bitmap decode(String filePath, Options options) {
+ return BitmapFactory.decodeFile(filePath, options);
+ }
+ };
+
+ private static final Decoder<byte[]> sByteArrayDecoder = new OnlyDecode<byte[]>() {
+ @Override
+ public Bitmap decode(byte[] data, Options options) {
+ return BitmapFactory.decodeByteArray(data, 0, data.length, options);
+ }
+ };
+
+ private static <T> Bitmap delegateDecode(Decoder<T> decoder, T input, int width, int height) {
+ BitmapFactory.Options options = getOptions();
+ GalleryBitmapPool pool = GalleryBitmapPool.getInstance();
+ try {
+ options.inJustDecodeBounds = true;
+ if (!decoder.decodeBounds(input, options)) {
+ return null;
+ }
+ options.inJustDecodeBounds = false;
+ Bitmap reuseBitmap = null;
+ if (width != NO_SCALING && options.outWidth >= width && options.outHeight >= height) {
+ setScaling(options, width, height);
+ } else {
+ reuseBitmap = pool.get(options.outWidth, options.outHeight);
+ }
+ options.inBitmap = reuseBitmap;
+ Bitmap decodedBitmap = decoder.decode(input, options);
+ if (reuseBitmap != null && decodedBitmap != reuseBitmap) {
+ pool.put(reuseBitmap);
+ }
+ return decodedBitmap;
+ } catch (IllegalArgumentException e) {
+ if (options.inBitmap == null) {
+ throw e;
+ }
+ pool.put(options.inBitmap);
+ options.inBitmap = null;
+ return decoder.decode(input, options);
+ } finally {
+ options.inBitmap = null;
+ options.inJustDecodeBounds = false;
+ sOptions.release(options);
+ }
+ }
+
+ public static Bitmap decode(InputStream in) {
+ try {
+ if (!in.markSupported()) {
+ in = new BufferedInputStream(in);
+ }
+ return delegateDecode(sStreamDecoder, in, NO_SCALING, NO_SCALING);
+ } finally {
+ Utils.closeSilently(in);
+ }
+ }
+
+ public static Bitmap decode(File file) {
+ return decodeFile(file.getPath());
+ }
+
+ public static Bitmap decodeFile(String path) {
+ return delegateDecode(sFileDecoder, path, NO_SCALING, NO_SCALING);
+ }
+
+ public static Bitmap decodeByteArray(byte[] data) {
+ return delegateDecode(sByteArrayDecoder, data, NO_SCALING, NO_SCALING);
+ }
+
+ public static void put(Bitmap bitmap) {
+ GalleryBitmapPool.getInstance().put(bitmap);
+ }
+
+ /**
+ * Decodes to a specific size. If the dimensions of the image don't match
+ * width x height, the resulting image will be in the proportions of the
+ * decoded image, but will be scaled to fill the dimensions. For example, if
+ * width and height are 10x10 and the image is 200x100, the resulting image
+ * will be scaled/sampled to 20x10.
+ */
+ public static Bitmap decodeFile(String path, int width, int height) {
+ return delegateDecode(sFileDecoder, path, width, height);
+ }
+
+ /** @see #decodeFile(String, int, int) */
+ public static Bitmap decodeByteArray(byte[] data, int width, int height) {
+ return delegateDecode(sByteArrayDecoder, data, width, height);
+ }
+
+ /** @see #decodeFile(String, int, int) */
+ public static Bitmap decode(InputStream in, int width, int height) {
+ try {
+ if (!in.markSupported()) {
+ in = new BufferedInputStream(in);
+ }
+ return delegateDecode(sStreamDecoder, in, width, height);
+ } finally {
+ Utils.closeSilently(in);
+ }
+ }
+
+ private static BitmapFactory.Options getOptions() {
+ BitmapFactory.Options opts = sOptions.acquire();
+ if (opts == null) {
+ opts = new BitmapFactory.Options();
+ opts.inMutable = true;
+ opts.inPreferredConfig = Config.ARGB_8888;
+ opts.inTempStorage = new byte[TEMP_STORAGE_SIZE_BYTES];
+ }
+ opts.inSampleSize = 1;
+ opts.inDensity = 1;
+ opts.inTargetDensity = 1;
+
+ return opts;
+ }
+
+ // Sets the options to sample then scale the image so that the image's
+ // minimum dimension will match side.
+ private static void setScaling(BitmapFactory.Options options, int width, int height) {
+ float widthScale = ((float)options.outWidth)/ width;
+ float heightScale = ((float) options.outHeight)/height;
+ int side = (widthScale < heightScale) ? width : height;
+ options.inSampleSize = BitmapUtils.computeSampleSize(options.outWidth, options.outHeight,
+ side, BitmapUtils.UNCONSTRAINED);
+ int constraint;
+ if (options.outWidth < options.outHeight) {
+ // Width is the constraint. Scale so that width = side.
+ constraint = options.outWidth;
+ } else {
+ // Height is the constraint. Scale so that height = side.
+ constraint = options.outHeight;
+ }
+ options.inDensity = constraint / options.inSampleSize;
+ options.inTargetDensity = side;
+ }
+}
diff --git a/src/com/android/photos/data/FileRetriever.java b/src/com/android/photos/data/FileRetriever.java
new file mode 100644
index 000000000..eb7686ef6
--- /dev/null
+++ b/src/com/android/photos/data/FileRetriever.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.graphics.Bitmap;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.android.gallery3d.common.BitmapUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+public class FileRetriever implements MediaRetriever {
+ private static final String TAG = FileRetriever.class.getSimpleName();
+
+ @Override
+ public File getLocalFile(Uri contentUri) {
+ return new File(contentUri.getPath());
+ }
+
+ @Override
+ public MediaSize getFastImageSize(Uri contentUri, MediaSize size) {
+ if (isVideo(contentUri)) {
+ return null;
+ }
+ return MediaSize.TemporaryThumbnail;
+ }
+
+ @Override
+ public byte[] getTemporaryImage(Uri contentUri, MediaSize fastImageSize) {
+
+ try {
+ ExifInterface exif = new ExifInterface(contentUri.getPath());
+ if (exif.hasThumbnail()) {
+ return exif.getThumbnail();
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Unable to load exif for " + contentUri);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean getMedia(Uri contentUri, MediaSize imageSize, File tempFile) {
+ if (imageSize == MediaSize.Original) {
+ return false; // getLocalFile should always return the original.
+ }
+ if (imageSize == MediaSize.Thumbnail) {
+ File preview = MediaCache.getInstance().getCachedFile(contentUri, MediaSize.Preview);
+ if (preview != null) {
+ // Just downsample the preview, it is faster.
+ return MediaCacheUtils.downsample(preview, imageSize, tempFile);
+ }
+ }
+ File highRes = new File(contentUri.getPath());
+ boolean success;
+ if (!isVideo(contentUri)) {
+ success = MediaCacheUtils.downsample(highRes, imageSize, tempFile);
+ } else {
+ // Video needs to extract the bitmap.
+ Bitmap bitmap = BitmapUtils.createVideoThumbnail(highRes.getPath());
+ if (bitmap == null) {
+ return false;
+ } else if (imageSize == MediaSize.Thumbnail
+ && !MediaCacheUtils.needsDownsample(bitmap, MediaSize.Preview)
+ && MediaCacheUtils.writeToFile(bitmap, tempFile)) {
+ // Opportunistically save preview
+ MediaCache mediaCache = MediaCache.getInstance();
+ mediaCache.insertIntoCache(contentUri, MediaSize.Preview, tempFile);
+ }
+ // Now scale the image
+ success = MediaCacheUtils.downsample(bitmap, imageSize, tempFile);
+ }
+ return success;
+ }
+
+ @Override
+ public Uri normalizeUri(Uri contentUri, MediaSize size) {
+ return contentUri;
+ }
+
+ @Override
+ public MediaSize normalizeMediaSize(Uri contentUri, MediaSize size) {
+ return size;
+ }
+
+ private static boolean isVideo(Uri uri) {
+ MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+ String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
+ String mimeType = mimeTypeMap.getMimeTypeFromExtension(extension);
+ return (mimeType != null && mimeType.startsWith("video/"));
+ }
+}
diff --git a/src/com/android/photos/data/GalleryBitmapPool.java b/src/com/android/photos/data/GalleryBitmapPool.java
new file mode 100644
index 000000000..390a0d42f
--- /dev/null
+++ b/src/com/android/photos/data/GalleryBitmapPool.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.util.Pools.Pool;
+import android.util.Pools.SynchronizedPool;
+
+import com.android.photos.data.SparseArrayBitmapPool.Node;
+
+/**
+ * Pool allowing the efficient reuse of bitmaps in order to avoid long
+ * garbage collection pauses.
+ */
+public class GalleryBitmapPool {
+
+ private static final int CAPACITY_BYTES = 20971520;
+
+ // We found that Gallery uses bitmaps that are either square (for example,
+ // tiles of large images or square thumbnails), match one of the common
+ // photo aspect ratios (4x3, 3x2, or 16x9), or, less commonly, are of some
+ // other aspect ratio. Taking advantage of this information, we use 3
+ // SparseArrayBitmapPool instances to back the GalleryBitmapPool, which affords
+ // O(1) lookups for square bitmaps, and average-case - but *not* asymptotically -
+ // O(1) lookups for common photo aspect ratios and other miscellaneous aspect
+ // ratios. Beware of the pathological case where there are many bitmaps added
+ // to the pool with different non-square aspect ratios but the same width, as
+ // performance will degrade and the average case lookup will approach
+ // O(# of different aspect ratios).
+ private static final int POOL_INDEX_NONE = -1;
+ private static final int POOL_INDEX_SQUARE = 0;
+ private static final int POOL_INDEX_PHOTO = 1;
+ private static final int POOL_INDEX_MISC = 2;
+
+ private static final Point[] COMMON_PHOTO_ASPECT_RATIOS =
+ { new Point(4, 3), new Point(3, 2), new Point(16, 9) };
+
+ private int mCapacityBytes;
+ private SparseArrayBitmapPool [] mPools;
+ private Pool<Node> mSharedNodePool = new SynchronizedPool<Node>(128);
+
+ private GalleryBitmapPool(int capacityBytes) {
+ mPools = new SparseArrayBitmapPool[3];
+ mPools[POOL_INDEX_SQUARE] = new SparseArrayBitmapPool(capacityBytes / 3, mSharedNodePool);
+ mPools[POOL_INDEX_PHOTO] = new SparseArrayBitmapPool(capacityBytes / 3, mSharedNodePool);
+ mPools[POOL_INDEX_MISC] = new SparseArrayBitmapPool(capacityBytes / 3, mSharedNodePool);
+ mCapacityBytes = capacityBytes;
+ }
+
+ private static GalleryBitmapPool sInstance = new GalleryBitmapPool(CAPACITY_BYTES);
+
+ public static GalleryBitmapPool getInstance() {
+ return sInstance;
+ }
+
+ private SparseArrayBitmapPool getPoolForDimensions(int width, int height) {
+ int index = getPoolIndexForDimensions(width, height);
+ if (index == POOL_INDEX_NONE) {
+ return null;
+ } else {
+ return mPools[index];
+ }
+ }
+
+ private int getPoolIndexForDimensions(int width, int height) {
+ if (width <= 0 || height <= 0) {
+ return POOL_INDEX_NONE;
+ }
+ if (width == height) {
+ return POOL_INDEX_SQUARE;
+ }
+ int min, max;
+ if (width > height) {
+ min = height;
+ max = width;
+ } else {
+ min = width;
+ max = height;
+ }
+ for (Point ar : COMMON_PHOTO_ASPECT_RATIOS) {
+ if (min * ar.x == max * ar.y) {
+ return POOL_INDEX_PHOTO;
+ }
+ }
+ return POOL_INDEX_MISC;
+ }
+
+ /**
+ * @return Capacity of the pool in bytes.
+ */
+ public synchronized int getCapacity() {
+ return mCapacityBytes;
+ }
+
+ /**
+ * @return Approximate total size in bytes of the bitmaps stored in the pool.
+ */
+ public int getSize() {
+ // Note that this only returns an approximate size, since multiple threads
+ // might be getting and putting Bitmaps from the pool and we lock at the
+ // sub-pool level to avoid unnecessary blocking.
+ int total = 0;
+ for (SparseArrayBitmapPool p : mPools) {
+ total += p.getSize();
+ }
+ return total;
+ }
+
+ /**
+ * @return Bitmap from the pool with the desired height/width or null if none available.
+ */
+ public Bitmap get(int width, int height) {
+ SparseArrayBitmapPool pool = getPoolForDimensions(width, height);
+ if (pool == null) {
+ return null;
+ } else {
+ return pool.get(width, height);
+ }
+ }
+
+ /**
+ * Adds the given bitmap to the pool.
+ * @return Whether the bitmap was added to the pool.
+ */
+ public boolean put(Bitmap b) {
+ if (b == null || b.getConfig() != Bitmap.Config.ARGB_8888) {
+ return false;
+ }
+ SparseArrayBitmapPool pool = getPoolForDimensions(b.getWidth(), b.getHeight());
+ if (pool == null) {
+ b.recycle();
+ return false;
+ } else {
+ return pool.put(b);
+ }
+ }
+
+ /**
+ * Empty the pool, recycling all the bitmaps currently in it.
+ */
+ public void clear() {
+ for (SparseArrayBitmapPool p : mPools) {
+ p.clear();
+ }
+ }
+}
diff --git a/src/com/android/photos/data/MediaCache.java b/src/com/android/photos/data/MediaCache.java
new file mode 100644
index 000000000..0952a4017
--- /dev/null
+++ b/src/com/android/photos/data/MediaCache.java
@@ -0,0 +1,676 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Log;
+
+import com.android.photos.data.MediaCacheDatabase.Action;
+import com.android.photos.data.MediaRetriever.MediaSize;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+
+/**
+ * MediaCache keeps a cache of images, videos, thumbnails and previews. Calls to
+ * retrieve a specific media item are executed asynchronously. The caller has an
+ * option to receive a notification for lower resolution images that happen to
+ * be available prior to the one requested.
+ * <p>
+ * When an media item has been retrieved, the notification for it is called on a
+ * separate notifier thread. This thread should not be held for a long time so
+ * that other notifications may happen.
+ * </p>
+ * <p>
+ * Media items are uniquely identified by their content URIs. Each
+ * scheme/authority can offer its own MediaRetriever, running in its own thread.
+ * </p>
+ * <p>
+ * The MediaCache is an LRU cache, but does not allow the thumbnail cache to
+ * drop below a minimum size. This prevents browsing through original images to
+ * wipe out the thumbnails.
+ * </p>
+ */
+public class MediaCache {
+ static final String TAG = MediaCache.class.getSimpleName();
+ /** Subdirectory containing the image cache. */
+ static final String IMAGE_CACHE_SUBDIR = "image_cache";
+ /** File name extension to use for cached images. */
+ static final String IMAGE_EXTENSION = ".cache";
+ /** File name extension to use for temporary cached images while retrieving. */
+ static final String TEMP_IMAGE_EXTENSION = ".temp";
+
+ public static interface ImageReady {
+ void imageReady(InputStream bitmapInputStream);
+ }
+
+ public static interface OriginalReady {
+ void originalReady(File originalFile);
+ }
+
+ /** A Thread for each MediaRetriever */
+ private class ProcessQueue extends Thread {
+ private Queue<ProcessingJob> mQueue;
+
+ public ProcessQueue(Queue<ProcessingJob> queue) {
+ mQueue = queue;
+ }
+
+ @Override
+ public void run() {
+ while (mRunning) {
+ ProcessingJob status;
+ synchronized (mQueue) {
+ while (mQueue.isEmpty()) {
+ try {
+ mQueue.wait();
+ } catch (InterruptedException e) {
+ if (!mRunning) {
+ return;
+ }
+ Log.w(TAG, "Unexpected interruption", e);
+ }
+ }
+ status = mQueue.remove();
+ }
+ processTask(status);
+ }
+ }
+ };
+
+ private interface NotifyReady {
+ void notifyReady();
+
+ void setFile(File file) throws FileNotFoundException;
+
+ boolean isPrefetch();
+ }
+
+ private static class NotifyOriginalReady implements NotifyReady {
+ private final OriginalReady mCallback;
+ private File mFile;
+
+ public NotifyOriginalReady(OriginalReady callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void notifyReady() {
+ if (mCallback != null) {
+ mCallback.originalReady(mFile);
+ }
+ }
+
+ @Override
+ public void setFile(File file) {
+ mFile = file;
+ }
+
+ @Override
+ public boolean isPrefetch() {
+ return mCallback == null;
+ }
+ }
+
+ private static class NotifyImageReady implements NotifyReady {
+ private final ImageReady mCallback;
+ private InputStream mInputStream;
+
+ public NotifyImageReady(ImageReady callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void notifyReady() {
+ if (mCallback != null) {
+ mCallback.imageReady(mInputStream);
+ }
+ }
+
+ @Override
+ public void setFile(File file) throws FileNotFoundException {
+ mInputStream = new FileInputStream(file);
+ }
+
+ public void setBytes(byte[] bytes) {
+ mInputStream = new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public boolean isPrefetch() {
+ return mCallback == null;
+ }
+ }
+
+ /** A media item to be retrieved and its notifications. */
+ private static class ProcessingJob {
+ public ProcessingJob(Uri uri, MediaSize size, NotifyReady complete,
+ NotifyImageReady lowResolution) {
+ this.contentUri = uri;
+ this.size = size;
+ this.complete = complete;
+ this.lowResolution = lowResolution;
+ }
+ public Uri contentUri;
+ public MediaSize size;
+ public NotifyImageReady lowResolution;
+ public NotifyReady complete;
+ }
+
+ private boolean mRunning = true;
+ private static MediaCache sInstance;
+ private File mCacheDir;
+ private Context mContext;
+ private Queue<NotifyReady> mCallbacks = new LinkedList<NotifyReady>();
+ private Map<String, MediaRetriever> mRetrievers = new HashMap<String, MediaRetriever>();
+ private Map<String, List<ProcessingJob>> mTasks = new HashMap<String, List<ProcessingJob>>();
+ private List<ProcessQueue> mProcessingThreads = new ArrayList<ProcessQueue>();
+ private MediaCacheDatabase mDatabaseHelper;
+ private long mTempImageNumber = 1;
+ private Object mTempImageNumberLock = new Object();
+
+ private long mMaxCacheSize = 40 * 1024 * 1024; // 40 MB
+ private long mMinThumbCacheSize = 4 * 1024 * 1024; // 4 MB
+ private long mCacheSize = -1;
+ private long mThumbCacheSize = -1;
+ private Object mCacheSizeLock = new Object();
+
+ private Action mNotifyCachedLowResolution = new Action() {
+ @Override
+ public void execute(Uri uri, long id, MediaSize size, Object parameter) {
+ ProcessingJob job = (ProcessingJob) parameter;
+ File file = createCacheImagePath(id);
+ addNotification(job.lowResolution, file);
+ }
+ };
+
+ private Action mMoveTempToCache = new Action() {
+ @Override
+ public void execute(Uri uri, long id, MediaSize size, Object parameter) {
+ File tempFile = (File) parameter;
+ File cacheFile = createCacheImagePath(id);
+ tempFile.renameTo(cacheFile);
+ }
+ };
+
+ private Action mDeleteFile = new Action() {
+ @Override
+ public void execute(Uri uri, long id, MediaSize size, Object parameter) {
+ File file = createCacheImagePath(id);
+ file.delete();
+ synchronized (mCacheSizeLock) {
+ if (mCacheSize != -1) {
+ long length = (Long) parameter;
+ mCacheSize -= length;
+ if (size == MediaSize.Thumbnail) {
+ mThumbCacheSize -= length;
+ }
+ }
+ }
+ }
+ };
+
+ /** The thread used to make ImageReady and OriginalReady callbacks. */
+ private Thread mProcessNotifications = new Thread() {
+ @Override
+ public void run() {
+ while (mRunning) {
+ NotifyReady notifyImage;
+ synchronized (mCallbacks) {
+ while (mCallbacks.isEmpty()) {
+ try {
+ mCallbacks.wait();
+ } catch (InterruptedException e) {
+ if (!mRunning) {
+ return;
+ }
+ Log.w(TAG, "Unexpected Interruption, continuing");
+ }
+ }
+ notifyImage = mCallbacks.remove();
+ }
+
+ notifyImage.notifyReady();
+ }
+ }
+ };
+
+ public static synchronized void initialize(Context context) {
+ if (sInstance == null) {
+ sInstance = new MediaCache(context);
+ MediaCacheUtils.initialize(context);
+ }
+ }
+
+ public static MediaCache getInstance() {
+ return sInstance;
+ }
+
+ public static synchronized void shutdown() {
+ sInstance.mRunning = false;
+ sInstance.mProcessNotifications.interrupt();
+ for (ProcessQueue processingThread : sInstance.mProcessingThreads) {
+ processingThread.interrupt();
+ }
+ sInstance = null;
+ }
+
+ private MediaCache(Context context) {
+ mDatabaseHelper = new MediaCacheDatabase(context);
+ mProcessNotifications.start();
+ mContext = context;
+ }
+
+ // This is used for testing.
+ public void setCacheDir(File cacheDir) {
+ cacheDir.mkdirs();
+ mCacheDir = cacheDir;
+ }
+
+ public File getCacheDir() {
+ synchronized (mContext) {
+ if (mCacheDir == null) {
+ String state = Environment.getExternalStorageState();
+ File baseDir;
+ if (Environment.MEDIA_MOUNTED.equals(state)) {
+ baseDir = mContext.getExternalCacheDir();
+ } else {
+ // Stored in internal cache
+ baseDir = mContext.getCacheDir();
+ }
+ mCacheDir = new File(baseDir, IMAGE_CACHE_SUBDIR);
+ mCacheDir.mkdirs();
+ }
+ return mCacheDir;
+ }
+ }
+
+ /**
+ * Invalidates all cached images related to a given contentUri. This call
+ * doesn't complete until the images have been removed from the cache.
+ */
+ public void invalidate(Uri contentUri) {
+ mDatabaseHelper.delete(contentUri, mDeleteFile);
+ }
+
+ public void clearCacheDir() {
+ File[] cachedFiles = getCacheDir().listFiles();
+ if (cachedFiles != null) {
+ for (File cachedFile : cachedFiles) {
+ cachedFile.delete();
+ }
+ }
+ }
+
+ /**
+ * Add a MediaRetriever for a Uri scheme and authority. This MediaRetriever
+ * will be granted its own thread for retrieving images.
+ */
+ public void addRetriever(String scheme, String authority, MediaRetriever retriever) {
+ String differentiator = getDifferentiator(scheme, authority);
+ synchronized (mRetrievers) {
+ mRetrievers.put(differentiator, retriever);
+ }
+ synchronized (mTasks) {
+ LinkedList<ProcessingJob> queue = new LinkedList<ProcessingJob>();
+ mTasks.put(differentiator, queue);
+ new ProcessQueue(queue).start();
+ }
+ }
+
+ /**
+ * Retrieves a thumbnail. complete will be called when the thumbnail is
+ * available. If lowResolution is not null and a lower resolution thumbnail
+ * is available before the thumbnail, lowResolution will be called prior to
+ * complete. All callbacks will be made on a thread other than the calling
+ * thread.
+ *
+ * @param contentUri The URI for the full resolution image to search for.
+ * @param complete Callback for when the image has been retrieved.
+ * @param lowResolution If not null and a lower resolution image is
+ * available prior to retrieving the thumbnail, this will be
+ * called with the low resolution bitmap.
+ */
+ public void retrieveThumbnail(Uri contentUri, ImageReady complete, ImageReady lowResolution) {
+ addTask(contentUri, complete, lowResolution, MediaSize.Thumbnail);
+ }
+
+ /**
+ * Retrieves a preview. complete will be called when the preview is
+ * available. If lowResolution is not null and a lower resolution preview is
+ * available before the preview, lowResolution will be called prior to
+ * complete. All callbacks will be made on a thread other than the calling
+ * thread.
+ *
+ * @param contentUri The URI for the full resolution image to search for.
+ * @param complete Callback for when the image has been retrieved.
+ * @param lowResolution If not null and a lower resolution image is
+ * available prior to retrieving the preview, this will be called
+ * with the low resolution bitmap.
+ */
+ public void retrievePreview(Uri contentUri, ImageReady complete, ImageReady lowResolution) {
+ addTask(contentUri, complete, lowResolution, MediaSize.Preview);
+ }
+
+ /**
+ * Retrieves the original image or video. complete will be called when the
+ * media is available on the local file system. If lowResolution is not null
+ * and a lower resolution preview is available before the original,
+ * lowResolution will be called prior to complete. All callbacks will be
+ * made on a thread other than the calling thread.
+ *
+ * @param contentUri The URI for the full resolution image to search for.
+ * @param complete Callback for when the image has been retrieved.
+ * @param lowResolution If not null and a lower resolution image is
+ * available prior to retrieving the preview, this will be called
+ * with the low resolution bitmap.
+ */
+ public void retrieveOriginal(Uri contentUri, OriginalReady complete, ImageReady lowResolution) {
+ File localFile = getLocalFile(contentUri);
+ if (localFile != null) {
+ addNotification(new NotifyOriginalReady(complete), localFile);
+ } else {
+ NotifyImageReady notifyLowResolution = (lowResolution == null) ? null
+ : new NotifyImageReady(lowResolution);
+ addTask(contentUri, new NotifyOriginalReady(complete), notifyLowResolution,
+ MediaSize.Original);
+ }
+ }
+
+ /**
+ * Looks for an already cached media at a specific size.
+ *
+ * @param contentUri The original media item content URI
+ * @param size The target size to search for in the cache
+ * @return The cached file location or null if it is not cached.
+ */
+ public File getCachedFile(Uri contentUri, MediaSize size) {
+ Long cachedId = mDatabaseHelper.getCached(contentUri, size);
+ File file = null;
+ if (cachedId != null) {
+ file = createCacheImagePath(cachedId);
+ if (!file.exists()) {
+ mDatabaseHelper.delete(contentUri, size, mDeleteFile);
+ file = null;
+ }
+ }
+ return file;
+ }
+
+ /**
+ * Inserts a media item into the cache.
+ *
+ * @param contentUri The original media item URI.
+ * @param size The size of the media item to store in the cache.
+ * @param tempFile The temporary file where the image is stored. This file
+ * will no longer exist after executing this method.
+ * @return The new location, in the cache, of the media item or null if it
+ * wasn't possible to move into the cache.
+ */
+ public File insertIntoCache(Uri contentUri, MediaSize size, File tempFile) {
+ long fileSize = tempFile.length();
+ if (fileSize == 0) {
+ return null;
+ }
+ File cacheFile = null;
+ SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+ // Ensure that this step is atomic
+ db.beginTransaction();
+ try {
+ Long id = mDatabaseHelper.getCached(contentUri, size);
+ if (id != null) {
+ cacheFile = createCacheImagePath(id);
+ if (tempFile.renameTo(cacheFile)) {
+ mDatabaseHelper.updateLength(id, fileSize);
+ } else {
+ Log.w(TAG, "Could not update cached file with " + tempFile);
+ tempFile.delete();
+ cacheFile = null;
+ }
+ } else {
+ ensureFreeCacheSpace(tempFile.length(), size);
+ id = mDatabaseHelper.insert(contentUri, size, mMoveTempToCache, tempFile);
+ cacheFile = createCacheImagePath(id);
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ return cacheFile;
+ }
+
+ /**
+ * For testing purposes.
+ */
+ public void setMaxCacheSize(long maxCacheSize) {
+ synchronized (mCacheSizeLock) {
+ mMaxCacheSize = maxCacheSize;
+ mMinThumbCacheSize = mMaxCacheSize / 10;
+ mCacheSize = -1;
+ mThumbCacheSize = -1;
+ }
+ }
+
+ private File createCacheImagePath(long id) {
+ return new File(getCacheDir(), String.valueOf(id) + IMAGE_EXTENSION);
+ }
+
+ private void addTask(Uri contentUri, ImageReady complete, ImageReady lowResolution,
+ MediaSize size) {
+ NotifyReady notifyComplete = new NotifyImageReady(complete);
+ NotifyImageReady notifyLowResolution = null;
+ if (lowResolution != null) {
+ notifyLowResolution = new NotifyImageReady(lowResolution);
+ }
+ addTask(contentUri, notifyComplete, notifyLowResolution, size);
+ }
+
+ private void addTask(Uri contentUri, NotifyReady complete, NotifyImageReady lowResolution,
+ MediaSize size) {
+ MediaRetriever retriever = getMediaRetriever(contentUri);
+ Uri uri = retriever.normalizeUri(contentUri, size);
+ if (uri == null) {
+ throw new IllegalArgumentException("No MediaRetriever for " + contentUri);
+ }
+ size = retriever.normalizeMediaSize(uri, size);
+
+ File cachedFile = getCachedFile(uri, size);
+ if (cachedFile != null) {
+ addNotification(complete, cachedFile);
+ return;
+ }
+ String differentiator = getDifferentiator(uri.getScheme(), uri.getAuthority());
+ synchronized (mTasks) {
+ List<ProcessingJob> tasks = mTasks.get(differentiator);
+ if (tasks == null) {
+ throw new IllegalArgumentException("Cannot find retriever for: " + uri);
+ }
+ synchronized (tasks) {
+ ProcessingJob job = new ProcessingJob(uri, size, complete, lowResolution);
+ if (complete.isPrefetch()) {
+ tasks.add(job);
+ } else {
+ int index = tasks.size() - 1;
+ while (index >= 0 && tasks.get(index).complete.isPrefetch()) {
+ index--;
+ }
+ tasks.add(index + 1, job);
+ }
+ tasks.notifyAll();
+ }
+ }
+ }
+
+ private MediaRetriever getMediaRetriever(Uri uri) {
+ String differentiator = getDifferentiator(uri.getScheme(), uri.getAuthority());
+ MediaRetriever retriever;
+ synchronized (mRetrievers) {
+ retriever = mRetrievers.get(differentiator);
+ }
+ if (retriever == null) {
+ throw new IllegalArgumentException("No MediaRetriever for " + uri);
+ }
+ return retriever;
+ }
+
+ private File getLocalFile(Uri uri) {
+ MediaRetriever retriever = getMediaRetriever(uri);
+ File localFile = null;
+ if (retriever != null) {
+ localFile = retriever.getLocalFile(uri);
+ }
+ return localFile;
+ }
+
+ private MediaSize getFastImageSize(Uri uri, MediaSize size) {
+ MediaRetriever retriever = getMediaRetriever(uri);
+ return retriever.getFastImageSize(uri, size);
+ }
+
+ private boolean isFastImageBetter(MediaSize fastImageType, MediaSize size) {
+ if (fastImageType == null) {
+ return false;
+ }
+ if (size == null) {
+ return true;
+ }
+ return fastImageType.isBetterThan(size);
+ }
+
+ private byte[] getTemporaryImage(Uri uri, MediaSize fastImageType) {
+ MediaRetriever retriever = getMediaRetriever(uri);
+ return retriever.getTemporaryImage(uri, fastImageType);
+ }
+
+ private void processTask(ProcessingJob job) {
+ File cachedFile = getCachedFile(job.contentUri, job.size);
+ if (cachedFile != null) {
+ addNotification(job.complete, cachedFile);
+ return;
+ }
+
+ boolean hasLowResolution = job.lowResolution != null;
+ if (hasLowResolution) {
+ MediaSize cachedSize = mDatabaseHelper.executeOnBestCached(job.contentUri, job.size,
+ mNotifyCachedLowResolution);
+ MediaSize fastImageSize = getFastImageSize(job.contentUri, job.size);
+ if (isFastImageBetter(fastImageSize, cachedSize)) {
+ if (fastImageSize.isTemporary()) {
+ byte[] bytes = getTemporaryImage(job.contentUri, fastImageSize);
+ if (bytes != null) {
+ addNotification(job.lowResolution, bytes);
+ }
+ } else {
+ File lowFile = getMedia(job.contentUri, fastImageSize);
+ if (lowFile != null) {
+ addNotification(job.lowResolution, lowFile);
+ }
+ }
+ }
+ }
+
+ // Now get the full size desired
+ File fullSizeFile = getMedia(job.contentUri, job.size);
+ if (fullSizeFile != null) {
+ addNotification(job.complete, fullSizeFile);
+ }
+ }
+
+ private void addNotification(NotifyReady callback, File file) {
+ try {
+ callback.setFile(file);
+ synchronized (mCallbacks) {
+ mCallbacks.add(callback);
+ mCallbacks.notifyAll();
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Unable to read file " + file, e);
+ }
+ }
+
+ private void addNotification(NotifyImageReady callback, byte[] bytes) {
+ callback.setBytes(bytes);
+ synchronized (mCallbacks) {
+ mCallbacks.add(callback);
+ mCallbacks.notifyAll();
+ }
+ }
+
+ private File getMedia(Uri uri, MediaSize size) {
+ long imageNumber;
+ synchronized (mTempImageNumberLock) {
+ imageNumber = mTempImageNumber++;
+ }
+ File tempFile = new File(getCacheDir(), String.valueOf(imageNumber) + TEMP_IMAGE_EXTENSION);
+ MediaRetriever retriever = getMediaRetriever(uri);
+ boolean retrieved = retriever.getMedia(uri, size, tempFile);
+ File cachedFile = null;
+ if (retrieved) {
+ ensureFreeCacheSpace(tempFile.length(), size);
+ long id = mDatabaseHelper.insert(uri, size, mMoveTempToCache, tempFile);
+ cachedFile = createCacheImagePath(id);
+ }
+ return cachedFile;
+ }
+
+ private static String getDifferentiator(String scheme, String authority) {
+ if (authority == null) {
+ return scheme;
+ }
+ StringBuilder differentiator = new StringBuilder(scheme);
+ differentiator.append(':');
+ differentiator.append(authority);
+ return differentiator.toString();
+ }
+
+ private void ensureFreeCacheSpace(long size, MediaSize mediaSize) {
+ synchronized (mCacheSizeLock) {
+ if (mCacheSize == -1 || mThumbCacheSize == -1) {
+ mCacheSize = mDatabaseHelper.getCacheSize();
+ mThumbCacheSize = mDatabaseHelper.getThumbnailCacheSize();
+ if (mCacheSize == -1 || mThumbCacheSize == -1) {
+ Log.e(TAG, "Can't determine size of the image cache");
+ return;
+ }
+ }
+ mCacheSize += size;
+ if (mediaSize == MediaSize.Thumbnail) {
+ mThumbCacheSize += size;
+ }
+ if (mCacheSize > mMaxCacheSize) {
+ shrinkCacheLocked();
+ }
+ }
+ }
+
+ private void shrinkCacheLocked() {
+ long deleteSize = mMinThumbCacheSize;
+ boolean includeThumbnails = (mThumbCacheSize - deleteSize) > mMinThumbCacheSize;
+ mDatabaseHelper.deleteOldCached(includeThumbnails, deleteSize, mDeleteFile);
+ }
+}
diff --git a/src/com/android/photos/data/MediaCacheDatabase.java b/src/com/android/photos/data/MediaCacheDatabase.java
new file mode 100644
index 000000000..c92ac0fdf
--- /dev/null
+++ b/src/com/android/photos/data/MediaCacheDatabase.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+import com.android.photos.data.MediaRetriever.MediaSize;
+
+import java.io.File;
+
+class MediaCacheDatabase extends SQLiteOpenHelper {
+ public static final int DB_VERSION = 1;
+ public static final String DB_NAME = "mediacache.db";
+
+ /** Internal database table used for the media cache */
+ public static final String TABLE = "media_cache";
+
+ private static interface Columns extends BaseColumns {
+ /** The Content URI of the original image. */
+ public static final String URI = "uri";
+ /** MediaSize.getValue() values. */
+ public static final String MEDIA_SIZE = "media_size";
+ /** The last time this image was queried. */
+ public static final String LAST_ACCESS = "last_access";
+ /** The image size in bytes. */
+ public static final String SIZE_IN_BYTES = "size";
+ }
+
+ static interface Action {
+ void execute(Uri uri, long id, MediaSize size, Object parameter);
+ }
+
+ private static final String[] PROJECTION_ID = {
+ Columns._ID,
+ };
+
+ private static final String[] PROJECTION_CACHED = {
+ Columns._ID, Columns.MEDIA_SIZE, Columns.SIZE_IN_BYTES,
+ };
+
+ private static final String[] PROJECTION_CACHE_SIZE = {
+ "SUM(" + Columns.SIZE_IN_BYTES + ")"
+ };
+
+ private static final String[] PROJECTION_DELETE_OLD = {
+ Columns._ID, Columns.URI, Columns.MEDIA_SIZE, Columns.SIZE_IN_BYTES, Columns.LAST_ACCESS,
+ };
+
+ public static final String CREATE_TABLE = "CREATE TABLE " + TABLE + "("
+ + Columns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ + Columns.URI + " TEXT NOT NULL,"
+ + Columns.MEDIA_SIZE + " INTEGER NOT NULL,"
+ + Columns.LAST_ACCESS + " INTEGER NOT NULL,"
+ + Columns.SIZE_IN_BYTES + " INTEGER NOT NULL,"
+ + "UNIQUE(" + Columns.URI + ", " + Columns.MEDIA_SIZE + "))";
+
+ public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE;
+
+ public static final String WHERE_THUMBNAIL = Columns.MEDIA_SIZE + " = "
+ + MediaSize.Thumbnail.getValue();
+
+ public static final String WHERE_NOT_THUMBNAIL = Columns.MEDIA_SIZE + " <> "
+ + MediaSize.Thumbnail.getValue();
+
+ public static final String WHERE_CLEAR_CACHE = Columns.LAST_ACCESS + " <= ?";
+
+ public static final String WHERE_CLEAR_CACHE_LARGE = WHERE_CLEAR_CACHE + " AND "
+ + WHERE_NOT_THUMBNAIL;
+
+ static class QueryCacheResults {
+ public QueryCacheResults(long id, int sizeVal) {
+ this.id = id;
+ this.size = MediaSize.fromInteger(sizeVal);
+ }
+ public long id;
+ public MediaSize size;
+ }
+
+ public MediaCacheDatabase(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(CREATE_TABLE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ db.execSQL(DROP_TABLE);
+ onCreate(db);
+ MediaCache.getInstance().clearCacheDir();
+ }
+
+ public Long getCached(Uri uri, MediaSize size) {
+ String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " = ?";
+ SQLiteDatabase db = getWritableDatabase();
+ String[] whereArgs = {
+ uri.toString(), String.valueOf(size.getValue()),
+ };
+ Cursor cursor = db.query(TABLE, PROJECTION_ID, where, whereArgs, null, null, null);
+ Long id = null;
+ if (cursor.moveToNext()) {
+ id = cursor.getLong(0);
+ }
+ cursor.close();
+ if (id != null) {
+ String[] updateArgs = {
+ id.toString()
+ };
+ ContentValues values = new ContentValues();
+ values.put(Columns.LAST_ACCESS, System.currentTimeMillis());
+ db.beginTransaction();
+ try {
+ db.update(TABLE, values, Columns._ID + " = ?", updateArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+ return id;
+ }
+
+ public MediaSize executeOnBestCached(Uri uri, MediaSize size, Action action) {
+ String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " < ?";
+ String orderBy = Columns.MEDIA_SIZE + " DESC";
+ SQLiteDatabase db = getReadableDatabase();
+ String[] whereArgs = {
+ uri.toString(), String.valueOf(size.getValue()),
+ };
+ Cursor cursor = db.query(TABLE, PROJECTION_CACHED, where, whereArgs, null, null, orderBy);
+ MediaSize bestSize = null;
+ if (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ bestSize = MediaSize.fromInteger(cursor.getInt(1));
+ long fileSize = cursor.getLong(2);
+ action.execute(uri, id, bestSize, fileSize);
+ }
+ cursor.close();
+ return bestSize;
+ }
+
+ public long insert(Uri uri, MediaSize size, Action action, File tempFile) {
+ SQLiteDatabase db = getWritableDatabase();
+ db.beginTransaction();
+ try {
+ ContentValues values = new ContentValues();
+ values.put(Columns.LAST_ACCESS, System.currentTimeMillis());
+ values.put(Columns.MEDIA_SIZE, size.getValue());
+ values.put(Columns.URI, uri.toString());
+ values.put(Columns.SIZE_IN_BYTES, tempFile.length());
+ long id = db.insert(TABLE, null, values);
+ if (id != -1) {
+ action.execute(uri, id, size, tempFile);
+ db.setTransactionSuccessful();
+ }
+ return id;
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public void updateLength(long id, long fileSize) {
+ ContentValues values = new ContentValues();
+ values.put(Columns.SIZE_IN_BYTES, fileSize);
+ String[] whereArgs = {
+ String.valueOf(id)
+ };
+ SQLiteDatabase db = getWritableDatabase();
+ db.beginTransaction();
+ try {
+ db.update(TABLE, values, Columns._ID + " = ?", whereArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public void delete(Uri uri, MediaSize size, Action action) {
+ String where = Columns.URI + " = ? AND " + Columns.MEDIA_SIZE + " = ?";
+ String[] whereArgs = {
+ uri.toString(), String.valueOf(size.getValue()),
+ };
+ deleteRows(uri, where, whereArgs, action);
+ }
+
+ public void delete(Uri uri, Action action) {
+ String where = Columns.URI + " = ?";
+ String[] whereArgs = {
+ uri.toString()
+ };
+ deleteRows(uri, where, whereArgs, action);
+ }
+
+ private void deleteRows(Uri uri, String where, String[] whereArgs, Action action) {
+ SQLiteDatabase db = getWritableDatabase();
+ // Make this an atomic operation
+ db.beginTransaction();
+ Cursor cursor = db.query(TABLE, PROJECTION_CACHED, where, whereArgs, null, null, null);
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ MediaSize size = MediaSize.fromInteger(cursor.getInt(1));
+ long length = cursor.getLong(2);
+ action.execute(uri, id, size, length);
+ }
+ cursor.close();
+ try {
+ db.delete(TABLE, where, whereArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public void deleteOldCached(boolean includeThumbnails, long deleteSize, Action action) {
+ String where = includeThumbnails ? null : WHERE_NOT_THUMBNAIL;
+ long lastAccess = 0;
+ SQLiteDatabase db = getWritableDatabase();
+ db.beginTransaction();
+ try {
+ Cursor cursor = db.query(TABLE, PROJECTION_DELETE_OLD, where, null, null, null,
+ Columns.LAST_ACCESS);
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ String uri = cursor.getString(1);
+ MediaSize size = MediaSize.fromInteger(cursor.getInt(2));
+ long length = cursor.getLong(3);
+ long imageLastAccess = cursor.getLong(4);
+
+ if (imageLastAccess != lastAccess && deleteSize < 0) {
+ break; // We've deleted enough.
+ }
+ lastAccess = imageLastAccess;
+ action.execute(Uri.parse(uri), id, size, length);
+ deleteSize -= length;
+ }
+ cursor.close();
+ String[] whereArgs = {
+ String.valueOf(lastAccess),
+ };
+ String whereDelete = includeThumbnails ? WHERE_CLEAR_CACHE : WHERE_CLEAR_CACHE_LARGE;
+ db.delete(TABLE, whereDelete, whereArgs);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ public long getCacheSize() {
+ return getCacheSize(null);
+ }
+
+ public long getThumbnailCacheSize() {
+ return getCacheSize(WHERE_THUMBNAIL);
+ }
+
+ private long getCacheSize(String where) {
+ SQLiteDatabase db = getReadableDatabase();
+ Cursor cursor = db.query(TABLE, PROJECTION_CACHE_SIZE, where, null, null, null, null);
+ long size = -1;
+ if (cursor.moveToNext()) {
+ size = cursor.getLong(0);
+ }
+ cursor.close();
+ return size;
+ }
+}
diff --git a/src/com/android/photos/data/MediaCacheUtils.java b/src/com/android/photos/data/MediaCacheUtils.java
new file mode 100644
index 000000000..e3ccd1402
--- /dev/null
+++ b/src/com/android/photos/data/MediaCacheUtils.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+import android.util.Pools.SimplePool;
+import android.util.Pools.SynchronizedPool;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.DecodeUtils;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.android.photos.data.MediaRetriever.MediaSize;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class MediaCacheUtils {
+ private static final String TAG = MediaCacheUtils.class.getSimpleName();
+ private static int QUALITY = 80;
+ private static final int BUFFER_SIZE = 4096;
+ private static final SimplePool<byte[]> mBufferPool = new SynchronizedPool<byte[]>(5);
+
+ private static final JobContext sJobStub = new JobContext() {
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void setCancelListener(CancelListener listener) {
+ }
+
+ @Override
+ public boolean setMode(int mode) {
+ return true;
+ }
+ };
+
+ private static int mTargetThumbnailSize;
+ private static int mTargetPreviewSize;
+
+ public static void initialize(Context context) {
+ Resources resources = context.getResources();
+ mTargetThumbnailSize = resources.getDimensionPixelSize(R.dimen.size_thumbnail);
+ mTargetPreviewSize = resources.getDimensionPixelSize(R.dimen.size_preview);
+ }
+
+ public static int getTargetSize(MediaSize size) {
+ return (size == MediaSize.Thumbnail) ? mTargetThumbnailSize : mTargetPreviewSize;
+ }
+
+ public static boolean downsample(File inBitmap, MediaSize targetSize, File outBitmap) {
+ if (MediaSize.Original == targetSize) {
+ return false; // MediaCache should use the local path for this.
+ }
+ int size = getTargetSize(targetSize);
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ // TODO: remove unnecessary job context from DecodeUtils.
+ Bitmap bitmap = DecodeUtils.decodeThumbnail(sJobStub, inBitmap.getPath(), options, size,
+ MediaItem.TYPE_THUMBNAIL);
+ boolean success = (bitmap != null);
+ if (success) {
+ success = writeAndRecycle(bitmap, outBitmap);
+ }
+ return success;
+ }
+
+ public static boolean downsample(Bitmap inBitmap, MediaSize size, File outBitmap) {
+ if (MediaSize.Original == size) {
+ return false; // MediaCache should use the local path for this.
+ }
+ int targetSize = getTargetSize(size);
+ boolean success;
+ if (!needsDownsample(inBitmap, size)) {
+ success = writeAndRecycle(inBitmap, outBitmap);
+ } else {
+ float maxDimension = Math.max(inBitmap.getWidth(), inBitmap.getHeight());
+ float scale = targetSize / maxDimension;
+ int targetWidth = Math.round(scale * inBitmap.getWidth());
+ int targetHeight = Math.round(scale * inBitmap.getHeight());
+ Bitmap scaled = Bitmap.createScaledBitmap(inBitmap, targetWidth, targetHeight, false);
+ success = writeAndRecycle(scaled, outBitmap);
+ inBitmap.recycle();
+ }
+ return success;
+ }
+
+ public static boolean extractImageFromVideo(File inVideo, File outBitmap) {
+ Bitmap bitmap = BitmapUtils.createVideoThumbnail(inVideo.getPath());
+ return writeAndRecycle(bitmap, outBitmap);
+ }
+
+ public static boolean needsDownsample(Bitmap bitmap, MediaSize size) {
+ if (size == MediaSize.Original) {
+ return false;
+ }
+ int targetSize = getTargetSize(size);
+ int maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
+ return maxDimension > (targetSize * 4 / 3);
+ }
+
+ public static boolean writeAndRecycle(Bitmap bitmap, File outBitmap) {
+ boolean success = writeToFile(bitmap, outBitmap);
+ bitmap.recycle();
+ return success;
+ }
+
+ public static boolean writeToFile(Bitmap bitmap, File outBitmap) {
+ boolean success = false;
+ try {
+ FileOutputStream out = new FileOutputStream(outBitmap);
+ success = bitmap.compress(CompressFormat.JPEG, QUALITY, out);
+ out.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Couldn't write bitmap to cache", e);
+ // success is already false
+ }
+ return success;
+ }
+
+ public static int copyStream(InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = mBufferPool.acquire();
+ if (buffer == null) {
+ buffer = new byte[BUFFER_SIZE];
+ }
+ try {
+ int totalWritten = 0;
+ int bytesRead;
+ while ((bytesRead = in.read(buffer)) >= 0) {
+ out.write(buffer, 0, bytesRead);
+ totalWritten += bytesRead;
+ }
+ return totalWritten;
+ } finally {
+ Utils.closeSilently(in);
+ Utils.closeSilently(out);
+ mBufferPool.release(buffer);
+ }
+ }
+}
diff --git a/src/com/android/photos/data/MediaRetriever.java b/src/com/android/photos/data/MediaRetriever.java
new file mode 100644
index 000000000..f383e5ffa
--- /dev/null
+++ b/src/com/android/photos/data/MediaRetriever.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.net.Uri;
+
+import java.io.File;
+
+public interface MediaRetriever {
+ public enum MediaSize {
+ TemporaryThumbnail(5), Thumbnail(10), TemporaryPreview(15), Preview(20), Original(30);
+
+ private final int mValue;
+
+ private MediaSize(int value) {
+ mValue = value;
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+
+ static MediaSize fromInteger(int value) {
+ switch (value) {
+ case 10:
+ return MediaSize.Thumbnail;
+ case 20:
+ return MediaSize.Preview;
+ case 30:
+ return MediaSize.Original;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ public boolean isBetterThan(MediaSize that) {
+ return mValue > that.mValue;
+ }
+
+ public boolean isTemporary() {
+ return this == TemporaryThumbnail || this == TemporaryPreview;
+ }
+ }
+
+ /**
+ * Returns the local File for the given Uri. If the image is not stored
+ * locally, null should be returned. The image should not be retrieved if it
+ * isn't already available.
+ *
+ * @param contentUri The media URI to search for.
+ * @return The local File of the image if it is available or null if it
+ * isn't.
+ */
+ File getLocalFile(Uri contentUri);
+
+ /**
+ * Returns the fast access image type for a given image size, if supported.
+ * This image should be smaller than size and should be quick to retrieve.
+ * It does not have to obey the expected aspect ratio.
+ *
+ * @param contentUri The original media Uri.
+ * @param size The target size to search for a fast-access image.
+ * @return The fast image type supported for the given image size or null of
+ * no fast image is supported.
+ */
+ MediaSize getFastImageSize(Uri contentUri, MediaSize size);
+
+ /**
+ * Returns a byte array containing the contents of the fast temporary image
+ * for a given image size. For example, a thumbnail may be smaller or of a
+ * different aspect ratio than the generated thumbnail.
+ *
+ * @param contentUri The original media Uri.
+ * @param temporarySize The target media size. Guaranteed to be a MediaSize
+ * for which isTemporary() returns true.
+ * @return A byte array of contents for for the given contentUri and
+ * fastImageType. null can be retrieved if the quick retrieval
+ * fails.
+ */
+ byte[] getTemporaryImage(Uri contentUri, MediaSize temporarySize);
+
+ /**
+ * Retrieves an image and saves it to a file.
+ *
+ * @param contentUri The original media Uri.
+ * @param size The target media size.
+ * @param tempFile The file to write the bitmap to.
+ * @return <code>true</code> on success.
+ */
+ boolean getMedia(Uri contentUri, MediaSize imageSize, File tempFile);
+
+ /**
+ * Normalizes a URI that may have additional parameters. It is fine to
+ * return contentUri. This is executed on the calling thread, so it must be
+ * a fast access operation and cannot depend, for example, on I/O.
+ *
+ * @param contentUri The URI to normalize
+ * @param size The size of the image being requested
+ * @return The normalized URI representation of contentUri.
+ */
+ Uri normalizeUri(Uri contentUri, MediaSize size);
+
+ /**
+ * Normalize the MediaSize for a given URI. Typically the size returned
+ * would be the passed-in size. Some URIs may only have one size used and
+ * should be treaded as Thumbnails, for example. This is executed on the
+ * calling thread, so it must be a fast access operation and cannot depend,
+ * for example, on I/O.
+ *
+ * @param contentUri The URI for the size being normalized.
+ * @param size The size to be normalized.
+ * @return The normalized size of the given URI.
+ */
+ MediaSize normalizeMediaSize(Uri contentUri, MediaSize size);
+}
diff --git a/src/com/android/photos/data/NotificationWatcher.java b/src/com/android/photos/data/NotificationWatcher.java
new file mode 100644
index 000000000..9041c236f
--- /dev/null
+++ b/src/com/android/photos/data/NotificationWatcher.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.net.Uri;
+
+import com.android.photos.data.PhotoProvider.ChangeNotification;
+
+import java.util.ArrayList;
+
+/**
+ * Used for capturing notifications from PhotoProvider without relying on
+ * ContentResolver. MockContentResolver does not allow sending notification to
+ * ContentObservers, so PhotoProvider allows this alternative for testing.
+ */
+public class NotificationWatcher implements ChangeNotification {
+ private ArrayList<Uri> mUris = new ArrayList<Uri>();
+ private boolean mSyncToNetwork = false;
+
+ @Override
+ public void notifyChange(Uri uri, boolean syncToNetwork) {
+ mUris.add(uri);
+ mSyncToNetwork = mSyncToNetwork || syncToNetwork;
+ }
+
+ public boolean isNotified(Uri uri) {
+ return mUris.contains(uri);
+ }
+
+ public int notificationCount() {
+ return mUris.size();
+ }
+
+ public boolean syncToNetwork() {
+ return mSyncToNetwork;
+ }
+
+ public void reset() {
+ mUris.clear();
+ mSyncToNetwork = false;
+ }
+}
diff --git a/src/com/android/photos/data/PhotoDatabase.java b/src/com/android/photos/data/PhotoDatabase.java
new file mode 100644
index 000000000..0c7b22730
--- /dev/null
+++ b/src/com/android/photos/data/PhotoDatabase.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.android.photos.data.PhotoProvider.Accounts;
+import com.android.photos.data.PhotoProvider.Albums;
+import com.android.photos.data.PhotoProvider.Metadata;
+import com.android.photos.data.PhotoProvider.Photos;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Used in PhotoProvider to create and access the database containing
+ * information about photo and video information stored on the server.
+ */
+public class PhotoDatabase extends SQLiteOpenHelper {
+ @SuppressWarnings("unused")
+ private static final String TAG = PhotoDatabase.class.getSimpleName();
+ static final int DB_VERSION = 3;
+
+ private static final String SQL_CREATE_TABLE = "CREATE TABLE ";
+
+ private static final String[][] CREATE_PHOTO = {
+ { Photos._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" },
+ // Photos.ACCOUNT_ID is a foreign key to Accounts._ID
+ { Photos.ACCOUNT_ID, "INTEGER NOT NULL" },
+ { Photos.WIDTH, "INTEGER NOT NULL" },
+ { Photos.HEIGHT, "INTEGER NOT NULL" },
+ { Photos.DATE_TAKEN, "INTEGER NOT NULL" },
+ // Photos.ALBUM_ID is a foreign key to Albums._ID
+ { Photos.ALBUM_ID, "INTEGER" },
+ { Photos.MIME_TYPE, "TEXT NOT NULL" },
+ { Photos.TITLE, "TEXT" },
+ { Photos.DATE_MODIFIED, "INTEGER" },
+ { Photos.ROTATION, "INTEGER" },
+ };
+
+ private static final String[][] CREATE_ALBUM = {
+ { Albums._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" },
+ // Albums.ACCOUNT_ID is a foreign key to Accounts._ID
+ { Albums.ACCOUNT_ID, "INTEGER NOT NULL" },
+ // Albums.PARENT_ID is a foreign key to Albums._ID
+ { Albums.PARENT_ID, "INTEGER" },
+ { Albums.ALBUM_TYPE, "TEXT" },
+ { Albums.VISIBILITY, "INTEGER NOT NULL" },
+ { Albums.LOCATION_STRING, "TEXT" },
+ { Albums.TITLE, "TEXT NOT NULL" },
+ { Albums.SUMMARY, "TEXT" },
+ { Albums.DATE_PUBLISHED, "INTEGER" },
+ { Albums.DATE_MODIFIED, "INTEGER" },
+ createUniqueConstraint(Albums.PARENT_ID, Albums.TITLE),
+ };
+
+ private static final String[][] CREATE_METADATA = {
+ { Metadata._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" },
+ // Metadata.PHOTO_ID is a foreign key to Photos._ID
+ { Metadata.PHOTO_ID, "INTEGER NOT NULL" },
+ { Metadata.KEY, "TEXT NOT NULL" },
+ { Metadata.VALUE, "TEXT NOT NULL" },
+ createUniqueConstraint(Metadata.PHOTO_ID, Metadata.KEY),
+ };
+
+ private static final String[][] CREATE_ACCOUNT = {
+ { Accounts._ID, "INTEGER PRIMARY KEY AUTOINCREMENT" },
+ { Accounts.ACCOUNT_NAME, "TEXT UNIQUE NOT NULL" },
+ };
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ createTable(db, Accounts.TABLE, getAccountTableDefinition());
+ createTable(db, Albums.TABLE, getAlbumTableDefinition());
+ createTable(db, Photos.TABLE, getPhotoTableDefinition());
+ createTable(db, Metadata.TABLE, getMetadataTableDefinition());
+ }
+
+ public PhotoDatabase(Context context, String dbName, int dbVersion) {
+ super(context, dbName, null, dbVersion);
+ }
+
+ public PhotoDatabase(Context context, String dbName) {
+ super(context, dbName, null, DB_VERSION);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ recreate(db);
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ recreate(db);
+ }
+
+ private void recreate(SQLiteDatabase db) {
+ dropTable(db, Metadata.TABLE);
+ dropTable(db, Photos.TABLE);
+ dropTable(db, Albums.TABLE);
+ dropTable(db, Accounts.TABLE);
+ onCreate(db);
+ }
+
+ protected List<String[]> getAlbumTableDefinition() {
+ return tableCreationStrings(CREATE_ALBUM);
+ }
+
+ protected List<String[]> getPhotoTableDefinition() {
+ return tableCreationStrings(CREATE_PHOTO);
+ }
+
+ protected List<String[]> getMetadataTableDefinition() {
+ return tableCreationStrings(CREATE_METADATA);
+ }
+
+ protected List<String[]> getAccountTableDefinition() {
+ return tableCreationStrings(CREATE_ACCOUNT);
+ }
+
+ protected static void createTable(SQLiteDatabase db, String table, List<String[]> columns) {
+ StringBuilder create = new StringBuilder(SQL_CREATE_TABLE);
+ create.append(table).append('(');
+ boolean first = true;
+ for (String[] column : columns) {
+ if (!first) {
+ create.append(',');
+ }
+ first = false;
+ for (String val: column) {
+ create.append(val).append(' ');
+ }
+ }
+ create.append(')');
+ db.beginTransaction();
+ try {
+ db.execSQL(create.toString());
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ protected static String[] createUniqueConstraint(String column1, String column2) {
+ return new String[] {
+ "UNIQUE(", column1, ",", column2, ")"
+ };
+ }
+
+ protected static List<String[]> tableCreationStrings(String[][] createTable) {
+ ArrayList<String[]> create = new ArrayList<String[]>(createTable.length);
+ for (String[] line: createTable) {
+ create.add(line);
+ }
+ return create;
+ }
+
+ protected static void addToTable(List<String[]> createTable, String[][] columns, String[][] constraints) {
+ if (columns != null) {
+ for (String[] column: columns) {
+ createTable.add(0, column);
+ }
+ }
+ if (constraints != null) {
+ for (String[] constraint: constraints) {
+ createTable.add(constraint);
+ }
+ }
+ }
+
+ protected static void dropTable(SQLiteDatabase db, String table) {
+ db.beginTransaction();
+ try {
+ db.execSQL("drop table if exists " + table);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+}
diff --git a/src/com/android/photos/data/PhotoProvider.java b/src/com/android/photos/data/PhotoProvider.java
new file mode 100644
index 000000000..d4310ca95
--- /dev/null
+++ b/src/com/android/photos/data/PhotoProvider.java
@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.provider.BaseColumns;
+
+import com.android.gallery3d.common.ApiHelper;
+
+import java.util.List;
+
+/**
+ * A provider that gives access to photo and video information for media stored
+ * on the server. Only media that is or will be put on the server will be
+ * accessed by this provider. Use Photos.CONTENT_URI to query all photos and
+ * videos. Use Albums.CONTENT_URI to query all albums. Use Metadata.CONTENT_URI
+ * to query metadata about a photo or video, based on the ID of the media. Use
+ * ImageCache.THUMBNAIL_CONTENT_URI, ImageCache.PREVIEW_CONTENT_URI, or
+ * ImageCache.ORIGINAL_CONTENT_URI to query the path of the thumbnail, preview,
+ * or original-sized image respectfully. <br/>
+ * To add or update metadata, use the update function rather than insert. All
+ * values for the metadata must be in the ContentValues, even if they are also
+ * in the selection. The selection and selectionArgs are not used when updating
+ * metadata. If the metadata values are null, the row will be deleted.
+ */
+public class PhotoProvider extends SQLiteContentProvider {
+ @SuppressWarnings("unused")
+ private static final String TAG = PhotoProvider.class.getSimpleName();
+
+ protected static final String DB_NAME = "photo.db";
+ public static final String AUTHORITY = PhotoProviderAuthority.AUTHORITY;
+ static final Uri BASE_CONTENT_URI = new Uri.Builder().scheme("content").authority(AUTHORITY)
+ .build();
+
+ // Used to allow mocking out the change notification because
+ // MockContextResolver disallows system-wide notification.
+ public static interface ChangeNotification {
+ void notifyChange(Uri uri, boolean syncToNetwork);
+ }
+
+ /**
+ * Contains columns that can be accessed via Accounts.CONTENT_URI
+ */
+ public static interface Accounts extends BaseColumns {
+ /**
+ * Internal database table used for account information
+ */
+ public static final String TABLE = "accounts";
+ /**
+ * Content URI for account information
+ */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
+ /**
+ * User name for this account.
+ */
+ public static final String ACCOUNT_NAME = "name";
+ }
+
+ /**
+ * Contains columns that can be accessed via Photos.CONTENT_URI.
+ */
+ public static interface Photos extends BaseColumns {
+ /**
+ * The image_type query parameter required for requesting a specific
+ * size of image.
+ */
+ public static final String MEDIA_SIZE_QUERY_PARAMETER = "media_size";
+
+ /** Internal database table used for basic photo information. */
+ public static final String TABLE = "photos";
+ /** Content URI for basic photo and video information. */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
+
+ /** Long foreign key to Accounts._ID */
+ public static final String ACCOUNT_ID = "account_id";
+ /** Column name for the width of the original image. Integer value. */
+ public static final String WIDTH = "width";
+ /** Column name for the height of the original image. Integer value. */
+ public static final String HEIGHT = "height";
+ /**
+ * Column name for the date that the original image was taken. Long
+ * value indicating the milliseconds since epoch in the GMT time zone.
+ */
+ public static final String DATE_TAKEN = "date_taken";
+ /**
+ * Column name indicating the long value of the album id that this image
+ * resides in. Will be NULL if it it has not been uploaded to the
+ * server.
+ */
+ public static final String ALBUM_ID = "album_id";
+ /** The column name for the mime-type String. */
+ public static final String MIME_TYPE = "mime_type";
+ /** The title of the photo. String value. */
+ public static final String TITLE = "title";
+ /** The date the photo entry was last updated. Long value. */
+ public static final String DATE_MODIFIED = "date_modified";
+ /**
+ * The rotation of the photo in degrees, if rotation has not already
+ * been applied. Integer value.
+ */
+ public static final String ROTATION = "rotation";
+ }
+
+ /**
+ * Contains columns and Uri for accessing album information.
+ */
+ public static interface Albums extends BaseColumns {
+ /** Internal database table used album information. */
+ public static final String TABLE = "albums";
+ /** Content URI for album information. */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
+
+ /** Long foreign key to Accounts._ID */
+ public static final String ACCOUNT_ID = "account_id";
+ /** Parent directory or null if this is in the root. */
+ public static final String PARENT_ID = "parent_id";
+ /** The type of album. Non-null, if album is auto-generated. String value. */
+ public static final String ALBUM_TYPE = "album_type";
+ /**
+ * Column name for the visibility level of the album. Can be any of the
+ * VISIBILITY_* values.
+ */
+ public static final String VISIBILITY = "visibility";
+ /** The user-specified location associated with the album. String value. */
+ public static final String LOCATION_STRING = "location_string";
+ /** The title of the album. String value. */
+ public static final String TITLE = "title";
+ /** A short summary of the contents of the album. String value. */
+ public static final String SUMMARY = "summary";
+ /** The date the album was created. Long value */
+ public static final String DATE_PUBLISHED = "date_published";
+ /** The date the album entry was last updated. Long value. */
+ public static final String DATE_MODIFIED = "date_modified";
+
+ // Privacy values for Albums.VISIBILITY
+ public static final int VISIBILITY_PRIVATE = 1;
+ public static final int VISIBILITY_SHARED = 2;
+ public static final int VISIBILITY_PUBLIC = 3;
+ }
+
+ /**
+ * Contains columns and Uri for accessing photo and video metadata
+ */
+ public static interface Metadata extends BaseColumns {
+ /** Internal database table used metadata information. */
+ public static final String TABLE = "metadata";
+ /** Content URI for photo and video metadata. */
+ public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE);
+ /** Foreign key to photo_id. Long value. */
+ public static final String PHOTO_ID = "photo_id";
+ /** Metadata key. String value */
+ public static final String KEY = "key";
+ /**
+ * Metadata value. Type is based on key.
+ */
+ public static final String VALUE = "value";
+
+ /** A short summary of the photo. String value. */
+ public static final String KEY_SUMMARY = "summary";
+ /** The date the photo was added. Long value. */
+ public static final String KEY_PUBLISHED = "date_published";
+ /** The date the photo was last updated. Long value. */
+ public static final String KEY_DATE_UPDATED = "date_updated";
+ /** The size of the photo is bytes. Integer value. */
+ public static final String KEY_SIZE_IN_BTYES = "size";
+ /** The latitude associated with the photo. Double value. */
+ public static final String KEY_LATITUDE = "latitude";
+ /** The longitude associated with the photo. Double value. */
+ public static final String KEY_LONGITUDE = "longitude";
+
+ /** The make of the camera used. String value. */
+ public static final String KEY_EXIF_MAKE = ExifInterface.TAG_MAKE;
+ /** The model of the camera used. String value. */
+ public static final String KEY_EXIF_MODEL = ExifInterface.TAG_MODEL;;
+ /** The exposure time used. Float value. */
+ public static final String KEY_EXIF_EXPOSURE = ExifInterface.TAG_EXPOSURE_TIME;
+ /** Whether the flash was used. Boolean value. */
+ public static final String KEY_EXIF_FLASH = ExifInterface.TAG_FLASH;
+ /** The focal length used. Float value. */
+ public static final String KEY_EXIF_FOCAL_LENGTH = ExifInterface.TAG_FOCAL_LENGTH;
+ /** The fstop value used. Float value. */
+ public static final String KEY_EXIF_FSTOP = ExifInterface.TAG_APERTURE;
+ /** The ISO equivalent value used. Integer value. */
+ public static final String KEY_EXIF_ISO = ExifInterface.TAG_ISO;
+ }
+
+ // SQL used within this class.
+ protected static final String WHERE_ID = BaseColumns._ID + " = ?";
+ protected static final String WHERE_METADATA_ID = Metadata.PHOTO_ID + " = ? AND "
+ + Metadata.KEY + " = ?";
+
+ protected static final String SELECT_ALBUM_ID = "SELECT " + Albums._ID + " FROM "
+ + Albums.TABLE;
+ protected static final String SELECT_PHOTO_ID = "SELECT " + Photos._ID + " FROM "
+ + Photos.TABLE;
+ protected static final String SELECT_PHOTO_COUNT = "SELECT COUNT(*) FROM " + Photos.TABLE;
+ protected static final String DELETE_PHOTOS = "DELETE FROM " + Photos.TABLE;
+ protected static final String DELETE_METADATA = "DELETE FROM " + Metadata.TABLE;
+ protected static final String SELECT_METADATA_COUNT = "SELECT COUNT(*) FROM " + Metadata.TABLE;
+ protected static final String WHERE = " WHERE ";
+ protected static final String IN = " IN ";
+ protected static final String NESTED_SELECT_START = "(";
+ protected static final String NESTED_SELECT_END = ")";
+ protected static final String[] PROJECTION_COUNT = {
+ "COUNT(*)"
+ };
+
+ /**
+ * For selecting the mime-type for an image.
+ */
+ private static final String[] PROJECTION_MIME_TYPE = {
+ Photos.MIME_TYPE,
+ };
+
+ protected static final String[] BASE_COLUMNS_ID = {
+ BaseColumns._ID,
+ };
+
+ protected ChangeNotification mNotifier = null;
+ protected static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ protected static final int MATCH_PHOTO = 1;
+ protected static final int MATCH_PHOTO_ID = 2;
+ protected static final int MATCH_ALBUM = 3;
+ protected static final int MATCH_ALBUM_ID = 4;
+ protected static final int MATCH_METADATA = 5;
+ protected static final int MATCH_METADATA_ID = 6;
+ protected static final int MATCH_ACCOUNT = 7;
+ protected static final int MATCH_ACCOUNT_ID = 8;
+
+ static {
+ sUriMatcher.addURI(AUTHORITY, Photos.TABLE, MATCH_PHOTO);
+ // match against Photos._ID
+ sUriMatcher.addURI(AUTHORITY, Photos.TABLE + "/#", MATCH_PHOTO_ID);
+ sUriMatcher.addURI(AUTHORITY, Albums.TABLE, MATCH_ALBUM);
+ // match against Albums._ID
+ sUriMatcher.addURI(AUTHORITY, Albums.TABLE + "/#", MATCH_ALBUM_ID);
+ sUriMatcher.addURI(AUTHORITY, Metadata.TABLE, MATCH_METADATA);
+ // match against metadata/<Metadata._ID>
+ sUriMatcher.addURI(AUTHORITY, Metadata.TABLE + "/#", MATCH_METADATA_ID);
+ sUriMatcher.addURI(AUTHORITY, Accounts.TABLE, MATCH_ACCOUNT);
+ // match against Accounts._ID
+ sUriMatcher.addURI(AUTHORITY, Accounts.TABLE + "/#", MATCH_ACCOUNT_ID);
+ }
+
+ @Override
+ public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
+ boolean callerIsSyncAdapter) {
+ int match = matchUri(uri);
+ selection = addIdToSelection(match, selection);
+ selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
+ return deleteCascade(uri, match, selection, selectionArgs);
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ Cursor cursor = query(uri, PROJECTION_MIME_TYPE, null, null, null);
+ String mimeType = null;
+ if (cursor.moveToNext()) {
+ mimeType = cursor.getString(0);
+ }
+ cursor.close();
+ return mimeType;
+ }
+
+ @Override
+ public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
+ int match = matchUri(uri);
+ validateMatchTable(match);
+ String table = getTableFromMatch(match, uri);
+ SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
+ Uri insertedUri = null;
+ long id = db.insert(table, null, values);
+ if (id != -1) {
+ // uri already matches the table.
+ insertedUri = ContentUris.withAppendedId(uri, id);
+ postNotifyUri(insertedUri);
+ }
+ return insertedUri;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ return query(uri, projection, selection, selectionArgs, sortOrder, null);
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder, CancellationSignal cancellationSignal) {
+ projection = replaceCount(projection);
+ int match = matchUri(uri);
+ selection = addIdToSelection(match, selection);
+ selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
+ String table = getTableFromMatch(match, uri);
+ Cursor c = query(table, projection, selection, selectionArgs, sortOrder, cancellationSignal);
+ if (c != null) {
+ c.setNotificationUri(getContext().getContentResolver(), uri);
+ }
+ return c;
+ }
+
+ @Override
+ public int updateInTransaction(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs, boolean callerIsSyncAdapter) {
+ int match = matchUri(uri);
+ int rowsUpdated = 0;
+ SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
+ if (match == MATCH_METADATA) {
+ rowsUpdated = modifyMetadata(db, values);
+ } else {
+ selection = addIdToSelection(match, selection);
+ selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs);
+ String table = getTableFromMatch(match, uri);
+ rowsUpdated = db.update(table, values, selection, selectionArgs);
+ }
+ postNotifyUri(uri);
+ return rowsUpdated;
+ }
+
+ public void setMockNotification(ChangeNotification notification) {
+ mNotifier = notification;
+ }
+
+ protected static String addIdToSelection(int match, String selection) {
+ String where;
+ switch (match) {
+ case MATCH_PHOTO_ID:
+ case MATCH_ALBUM_ID:
+ case MATCH_METADATA_ID:
+ where = WHERE_ID;
+ break;
+ default:
+ return selection;
+ }
+ return DatabaseUtils.concatenateWhere(selection, where);
+ }
+
+ protected static String[] addIdToSelectionArgs(int match, Uri uri, String[] selectionArgs) {
+ String[] whereArgs;
+ switch (match) {
+ case MATCH_PHOTO_ID:
+ case MATCH_ALBUM_ID:
+ case MATCH_METADATA_ID:
+ whereArgs = new String[] {
+ uri.getPathSegments().get(1),
+ };
+ break;
+ default:
+ return selectionArgs;
+ }
+ return DatabaseUtils.appendSelectionArgs(selectionArgs, whereArgs);
+ }
+
+ protected static String[] addMetadataKeysToSelectionArgs(String[] selectionArgs, Uri uri) {
+ List<String> segments = uri.getPathSegments();
+ String[] additionalArgs = {
+ segments.get(1),
+ segments.get(2),
+ };
+
+ return DatabaseUtils.appendSelectionArgs(selectionArgs, additionalArgs);
+ }
+
+ protected static String getTableFromMatch(int match, Uri uri) {
+ String table;
+ switch (match) {
+ case MATCH_PHOTO:
+ case MATCH_PHOTO_ID:
+ table = Photos.TABLE;
+ break;
+ case MATCH_ALBUM:
+ case MATCH_ALBUM_ID:
+ table = Albums.TABLE;
+ break;
+ case MATCH_METADATA:
+ case MATCH_METADATA_ID:
+ table = Metadata.TABLE;
+ break;
+ case MATCH_ACCOUNT:
+ case MATCH_ACCOUNT_ID:
+ table = Accounts.TABLE;
+ break;
+ default:
+ throw unknownUri(uri);
+ }
+ return table;
+ }
+
+ @Override
+ public SQLiteOpenHelper getDatabaseHelper(Context context) {
+ return new PhotoDatabase(context, DB_NAME);
+ }
+
+ private int modifyMetadata(SQLiteDatabase db, ContentValues values) {
+ int rowCount;
+ if (values.get(Metadata.VALUE) == null) {
+ String[] selectionArgs = {
+ values.getAsString(Metadata.PHOTO_ID), values.getAsString(Metadata.KEY),
+ };
+ rowCount = db.delete(Metadata.TABLE, WHERE_METADATA_ID, selectionArgs);
+ } else {
+ long rowId = db.replace(Metadata.TABLE, null, values);
+ rowCount = (rowId == -1) ? 0 : 1;
+ }
+ return rowCount;
+ }
+
+ private int matchUri(Uri uri) {
+ int match = sUriMatcher.match(uri);
+ if (match == UriMatcher.NO_MATCH) {
+ throw unknownUri(uri);
+ }
+ return match;
+ }
+
+ @Override
+ protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) {
+ if (mNotifier != null) {
+ mNotifier.notifyChange(uri, syncToNetwork);
+ } else {
+ super.notifyChange(resolver, uri, syncToNetwork);
+ }
+ }
+
+ protected static IllegalArgumentException unknownUri(Uri uri) {
+ return new IllegalArgumentException("Unknown Uri format: " + uri);
+ }
+
+ protected static String nestWhere(String matchColumn, String table, String nestedWhere) {
+ String query = SQLiteQueryBuilder.buildQueryString(false, table, BASE_COLUMNS_ID,
+ nestedWhere, null, null, null, null);
+ return matchColumn + IN + NESTED_SELECT_START + query + NESTED_SELECT_END;
+ }
+
+ protected static String metadataSelectionFromPhotos(String where) {
+ return nestWhere(Metadata.PHOTO_ID, Photos.TABLE, where);
+ }
+
+ protected static String photoSelectionFromAlbums(String where) {
+ return nestWhere(Photos.ALBUM_ID, Albums.TABLE, where);
+ }
+
+ protected static String photoSelectionFromAccounts(String where) {
+ return nestWhere(Photos.ACCOUNT_ID, Accounts.TABLE, where);
+ }
+
+ protected static String albumSelectionFromAccounts(String where) {
+ return nestWhere(Albums.ACCOUNT_ID, Accounts.TABLE, where);
+ }
+
+ protected int deleteCascade(Uri uri, int match, String selection, String[] selectionArgs) {
+ switch (match) {
+ case MATCH_PHOTO:
+ case MATCH_PHOTO_ID:
+ deleteCascade(Metadata.CONTENT_URI, MATCH_METADATA,
+ metadataSelectionFromPhotos(selection), selectionArgs);
+ break;
+ case MATCH_ALBUM:
+ case MATCH_ALBUM_ID:
+ deleteCascade(Photos.CONTENT_URI, MATCH_PHOTO,
+ photoSelectionFromAlbums(selection), selectionArgs);
+ break;
+ case MATCH_ACCOUNT:
+ case MATCH_ACCOUNT_ID:
+ deleteCascade(Photos.CONTENT_URI, MATCH_PHOTO,
+ photoSelectionFromAccounts(selection), selectionArgs);
+ deleteCascade(Albums.CONTENT_URI, MATCH_ALBUM,
+ albumSelectionFromAccounts(selection), selectionArgs);
+ break;
+ }
+ SQLiteDatabase db = getDatabaseHelper().getWritableDatabase();
+ String table = getTableFromMatch(match, uri);
+ int deleted = db.delete(table, selection, selectionArgs);
+ if (deleted > 0) {
+ postNotifyUri(uri);
+ }
+ return deleted;
+ }
+
+ private static void validateMatchTable(int match) {
+ switch (match) {
+ case MATCH_PHOTO:
+ case MATCH_ALBUM:
+ case MATCH_METADATA:
+ case MATCH_ACCOUNT:
+ break;
+ default:
+ throw new IllegalArgumentException("Operation not allowed on an existing row.");
+ }
+ }
+
+ protected Cursor query(String table, String[] columns, String selection,
+ String[] selectionArgs, String orderBy, CancellationSignal cancellationSignal) {
+ SQLiteDatabase db = getDatabaseHelper().getReadableDatabase();
+ if (ApiHelper.HAS_CANCELLATION_SIGNAL) {
+ return db.query(false, table, columns, selection, selectionArgs, null, null,
+ orderBy, null, cancellationSignal);
+ } else {
+ return db.query(table, columns, selection, selectionArgs, null, null, orderBy);
+ }
+ }
+
+ protected static String[] replaceCount(String[] projection) {
+ if (projection != null && projection.length == 1
+ && BaseColumns._COUNT.equals(projection[0])) {
+ return PROJECTION_COUNT;
+ }
+ return projection;
+ }
+}
diff --git a/src/com/android/photos/data/PhotoSetLoader.java b/src/com/android/photos/data/PhotoSetLoader.java
new file mode 100644
index 000000000..56c82c4a9
--- /dev/null
+++ b/src/com/android/photos/data/PhotoSetLoader.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Files;
+import android.provider.MediaStore.Files.FileColumns;
+
+import com.android.photos.drawables.DataUriThumbnailDrawable;
+import com.android.photos.shims.LoaderCompatShim;
+
+import java.util.ArrayList;
+
+public class PhotoSetLoader extends CursorLoader implements LoaderCompatShim<Cursor> {
+
+ public static final String SUPPORTED_OPERATIONS = "supported_operations";
+
+ private static final Uri CONTENT_URI = Files.getContentUri("external");
+ public static final String[] PROJECTION = new String[] {
+ FileColumns._ID,
+ FileColumns.DATA,
+ FileColumns.WIDTH,
+ FileColumns.HEIGHT,
+ FileColumns.DATE_ADDED,
+ FileColumns.MEDIA_TYPE,
+ SUPPORTED_OPERATIONS,
+ };
+
+ private static final String SORT_ORDER = FileColumns.DATE_ADDED + " DESC";
+ private static final String SELECTION =
+ FileColumns.MEDIA_TYPE + " == " + FileColumns.MEDIA_TYPE_IMAGE
+ + " OR "
+ + FileColumns.MEDIA_TYPE + " == " + FileColumns.MEDIA_TYPE_VIDEO;
+
+ public static final int INDEX_ID = 0;
+ public static final int INDEX_DATA = 1;
+ public static final int INDEX_WIDTH = 2;
+ public static final int INDEX_HEIGHT = 3;
+ public static final int INDEX_DATE_ADDED = 4;
+ public static final int INDEX_MEDIA_TYPE = 5;
+ public static final int INDEX_SUPPORTED_OPERATIONS = 6;
+
+ private static final Uri GLOBAL_CONTENT_URI = Uri.parse("content://" + MediaStore.AUTHORITY + "/external/");
+ private final ContentObserver mGlobalObserver = new ForceLoadContentObserver();
+
+ public PhotoSetLoader(Context context) {
+ super(context, CONTENT_URI, PROJECTION, SELECTION, null, SORT_ORDER);
+ }
+
+ @Override
+ protected void onStartLoading() {
+ super.onStartLoading();
+ getContext().getContentResolver().registerContentObserver(GLOBAL_CONTENT_URI,
+ true, mGlobalObserver);
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+ getContext().getContentResolver().unregisterContentObserver(mGlobalObserver);
+ }
+
+ @Override
+ public Drawable drawableForItem(Cursor item, Drawable recycle) {
+ DataUriThumbnailDrawable drawable = null;
+ if (recycle == null || !(recycle instanceof DataUriThumbnailDrawable)) {
+ drawable = new DataUriThumbnailDrawable();
+ } else {
+ drawable = (DataUriThumbnailDrawable) recycle;
+ }
+ drawable.setImage(item.getString(INDEX_DATA),
+ item.getInt(INDEX_WIDTH), item.getInt(INDEX_HEIGHT));
+ return drawable;
+ }
+
+ @Override
+ public Uri uriForItem(Cursor item) {
+ return null;
+ }
+
+ @Override
+ public ArrayList<Uri> urisForSubItems(Cursor item) {
+ return null;
+ }
+
+ @Override
+ public void deleteItemWithPath(Object path) {
+
+ }
+
+ @Override
+ public Object getPathForItem(Cursor item) {
+ return null;
+ }
+}
diff --git a/src/com/android/photos/data/SQLiteContentProvider.java b/src/com/android/photos/data/SQLiteContentProvider.java
new file mode 100644
index 000000000..daffa6e79
--- /dev/null
+++ b/src/com/android/photos/data/SQLiteContentProvider.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * General purpose {@link ContentProvider} base class that uses SQLiteDatabase
+ * for storage.
+ */
+public abstract class SQLiteContentProvider extends ContentProvider {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "SQLiteContentProvider";
+
+ private SQLiteOpenHelper mOpenHelper;
+ private Set<Uri> mChangedUris;
+
+ private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
+ private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
+
+ /**
+ * Maximum number of operations allowed in a batch between yield points.
+ */
+ private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
+
+ @Override
+ public boolean onCreate() {
+ Context context = getContext();
+ mOpenHelper = getDatabaseHelper(context);
+ mChangedUris = new HashSet<Uri>();
+ return true;
+ }
+
+ @Override
+ public void shutdown() {
+ getDatabaseHelper().close();
+ }
+
+ /**
+ * Returns a {@link SQLiteOpenHelper} that can open the database.
+ */
+ public abstract SQLiteOpenHelper getDatabaseHelper(Context context);
+
+ /**
+ * The equivalent of the {@link #insert} method, but invoked within a
+ * transaction.
+ */
+ public abstract Uri insertInTransaction(Uri uri, ContentValues values,
+ boolean callerIsSyncAdapter);
+
+ /**
+ * The equivalent of the {@link #update} method, but invoked within a
+ * transaction.
+ */
+ public abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs, boolean callerIsSyncAdapter);
+
+ /**
+ * The equivalent of the {@link #delete} method, but invoked within a
+ * transaction.
+ */
+ public abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
+ boolean callerIsSyncAdapter);
+
+ /**
+ * Call this to add a URI to the list of URIs to be notified when the
+ * transaction is committed.
+ */
+ protected void postNotifyUri(Uri uri) {
+ synchronized (mChangedUris) {
+ mChangedUris.add(uri);
+ }
+ }
+
+ public boolean isCallerSyncAdapter(Uri uri) {
+ return false;
+ }
+
+ public SQLiteOpenHelper getDatabaseHelper() {
+ return mOpenHelper;
+ }
+
+ private boolean applyingBatch() {
+ return mApplyingBatch.get() != null && mApplyingBatch.get();
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ Uri result = null;
+ boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+ boolean applyingBatch = applyingBatch();
+ if (!applyingBatch) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ result = insertInTransaction(uri, values, callerIsSyncAdapter);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ onEndTransaction(callerIsSyncAdapter);
+ } else {
+ result = insertInTransaction(uri, values, callerIsSyncAdapter);
+ }
+ return result;
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] values) {
+ int numValues = values.length;
+ boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ for (int i = 0; i < numValues; i++) {
+ @SuppressWarnings("unused")
+ Uri result = insertInTransaction(uri, values[i], callerIsSyncAdapter);
+ db.yieldIfContendedSafely();
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ onEndTransaction(callerIsSyncAdapter);
+ return numValues;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ int count = 0;
+ boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+ boolean applyingBatch = applyingBatch();
+ if (!applyingBatch) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ count = updateInTransaction(uri, values, selection, selectionArgs,
+ callerIsSyncAdapter);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ onEndTransaction(callerIsSyncAdapter);
+ } else {
+ count = updateInTransaction(uri, values, selection, selectionArgs, callerIsSyncAdapter);
+ }
+
+ return count;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ int count = 0;
+ boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+ boolean applyingBatch = applyingBatch();
+ if (!applyingBatch) {
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ onEndTransaction(callerIsSyncAdapter);
+ } else {
+ count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter);
+ }
+ return count;
+ }
+
+ @Override
+ public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+ throws OperationApplicationException {
+ int ypCount = 0;
+ int opCount = 0;
+ boolean callerIsSyncAdapter = false;
+ SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+ db.beginTransaction();
+ try {
+ mApplyingBatch.set(true);
+ final int numOperations = operations.size();
+ final ContentProviderResult[] results = new ContentProviderResult[numOperations];
+ for (int i = 0; i < numOperations; i++) {
+ if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
+ throw new OperationApplicationException(
+ "Too many content provider operations between yield points. "
+ + "The maximum number of operations per yield point is "
+ + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
+ }
+ final ContentProviderOperation operation = operations.get(i);
+ if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri())) {
+ callerIsSyncAdapter = true;
+ }
+ if (i > 0 && operation.isYieldAllowed()) {
+ opCount = 0;
+ if (db.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) {
+ ypCount++;
+ }
+ }
+ results[i] = operation.apply(this, results, i);
+ }
+ db.setTransactionSuccessful();
+ return results;
+ } finally {
+ mApplyingBatch.set(false);
+ db.endTransaction();
+ onEndTransaction(callerIsSyncAdapter);
+ }
+ }
+
+ protected Set<Uri> onEndTransaction(boolean callerIsSyncAdapter) {
+ Set<Uri> changed;
+ synchronized (mChangedUris) {
+ changed = new HashSet<Uri>(mChangedUris);
+ mChangedUris.clear();
+ }
+ ContentResolver resolver = getContext().getContentResolver();
+ for (Uri uri : changed) {
+ boolean syncToNetwork = !callerIsSyncAdapter && syncToNetwork(uri);
+ notifyChange(resolver, uri, syncToNetwork);
+ }
+ return changed;
+ }
+
+ protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) {
+ resolver.notifyChange(uri, null, syncToNetwork);
+ }
+
+ protected boolean syncToNetwork(Uri uri) {
+ return false;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/photos/data/SparseArrayBitmapPool.java b/src/com/android/photos/data/SparseArrayBitmapPool.java
new file mode 100644
index 000000000..95e10267b
--- /dev/null
+++ b/src/com/android/photos/data/SparseArrayBitmapPool.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2013 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.photos.data;
+
+import android.graphics.Bitmap;
+import android.util.SparseArray;
+
+import android.util.Pools.Pool;
+import android.util.Pools.SimplePool;
+
+/**
+ * Bitmap pool backed by a sparse array indexing linked lists of bitmaps
+ * sharing the same width. Performance will degrade if using this to store
+ * many bitmaps with the same width but many different heights.
+ */
+public class SparseArrayBitmapPool {
+
+ private int mCapacityBytes;
+ private SparseArray<Node> mStore = new SparseArray<Node>();
+ private int mSizeBytes = 0;
+
+ private Pool<Node> mNodePool;
+ private Node mPoolNodesHead = null;
+ private Node mPoolNodesTail = null;
+
+ protected static class Node {
+ Bitmap bitmap;
+
+ // Each node is part of two doubly linked lists:
+ // - A pool-level list (accessed by mPoolNodesHead and mPoolNodesTail)
+ // that is used for FIFO eviction of nodes when the pool gets full.
+ // - A bucket-level list for each index of the sparse array, so that
+ // each index can store more than one item.
+ Node prevInBucket;
+ Node nextInBucket;
+ Node nextInPool;
+ Node prevInPool;
+ }
+
+ /**
+ * @param capacityBytes Maximum capacity of the pool in bytes.
+ * @param nodePool Shared pool to use for recycling linked list nodes, or null.
+ */
+ public SparseArrayBitmapPool(int capacityBytes, Pool<Node> nodePool) {
+ mCapacityBytes = capacityBytes;
+ if (nodePool == null) {
+ mNodePool = new SimplePool<Node>(32);
+ } else {
+ mNodePool = nodePool;
+ }
+ }
+
+ /**
+ * Set the maximum capacity of the pool, and if necessary trim it down to size.
+ */
+ public synchronized void setCapacity(int capacityBytes) {
+ mCapacityBytes = capacityBytes;
+
+ // No-op unless current size exceeds the new capacity.
+ freeUpCapacity(0);
+ }
+
+ private void freeUpCapacity(int bytesNeeded) {
+ int targetSize = mCapacityBytes - bytesNeeded;
+ // Repeatedly remove the oldest node until we have freed up at least bytesNeeded.
+ while (mPoolNodesTail != null && mSizeBytes > targetSize) {
+ unlinkAndRecycleNode(mPoolNodesTail, true);
+ }
+ }
+
+ private void unlinkAndRecycleNode(Node n, boolean recycleBitmap) {
+ // Unlink the node from its sparse array bucket list.
+ if (n.prevInBucket != null) {
+ // This wasn't the head, update the previous node.
+ n.prevInBucket.nextInBucket = n.nextInBucket;
+ } else {
+ // This was the head of the bucket, replace it with the next node.
+ mStore.put(n.bitmap.getWidth(), n.nextInBucket);
+ }
+ if (n.nextInBucket != null) {
+ // This wasn't the tail, update the next node.
+ n.nextInBucket.prevInBucket = n.prevInBucket;
+ }
+
+ // Unlink the node from the pool-wide list.
+ if (n.prevInPool != null) {
+ // This wasn't the head, update the previous node.
+ n.prevInPool.nextInPool = n.nextInPool;
+ } else {
+ // This was the head of the pool-wide list, update the head pointer.
+ mPoolNodesHead = n.nextInPool;
+ }
+ if (n.nextInPool != null) {
+ // This wasn't the tail, update the next node.
+ n.nextInPool.prevInPool = n.prevInPool;
+ } else {
+ // This was the tail, update the tail pointer.
+ mPoolNodesTail = n.prevInPool;
+ }
+
+ // Recycle the node.
+ n.nextInBucket = null;
+ n.nextInPool = null;
+ n.prevInBucket = null;
+ n.prevInPool = null;
+ mSizeBytes -= n.bitmap.getByteCount();
+ if (recycleBitmap) n.bitmap.recycle();
+ n.bitmap = null;
+ mNodePool.release(n);
+ }
+
+ /**
+ * @return Capacity of the pool in bytes.
+ */
+ public synchronized int getCapacity() {
+ return mCapacityBytes;
+ }
+
+ /**
+ * @return Total size in bytes of the bitmaps stored in the pool.
+ */
+ public synchronized int getSize() {
+ return mSizeBytes;
+ }
+
+ /**
+ * @return Bitmap from the pool with the desired height/width or null if none available.
+ */
+ public synchronized Bitmap get(int width, int height) {
+ Node cur = mStore.get(width);
+
+ // Traverse the list corresponding to the width bucket in the
+ // sparse array, and unlink and return the first bitmap that
+ // also has the correct height.
+ while (cur != null) {
+ if (cur.bitmap.getHeight() == height) {
+ Bitmap b = cur.bitmap;
+ unlinkAndRecycleNode(cur, false);
+ return b;
+ }
+ cur = cur.nextInBucket;
+ }
+ return null;
+ }
+
+ /**
+ * Adds the given bitmap to the pool.
+ * @return Whether the bitmap was added to the pool.
+ */
+ public synchronized boolean put(Bitmap b) {
+ if (b == null) {
+ return false;
+ }
+
+ // Ensure there is enough room to contain the new bitmap.
+ int bytes = b.getByteCount();
+ freeUpCapacity(bytes);
+
+ Node newNode = mNodePool.acquire();
+ if (newNode == null) {
+ newNode = new Node();
+ }
+ newNode.bitmap = b;
+
+ // We append to the head, and freeUpCapacity clears from the tail,
+ // resulting in FIFO eviction.
+ newNode.prevInBucket = null;
+ newNode.prevInPool = null;
+ newNode.nextInPool = mPoolNodesHead;
+ mPoolNodesHead = newNode;
+
+ // Insert the node into its appropriate bucket based on width.
+ int key = b.getWidth();
+ newNode.nextInBucket = mStore.get(key);
+ if (newNode.nextInBucket != null) {
+ // The bucket already had nodes, update the old head.
+ newNode.nextInBucket.prevInBucket = newNode;
+ }
+ mStore.put(key, newNode);
+
+ if (newNode.nextInPool == null) {
+ // This is the only node in the list, update the tail pointer.
+ mPoolNodesTail = newNode;
+ } else {
+ newNode.nextInPool.prevInPool = newNode;
+ }
+ mSizeBytes += bytes;
+ return true;
+ }
+
+ /**
+ * Empty the pool, recycling all the bitmaps currently in it.
+ */
+ public synchronized void clear() {
+ // Clearing is equivalent to ensuring all the capacity is available.
+ freeUpCapacity(mCapacityBytes);
+ }
+}
diff --git a/src/com/android/photos/drawables/AutoThumbnailDrawable.java b/src/com/android/photos/drawables/AutoThumbnailDrawable.java
new file mode 100644
index 000000000..b51b6709f
--- /dev/null
+++ b/src/com/android/photos/drawables/AutoThumbnailDrawable.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2013 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.photos.drawables;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import com.android.photos.data.GalleryBitmapPool;
+
+import java.io.InputStream;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public abstract class AutoThumbnailDrawable<T> extends Drawable {
+
+ private static final String TAG = "AutoThumbnailDrawable";
+
+ private static ExecutorService sThreadPool = Executors.newSingleThreadExecutor();
+ private static GalleryBitmapPool sBitmapPool = GalleryBitmapPool.getInstance();
+ private static byte[] sTempStorage = new byte[64 * 1024];
+
+ // UI thread only
+ private Paint mPaint = new Paint();
+ private Matrix mDrawMatrix = new Matrix();
+
+ // Decoder thread only
+ private BitmapFactory.Options mOptions = new BitmapFactory.Options();
+
+ // Shared, guarded by mLock
+ private Object mLock = new Object();
+ private Bitmap mBitmap;
+ protected T mData;
+ private boolean mIsQueued;
+ private int mImageWidth, mImageHeight;
+ private Rect mBounds = new Rect();
+ private int mSampleSize = 1;
+
+ public AutoThumbnailDrawable() {
+ mPaint.setAntiAlias(true);
+ mPaint.setFilterBitmap(true);
+ mDrawMatrix.reset();
+ mOptions.inTempStorage = sTempStorage;
+ }
+
+ protected abstract byte[] getPreferredImageBytes(T data);
+ protected abstract InputStream getFallbackImageStream(T data);
+ protected abstract boolean dataChangedLocked(T data);
+
+ public void setImage(T data, int width, int height) {
+ if (!dataChangedLocked(data)) return;
+ synchronized (mLock) {
+ mImageWidth = width;
+ mImageHeight = height;
+ mData = data;
+ setBitmapLocked(null);
+ refreshSampleSizeLocked();
+ }
+ invalidateSelf();
+ }
+
+ private void setBitmapLocked(Bitmap b) {
+ if (b == mBitmap) {
+ return;
+ }
+ if (mBitmap != null) {
+ sBitmapPool.put(mBitmap);
+ }
+ mBitmap = b;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ synchronized (mLock) {
+ mBounds.set(bounds);
+ if (mBounds.isEmpty()) {
+ mBitmap = null;
+ } else {
+ refreshSampleSizeLocked();
+ updateDrawMatrixLocked();
+ }
+ }
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mBitmap != null) {
+ canvas.save();
+ canvas.clipRect(mBounds);
+ canvas.concat(mDrawMatrix);
+ canvas.drawBitmap(mBitmap, 0, 0, mPaint);
+ canvas.restore();
+ } else {
+ // TODO: Draw placeholder...?
+ }
+ }
+
+ private void updateDrawMatrixLocked() {
+ if (mBitmap == null || mBounds.isEmpty()) {
+ mDrawMatrix.reset();
+ return;
+ }
+
+ float scale;
+ float dx = 0, dy = 0;
+
+ int dwidth = mBitmap.getWidth();
+ int dheight = mBitmap.getHeight();
+ int vwidth = mBounds.width();
+ int vheight = mBounds.height();
+
+ // Calculates a matrix similar to ScaleType.CENTER_CROP
+ if (dwidth * vheight > vwidth * dheight) {
+ scale = (float) vheight / (float) dheight;
+ dx = (vwidth - dwidth * scale) * 0.5f;
+ } else {
+ scale = (float) vwidth / (float) dwidth;
+ dy = (vheight - dheight * scale) * 0.5f;
+ }
+ if (scale < .8f) {
+ Log.w(TAG, "sample size was too small! Overdrawing! " + scale + ", " + mSampleSize);
+ } else if (scale > 1.5f) {
+ Log.w(TAG, "Potential quality loss! " + scale + ", " + mSampleSize);
+ }
+
+ mDrawMatrix.setScale(scale, scale);
+ mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
+ }
+
+ private int calculateSampleSizeLocked(int dwidth, int dheight) {
+ float scale;
+
+ int vwidth = mBounds.width();
+ int vheight = mBounds.height();
+
+ // Inverse of updateDrawMatrixLocked
+ if (dwidth * vheight > vwidth * dheight) {
+ scale = (float) dheight / (float) vheight;
+ } else {
+ scale = (float) dwidth / (float) vwidth;
+ }
+ int result = Math.round(scale);
+ return result > 0 ? result : 1;
+ }
+
+ private void refreshSampleSizeLocked() {
+ if (mBounds.isEmpty() || mImageWidth == 0 || mImageHeight == 0) {
+ return;
+ }
+
+ int sampleSize = calculateSampleSizeLocked(mImageWidth, mImageHeight);
+ if (sampleSize != mSampleSize || mBitmap == null) {
+ mSampleSize = sampleSize;
+ loadBitmapLocked();
+ }
+ }
+
+ private void loadBitmapLocked() {
+ if (!mIsQueued && !mBounds.isEmpty()) {
+ unscheduleSelf(mUpdateBitmap);
+ sThreadPool.execute(mLoadBitmap);
+ mIsQueued = true;
+ }
+ }
+
+ public float getAspectRatio() {
+ return (float) mImageWidth / (float) mImageHeight;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return -1;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return -1;
+ }
+
+ @Override
+ public int getOpacity() {
+ Bitmap bm = mBitmap;
+ return (bm == null || bm.hasAlpha() || mPaint.getAlpha() < 255) ?
+ PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ int oldAlpha = mPaint.getAlpha();
+ if (alpha != oldAlpha) {
+ mPaint.setAlpha(alpha);
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ invalidateSelf();
+ }
+
+ private final Runnable mLoadBitmap = new Runnable() {
+ @Override
+ public void run() {
+ T data;
+ synchronized (mLock) {
+ data = mData;
+ }
+ int preferredSampleSize = 1;
+ byte[] preferred = getPreferredImageBytes(data);
+ boolean hasPreferred = (preferred != null && preferred.length > 0);
+ if (hasPreferred) {
+ mOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(preferred, 0, preferred.length, mOptions);
+ mOptions.inJustDecodeBounds = false;
+ }
+ int sampleSize, width, height;
+ synchronized (mLock) {
+ if (dataChangedLocked(data)) {
+ return;
+ }
+ width = mImageWidth;
+ height = mImageHeight;
+ if (hasPreferred) {
+ preferredSampleSize = calculateSampleSizeLocked(
+ mOptions.outWidth, mOptions.outHeight);
+ }
+ sampleSize = calculateSampleSizeLocked(width, height);
+ mIsQueued = false;
+ }
+ Bitmap b = null;
+ InputStream is = null;
+ try {
+ if (hasPreferred) {
+ mOptions.inSampleSize = preferredSampleSize;
+ mOptions.inBitmap = sBitmapPool.get(
+ mOptions.outWidth / preferredSampleSize,
+ mOptions.outHeight / preferredSampleSize);
+ b = BitmapFactory.decodeByteArray(preferred, 0, preferred.length, mOptions);
+ if (mOptions.inBitmap != null && b != mOptions.inBitmap) {
+ sBitmapPool.put(mOptions.inBitmap);
+ mOptions.inBitmap = null;
+ }
+ }
+ if (b == null) {
+ is = getFallbackImageStream(data);
+ mOptions.inSampleSize = sampleSize;
+ mOptions.inBitmap = sBitmapPool.get(width / sampleSize, height / sampleSize);
+ b = BitmapFactory.decodeStream(is, null, mOptions);
+ if (mOptions.inBitmap != null && b != mOptions.inBitmap) {
+ sBitmapPool.put(mOptions.inBitmap);
+ mOptions.inBitmap = null;
+ }
+ }
+ } catch (Exception e) {
+ Log.d(TAG, "Failed to fetch bitmap", e);
+ return;
+ } finally {
+ try {
+ if (is != null) {
+ is.close();
+ }
+ } catch (Exception e) {}
+ if (b != null) {
+ synchronized (mLock) {
+ if (!dataChangedLocked(data)) {
+ setBitmapLocked(b);
+ scheduleSelf(mUpdateBitmap, 0);
+ }
+ }
+ }
+ }
+ }
+ };
+
+ private final Runnable mUpdateBitmap = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (AutoThumbnailDrawable.this) {
+ updateDrawMatrixLocked();
+ invalidateSelf();
+ }
+ }
+ };
+
+}
diff --git a/src/com/android/photos/drawables/DataUriThumbnailDrawable.java b/src/com/android/photos/drawables/DataUriThumbnailDrawable.java
new file mode 100644
index 000000000..c83b0c8fa
--- /dev/null
+++ b/src/com/android/photos/drawables/DataUriThumbnailDrawable.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013 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.photos.drawables;
+
+import android.media.ExifInterface;
+import android.text.TextUtils;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class DataUriThumbnailDrawable extends AutoThumbnailDrawable<String> {
+
+ @Override
+ protected byte[] getPreferredImageBytes(String data) {
+ byte[] thumbnail = null;
+ try {
+ ExifInterface exif = new ExifInterface(data);
+ if (exif.hasThumbnail()) {
+ thumbnail = exif.getThumbnail();
+ }
+ } catch (IOException e) { }
+ return thumbnail;
+ }
+
+ @Override
+ protected InputStream getFallbackImageStream(String data) {
+ try {
+ return new FileInputStream(data);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ @Override
+ protected boolean dataChangedLocked(String data) {
+ return !TextUtils.equals(mData, data);
+ }
+}
diff --git a/src/com/android/photos/shims/BitmapJobDrawable.java b/src/com/android/photos/shims/BitmapJobDrawable.java
new file mode 100644
index 000000000..32dbc8078
--- /dev/null
+++ b/src/com/android/photos/shims/BitmapJobDrawable.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2013 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.photos.shims;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.ui.BitmapLoader;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.FutureListener;
+import com.android.gallery3d.util.ThreadPool;
+import com.android.photos.data.GalleryBitmapPool;
+
+
+public class BitmapJobDrawable extends Drawable implements Runnable {
+
+ private ThumbnailLoader mLoader;
+ private MediaItem mItem;
+ private Bitmap mBitmap;
+ private Paint mPaint = new Paint();
+ private Matrix mDrawMatrix = new Matrix();
+ private int mRotation = 0;
+
+ public BitmapJobDrawable() {
+ }
+
+ public void setMediaItem(MediaItem item) {
+ if (mItem == item) return;
+
+ if (mLoader != null) {
+ mLoader.cancelLoad();
+ }
+ mItem = item;
+ if (mBitmap != null) {
+ GalleryBitmapPool.getInstance().put(mBitmap);
+ mBitmap = null;
+ }
+ if (mItem != null) {
+ // TODO: Figure out why ThumbnailLoader doesn't like to be re-used
+ mLoader = new ThumbnailLoader(this);
+ mLoader.startLoad();
+ mRotation = mItem.getRotation();
+ }
+ invalidateSelf();
+ }
+
+ @Override
+ public void run() {
+ Bitmap bitmap = mLoader.getBitmap();
+ if (bitmap != null) {
+ mBitmap = bitmap;
+ updateDrawMatrix();
+ }
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ updateDrawMatrix();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ Rect bounds = getBounds();
+ if (mBitmap != null) {
+ canvas.save();
+ canvas.clipRect(bounds);
+ canvas.concat(mDrawMatrix);
+ canvas.rotate(mRotation, bounds.centerX(), bounds.centerY());
+ canvas.drawBitmap(mBitmap, 0, 0, mPaint);
+ canvas.restore();
+ } else {
+ mPaint.setColor(0xFFCCCCCC);
+ canvas.drawRect(bounds, mPaint);
+ }
+ }
+
+ private void updateDrawMatrix() {
+ Rect bounds = getBounds();
+ if (mBitmap == null || bounds.isEmpty()) {
+ mDrawMatrix.reset();
+ return;
+ }
+
+ float scale;
+ float dx = 0, dy = 0;
+
+ int dwidth = mBitmap.getWidth();
+ int dheight = mBitmap.getHeight();
+ int vwidth = bounds.width();
+ int vheight = bounds.height();
+
+ // Calculates a matrix similar to ScaleType.CENTER_CROP
+ if (dwidth * vheight > vwidth * dheight) {
+ scale = (float) vheight / (float) dheight;
+ dx = (vwidth - dwidth * scale) * 0.5f;
+ } else {
+ scale = (float) vwidth / (float) dwidth;
+ dy = (vheight - dheight * scale) * 0.5f;
+ }
+
+ mDrawMatrix.setScale(scale, scale);
+ mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
+ invalidateSelf();
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL);
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL);
+ }
+
+ @Override
+ public int getOpacity() {
+ Bitmap bm = mBitmap;
+ return (bm == null || bm.hasAlpha() || mPaint.getAlpha() < 255) ?
+ PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ int oldAlpha = mPaint.getAlpha();
+ if (alpha != oldAlpha) {
+ mPaint.setAlpha(alpha);
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ invalidateSelf();
+ }
+
+ private static class ThumbnailLoader extends BitmapLoader {
+ private static final ThreadPool sThreadPool = new ThreadPool(0, 2);
+ private BitmapJobDrawable mParent;
+
+ public ThumbnailLoader(BitmapJobDrawable parent) {
+ mParent = parent;
+ }
+
+ @Override
+ protected Future<Bitmap> submitBitmapTask(FutureListener<Bitmap> l) {
+ return sThreadPool.submit(
+ mParent.mItem.requestImage(MediaItem.TYPE_MICROTHUMBNAIL), this);
+ }
+
+ @Override
+ protected void onLoadComplete(Bitmap bitmap) {
+ mParent.scheduleSelf(mParent, 0);
+ }
+ }
+
+}
diff --git a/src/com/android/photos/shims/LoaderCompatShim.java b/src/com/android/photos/shims/LoaderCompatShim.java
new file mode 100644
index 000000000..d5bf710de
--- /dev/null
+++ b/src/com/android/photos/shims/LoaderCompatShim.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2013 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.photos.shims;
+
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+
+import java.util.ArrayList;
+
+
+public interface LoaderCompatShim<T> {
+ Drawable drawableForItem(T item, Drawable recycle);
+ Uri uriForItem(T item);
+ ArrayList<Uri> urisForSubItems(T item);
+ void deleteItemWithPath(Object path);
+ Object getPathForItem(T item);
+}
diff --git a/src/com/android/photos/shims/MediaItemsLoader.java b/src/com/android/photos/shims/MediaItemsLoader.java
new file mode 100644
index 000000000..6142355a9
--- /dev/null
+++ b/src/com/android/photos/shims/MediaItemsLoader.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2013 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.photos.shims;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.MediaStore.Files.FileColumns;
+import android.util.SparseArray;
+
+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.MediaSet.ItemConsumer;
+import com.android.gallery3d.data.MediaSet.SyncListener;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.Future;
+import com.android.photos.data.PhotoSetLoader;
+
+import java.util.ArrayList;
+
+/**
+ * Returns all MediaItems in a MediaSet, wrapping them in a cursor to appear
+ * like a PhotoSetLoader
+ */
+public class MediaItemsLoader extends AsyncTaskLoader<Cursor> implements LoaderCompatShim<Cursor> {
+
+ private static final SyncListener sNullListener = new SyncListener() {
+ @Override
+ public void onSyncDone(MediaSet mediaSet, int resultCode) {
+ }
+ };
+
+ private final MediaSet mMediaSet;
+ private final DataManager mDataManager;
+ private Future<Integer> mSyncTask = null;
+ private ContentListener mObserver = new ContentListener() {
+ @Override
+ public void onContentDirty() {
+ onContentChanged();
+ }
+ };
+ private SparseArray<MediaItem> mMediaItems;
+
+ public MediaItemsLoader(Context context) {
+ super(context);
+ mDataManager = DataManager.from(context);
+ String path = mDataManager.getTopSetPath(DataManager.INCLUDE_ALL);
+ mMediaSet = mDataManager.getMediaSet(path);
+ }
+
+ public MediaItemsLoader(Context context, String parentPath) {
+ super(context);
+ mDataManager = DataManager.from(getContext());
+ mMediaSet = mDataManager.getMediaSet(parentPath);
+ }
+
+ @Override
+ protected void onStartLoading() {
+ super.onStartLoading();
+ mMediaSet.addContentListener(mObserver);
+ mSyncTask = mMediaSet.requestSync(sNullListener);
+ forceLoad();
+ }
+
+ @Override
+ protected boolean onCancelLoad() {
+ if (mSyncTask != null) {
+ mSyncTask.cancel();
+ mSyncTask = null;
+ }
+ return super.onCancelLoad();
+ }
+
+ @Override
+ protected void onStopLoading() {
+ super.onStopLoading();
+ cancelLoad();
+ mMediaSet.removeContentListener(mObserver);
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+ onStopLoading();
+ }
+
+ @Override
+ public Cursor loadInBackground() {
+ // TODO: This probably doesn't work
+ mMediaSet.reload();
+ final MatrixCursor cursor = new MatrixCursor(PhotoSetLoader.PROJECTION);
+ final Object[] row = new Object[PhotoSetLoader.PROJECTION.length];
+ final SparseArray<MediaItem> mediaItems = new SparseArray<MediaItem>();
+ mMediaSet.enumerateTotalMediaItems(new ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ row[PhotoSetLoader.INDEX_ID] = index;
+ row[PhotoSetLoader.INDEX_DATA] = item.getContentUri().toString();
+ row[PhotoSetLoader.INDEX_DATE_ADDED] = item.getDateInMs();
+ row[PhotoSetLoader.INDEX_HEIGHT] = item.getHeight();
+ row[PhotoSetLoader.INDEX_WIDTH] = item.getWidth();
+ row[PhotoSetLoader.INDEX_WIDTH] = item.getWidth();
+ int rawMediaType = item.getMediaType();
+ int mappedMediaType = FileColumns.MEDIA_TYPE_NONE;
+ if (rawMediaType == MediaItem.MEDIA_TYPE_IMAGE) {
+ mappedMediaType = FileColumns.MEDIA_TYPE_IMAGE;
+ } else if (rawMediaType == MediaItem.MEDIA_TYPE_VIDEO) {
+ mappedMediaType = FileColumns.MEDIA_TYPE_VIDEO;
+ }
+ row[PhotoSetLoader.INDEX_MEDIA_TYPE] = mappedMediaType;
+ row[PhotoSetLoader.INDEX_SUPPORTED_OPERATIONS] =
+ item.getSupportedOperations();
+ cursor.addRow(row);
+ mediaItems.append(index, item);
+ }
+ });
+ synchronized (mMediaSet) {
+ mMediaItems = mediaItems;
+ }
+ return cursor;
+ }
+
+ @Override
+ public Drawable drawableForItem(Cursor item, Drawable recycle) {
+ BitmapJobDrawable drawable = null;
+ if (recycle == null || !(recycle instanceof BitmapJobDrawable)) {
+ drawable = new BitmapJobDrawable();
+ } else {
+ drawable = (BitmapJobDrawable) recycle;
+ }
+ int index = item.getInt(PhotoSetLoader.INDEX_ID);
+ drawable.setMediaItem(mMediaItems.get(index));
+ return drawable;
+ }
+
+ public static int getThumbnailSize() {
+ return MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL);
+ }
+
+ @Override
+ public Uri uriForItem(Cursor item) {
+ int index = item.getInt(PhotoSetLoader.INDEX_ID);
+ MediaItem mi = mMediaItems.get(index);
+ return mi == null ? null : mi.getContentUri();
+ }
+
+ @Override
+ public ArrayList<Uri> urisForSubItems(Cursor item) {
+ return null;
+ }
+
+ @Override
+ public void deleteItemWithPath(Object path) {
+ MediaObject o = mDataManager.getMediaObject((Path) path);
+ if (o != null) {
+ o.delete();
+ }
+ }
+
+ @Override
+ public Object getPathForItem(Cursor item) {
+ int index = item.getInt(PhotoSetLoader.INDEX_ID);
+ MediaItem mi = mMediaItems.get(index);
+ if (mi != null) {
+ return mi.getPath();
+ }
+ return null;
+ }
+
+}
diff --git a/src/com/android/photos/shims/MediaSetLoader.java b/src/com/android/photos/shims/MediaSetLoader.java
new file mode 100644
index 000000000..9093bc139
--- /dev/null
+++ b/src/com/android/photos/shims/MediaSetLoader.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2013 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.photos.shims;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+
+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.data.MediaSet.SyncListener;
+import com.android.gallery3d.util.Future;
+import com.android.photos.data.AlbumSetLoader;
+
+import java.util.ArrayList;
+
+/**
+ * Returns all MediaSets in a MediaSet, wrapping them in a cursor to appear
+ * like a AlbumSetLoader.
+ */
+public class MediaSetLoader extends AsyncTaskLoader<Cursor> implements LoaderCompatShim<Cursor>{
+
+ private static final SyncListener sNullListener = new SyncListener() {
+ @Override
+ public void onSyncDone(MediaSet mediaSet, int resultCode) {
+ }
+ };
+
+ private final MediaSet mMediaSet;
+ private final DataManager mDataManager;
+ private Future<Integer> mSyncTask = null;
+ private ContentListener mObserver = new ContentListener() {
+ @Override
+ public void onContentDirty() {
+ onContentChanged();
+ }
+ };
+
+ private ArrayList<MediaItem> mCoverItems;
+
+ public MediaSetLoader(Context context) {
+ super(context);
+ mDataManager = DataManager.from(context);
+ String path = mDataManager.getTopSetPath(DataManager.INCLUDE_ALL);
+ mMediaSet = mDataManager.getMediaSet(path);
+ }
+
+ public MediaSetLoader(Context context, String path) {
+ super(context);
+ mDataManager = DataManager.from(getContext());
+ mMediaSet = mDataManager.getMediaSet(path);
+ }
+
+ @Override
+ protected void onStartLoading() {
+ super.onStartLoading();
+ mMediaSet.addContentListener(mObserver);
+ mSyncTask = mMediaSet.requestSync(sNullListener);
+ forceLoad();
+ }
+
+ @Override
+ protected boolean onCancelLoad() {
+ if (mSyncTask != null) {
+ mSyncTask.cancel();
+ mSyncTask = null;
+ }
+ return super.onCancelLoad();
+ }
+
+ @Override
+ protected void onStopLoading() {
+ super.onStopLoading();
+ cancelLoad();
+ mMediaSet.removeContentListener(mObserver);
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+ onStopLoading();
+ }
+
+ @Override
+ public Cursor loadInBackground() {
+ // TODO: This probably doesn't work
+ mMediaSet.reload();
+ final MatrixCursor cursor = new MatrixCursor(AlbumSetLoader.PROJECTION);
+ final Object[] row = new Object[AlbumSetLoader.PROJECTION.length];
+ int count = mMediaSet.getSubMediaSetCount();
+ ArrayList<MediaItem> coverItems = new ArrayList<MediaItem>(count);
+ for (int i = 0; i < count; i++) {
+ MediaSet m = mMediaSet.getSubMediaSet(i);
+ m.reload();
+ row[AlbumSetLoader.INDEX_ID] = i;
+ row[AlbumSetLoader.INDEX_TITLE] = m.getName();
+ row[AlbumSetLoader.INDEX_COUNT] = m.getMediaItemCount();
+ row[AlbumSetLoader.INDEX_SUPPORTED_OPERATIONS] = m.getSupportedOperations();
+ MediaItem coverItem = m.getCoverMediaItem();
+ if (coverItem != null) {
+ row[AlbumSetLoader.INDEX_TIMESTAMP] = coverItem.getDateInMs();
+ }
+ coverItems.add(coverItem);
+ cursor.addRow(row);
+ }
+ synchronized (mMediaSet) {
+ mCoverItems = coverItems;
+ }
+ return cursor;
+ }
+
+ @Override
+ public Drawable drawableForItem(Cursor item, Drawable recycle) {
+ BitmapJobDrawable drawable = null;
+ if (recycle == null || !(recycle instanceof BitmapJobDrawable)) {
+ drawable = new BitmapJobDrawable();
+ } else {
+ drawable = (BitmapJobDrawable) recycle;
+ }
+ int index = item.getInt(AlbumSetLoader.INDEX_ID);
+ drawable.setMediaItem(mCoverItems.get(index));
+ return drawable;
+ }
+
+ public static int getThumbnailSize() {
+ return MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL);
+ }
+
+ @Override
+ public Uri uriForItem(Cursor item) {
+ int index = item.getInt(AlbumSetLoader.INDEX_ID);
+ MediaSet ms = mMediaSet.getSubMediaSet(index);
+ return ms == null ? null : ms.getContentUri();
+ }
+
+ @Override
+ public ArrayList<Uri> urisForSubItems(Cursor item) {
+ int index = item.getInt(AlbumSetLoader.INDEX_ID);
+ MediaSet ms = mMediaSet.getSubMediaSet(index);
+ if (ms == null) return null;
+ final ArrayList<Uri> result = new ArrayList<Uri>();
+ ms.enumerateMediaItems(new MediaSet.ItemConsumer() {
+ @Override
+ public void consume(int index, MediaItem item) {
+ if (item != null) {
+ result.add(item.getContentUri());
+ }
+ }
+ });
+ return result;
+ }
+
+ @Override
+ public void deleteItemWithPath(Object path) {
+ MediaObject o = mDataManager.getMediaObject((Path) path);
+ if (o != null) {
+ o.delete();
+ }
+ }
+
+ @Override
+ public Object getPathForItem(Cursor item) {
+ int index = item.getInt(AlbumSetLoader.INDEX_ID);
+ MediaSet ms = mMediaSet.getSubMediaSet(index);
+ if (ms != null) {
+ return ms.getPath();
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/photos/views/BlockingGLTextureView.java b/src/com/android/photos/views/BlockingGLTextureView.java
new file mode 100644
index 000000000..8a0505185
--- /dev/null
+++ b/src/com/android/photos/views/BlockingGLTextureView.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright (C) 2013 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.photos.views;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.opengl.GLSurfaceView.Renderer;
+import android.opengl.GLUtils;
+import android.util.Log;
+import android.view.TextureView;
+import android.view.TextureView.SurfaceTextureListener;
+
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+import javax.microedition.khronos.egl.EGLSurface;
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * A TextureView that supports blocking rendering for synchronous drawing
+ */
+public class BlockingGLTextureView extends TextureView
+ implements SurfaceTextureListener {
+
+ private RenderThread mRenderThread;
+
+ public BlockingGLTextureView(Context context) {
+ super(context);
+ setSurfaceTextureListener(this);
+ }
+
+ public void setRenderer(Renderer renderer) {
+ if (mRenderThread != null) {
+ throw new IllegalArgumentException("Renderer already set");
+ }
+ mRenderThread = new RenderThread(renderer);
+ }
+
+ public void render() {
+ mRenderThread.render();
+ }
+
+ public void destroy() {
+ if (mRenderThread != null) {
+ mRenderThread.finish();
+ mRenderThread = null;
+ }
+ }
+
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width,
+ int height) {
+ mRenderThread.setSurface(surface);
+ mRenderThread.setSize(width, height);
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width,
+ int height) {
+ mRenderThread.setSize(width, height);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ if (mRenderThread != null) {
+ mRenderThread.setSurface(null);
+ }
+ return false;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ destroy();
+ } catch (Throwable t) {
+ // Ignore
+ }
+ super.finalize();
+ }
+
+ /**
+ * An EGL helper class.
+ */
+
+ private static class EglHelper {
+ private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
+ private static final int EGL_OPENGL_ES2_BIT = 4;
+
+ EGL10 mEgl;
+ EGLDisplay mEglDisplay;
+ EGLSurface mEglSurface;
+ EGLConfig mEglConfig;
+ EGLContext mEglContext;
+
+ private EGLConfig chooseEglConfig() {
+ int[] configsCount = new int[1];
+ EGLConfig[] configs = new EGLConfig[1];
+ int[] configSpec = getConfig();
+ if (!mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount)) {
+ throw new IllegalArgumentException("eglChooseConfig failed " +
+ GLUtils.getEGLErrorString(mEgl.eglGetError()));
+ } else if (configsCount[0] > 0) {
+ return configs[0];
+ }
+ return null;
+ }
+
+ private static int[] getConfig() {
+ return new int[] {
+ EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
+ EGL10.EGL_RED_SIZE, 8,
+ EGL10.EGL_GREEN_SIZE, 8,
+ EGL10.EGL_BLUE_SIZE, 8,
+ EGL10.EGL_ALPHA_SIZE, 8,
+ EGL10.EGL_DEPTH_SIZE, 0,
+ EGL10.EGL_STENCIL_SIZE, 0,
+ EGL10.EGL_NONE
+ };
+ }
+
+ EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) {
+ int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
+ return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attribList);
+ }
+
+ /**
+ * Initialize EGL for a given configuration spec.
+ */
+ public void start() {
+ /*
+ * Get an EGL instance
+ */
+ mEgl = (EGL10) EGLContext.getEGL();
+
+ /*
+ * Get to the default display.
+ */
+ mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+
+ if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
+ throw new RuntimeException("eglGetDisplay failed");
+ }
+
+ /*
+ * We can now initialize EGL for that display
+ */
+ int[] version = new int[2];
+ if (!mEgl.eglInitialize(mEglDisplay, version)) {
+ throw new RuntimeException("eglInitialize failed");
+ }
+ mEglConfig = chooseEglConfig();
+
+ /*
+ * Create an EGL context. We want to do this as rarely as we can, because an
+ * EGL context is a somewhat heavy object.
+ */
+ mEglContext = createContext(mEgl, mEglDisplay, mEglConfig);
+
+ if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
+ mEglContext = null;
+ throwEglException("createContext");
+ }
+
+ mEglSurface = null;
+ }
+
+ /**
+ * Create an egl surface for the current SurfaceTexture surface. If a surface
+ * already exists, destroy it before creating the new surface.
+ *
+ * @return true if the surface was created successfully.
+ */
+ public boolean createSurface(SurfaceTexture surface) {
+ /*
+ * Check preconditions.
+ */
+ if (mEgl == null) {
+ throw new RuntimeException("egl not initialized");
+ }
+ if (mEglDisplay == null) {
+ throw new RuntimeException("eglDisplay not initialized");
+ }
+ if (mEglConfig == null) {
+ throw new RuntimeException("mEglConfig not initialized");
+ }
+
+ /*
+ * The window size has changed, so we need to create a new
+ * surface.
+ */
+ destroySurfaceImp();
+
+ /*
+ * Create an EGL surface we can render into.
+ */
+ if (surface != null) {
+ mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, null);
+ } else {
+ mEglSurface = null;
+ }
+
+ if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
+ int error = mEgl.eglGetError();
+ if (error == EGL10.EGL_BAD_NATIVE_WINDOW) {
+ Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
+ }
+ return false;
+ }
+
+ /*
+ * Before we can issue GL commands, we need to make sure
+ * the context is current and bound to a surface.
+ */
+ if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
+ /*
+ * Could not make the context current, probably because the underlying
+ * SurfaceView surface has been destroyed.
+ */
+ logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Create a GL object for the current EGL context.
+ */
+ public GL10 createGL() {
+ return (GL10) mEglContext.getGL();
+ }
+
+ /**
+ * Display the current render surface.
+ * @return the EGL error code from eglSwapBuffers.
+ */
+ public int swap() {
+ if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
+ return mEgl.eglGetError();
+ }
+ return EGL10.EGL_SUCCESS;
+ }
+
+ public void destroySurface() {
+ destroySurfaceImp();
+ }
+
+ private void destroySurfaceImp() {
+ if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
+ mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
+ EGL10.EGL_NO_SURFACE,
+ EGL10.EGL_NO_CONTEXT);
+ mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
+ mEglSurface = null;
+ }
+ }
+
+ public void finish() {
+ if (mEglContext != null) {
+ mEgl.eglDestroyContext(mEglDisplay, mEglContext);
+ mEglContext = null;
+ }
+ if (mEglDisplay != null) {
+ mEgl.eglTerminate(mEglDisplay);
+ mEglDisplay = null;
+ }
+ }
+
+ private void throwEglException(String function) {
+ throwEglException(function, mEgl.eglGetError());
+ }
+
+ public static void throwEglException(String function, int error) {
+ String message = formatEglError(function, error);
+ throw new RuntimeException(message);
+ }
+
+ public static void logEglErrorAsWarning(String tag, String function, int error) {
+ Log.w(tag, formatEglError(function, error));
+ }
+
+ public static String formatEglError(String function, int error) {
+ return function + " failed: " + error;
+ }
+
+ }
+
+ private static class RenderThread extends Thread {
+ private static final int INVALID = -1;
+ private static final int RENDER = 1;
+ private static final int CHANGE_SURFACE = 2;
+ private static final int RESIZE_SURFACE = 3;
+ private static final int FINISH = 4;
+
+ private EglHelper mEglHelper = new EglHelper();
+
+ private Object mLock = new Object();
+ private int mExecMsgId = INVALID;
+ private SurfaceTexture mSurface;
+ private Renderer mRenderer;
+ private int mWidth, mHeight;
+
+ private boolean mFinished = false;
+ private GL10 mGL;
+
+ public RenderThread(Renderer renderer) {
+ super("RenderThread");
+ mRenderer = renderer;
+ start();
+ }
+
+ private void checkRenderer() {
+ if (mRenderer == null) {
+ throw new IllegalArgumentException("Renderer is null!");
+ }
+ }
+
+ private void checkSurface() {
+ if (mSurface == null) {
+ throw new IllegalArgumentException("surface is null!");
+ }
+ }
+
+ public void setSurface(SurfaceTexture surface) {
+ // If the surface is null we're being torn down, don't need a
+ // renderer then
+ if (surface != null) {
+ checkRenderer();
+ }
+ mSurface = surface;
+ exec(CHANGE_SURFACE);
+ }
+
+ public void setSize(int width, int height) {
+ checkRenderer();
+ checkSurface();
+ mWidth = width;
+ mHeight = height;
+ exec(RESIZE_SURFACE);
+ }
+
+ public void render() {
+ checkRenderer();
+ if (mSurface != null) {
+ exec(RENDER);
+ mSurface.updateTexImage();
+ }
+ }
+
+ public void finish() {
+ mSurface = null;
+ exec(FINISH);
+ try {
+ join();
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+
+ private void exec(int msgid) {
+ synchronized (mLock) {
+ if (mExecMsgId != INVALID) {
+ throw new IllegalArgumentException(
+ "Message already set - multithreaded access?");
+ }
+ mExecMsgId = msgid;
+ mLock.notify();
+ try {
+ mLock.wait();
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ }
+
+ private void handleMessageLocked(int what) {
+ switch (what) {
+ case CHANGE_SURFACE:
+ if (mEglHelper.createSurface(mSurface)) {
+ mGL = mEglHelper.createGL();
+ mRenderer.onSurfaceCreated(mGL, mEglHelper.mEglConfig);
+ }
+ break;
+ case RESIZE_SURFACE:
+ mRenderer.onSurfaceChanged(mGL, mWidth, mHeight);
+ break;
+ case RENDER:
+ mRenderer.onDrawFrame(mGL);
+ mEglHelper.swap();
+ break;
+ case FINISH:
+ mEglHelper.destroySurface();
+ mEglHelper.finish();
+ mFinished = true;
+ break;
+ }
+ }
+
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ mEglHelper.start();
+ while (!mFinished) {
+ while (mExecMsgId == INVALID) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ handleMessageLocked(mExecMsgId);
+ mExecMsgId = INVALID;
+ mLock.notify();
+ }
+ mExecMsgId = FINISH;
+ }
+ }
+ }
+}
diff --git a/src/com/android/photos/views/GalleryThumbnailView.java b/src/com/android/photos/views/GalleryThumbnailView.java
new file mode 100644
index 000000000..e5dd6f2ff
--- /dev/null
+++ b/src/com/android/photos/views/GalleryThumbnailView.java
@@ -0,0 +1,883 @@
+/*
+ * Copyright (C) 2013 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.photos.views;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.VelocityTrackerCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.EdgeEffectCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.ListAdapter;
+import android.widget.OverScroller;
+
+import java.util.ArrayList;
+
+public class GalleryThumbnailView extends ViewGroup {
+
+ public interface GalleryThumbnailAdapter extends ListAdapter {
+ /**
+ * @param position Position to get the intrinsic aspect ratio for
+ * @return width / height
+ */
+ float getIntrinsicAspectRatio(int position);
+ }
+
+ private static final String TAG = "GalleryThumbnailView";
+ private static final float ASPECT_RATIO = (float) Math.sqrt(1.5f);
+ private static final int LAND_UNITS = 2;
+ private static final int PORT_UNITS = 3;
+
+ private GalleryThumbnailAdapter mAdapter;
+
+ private final RecycleBin mRecycler = new RecycleBin();
+
+ private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver();
+
+ private boolean mDataChanged;
+ private int mOldItemCount;
+ private int mItemCount;
+ private boolean mHasStableIds;
+
+ private int mFirstPosition;
+
+ private boolean mPopulating;
+ private boolean mInLayout;
+
+ private int mTouchSlop;
+ private int mMaximumVelocity;
+ private int mFlingVelocity;
+ private float mLastTouchX;
+ private float mTouchRemainderX;
+ private int mActivePointerId;
+
+ private static final int TOUCH_MODE_IDLE = 0;
+ private static final int TOUCH_MODE_DRAGGING = 1;
+ private static final int TOUCH_MODE_FLINGING = 2;
+
+ private int mTouchMode;
+ private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+ private final OverScroller mScroller;
+
+ private final EdgeEffectCompat mLeftEdge;
+ private final EdgeEffectCompat mRightEdge;
+
+ private int mLargeColumnWidth;
+ private int mSmallColumnWidth;
+ private int mLargeColumnUnitCount = 8;
+ private int mSmallColumnUnitCount = 10;
+
+ public GalleryThumbnailView(Context context) {
+ this(context, null);
+ }
+
+ public GalleryThumbnailView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public GalleryThumbnailView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final ViewConfiguration vc = ViewConfiguration.get(context);
+ mTouchSlop = vc.getScaledTouchSlop();
+ mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
+ mFlingVelocity = vc.getScaledMinimumFlingVelocity();
+ mScroller = new OverScroller(context);
+
+ mLeftEdge = new EdgeEffectCompat(context);
+ mRightEdge = new EdgeEffectCompat(context);
+ setWillNotDraw(false);
+ setClipToPadding(false);
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mPopulating) {
+ super.requestLayout();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthMode != MeasureSpec.EXACTLY) {
+ Log.e(TAG, "onMeasure: must have an exact width or match_parent! " +
+ "Using fallback spec of EXACTLY " + widthSize);
+ }
+ if (heightMode != MeasureSpec.EXACTLY) {
+ Log.e(TAG, "onMeasure: must have an exact height or match_parent! " +
+ "Using fallback spec of EXACTLY " + heightSize);
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+
+ float portSpaces = mLargeColumnUnitCount / PORT_UNITS;
+ float height = getMeasuredHeight() / portSpaces;
+ mLargeColumnWidth = (int) (height / ASPECT_RATIO);
+ portSpaces++;
+ height = getMeasuredHeight() / portSpaces;
+ mSmallColumnWidth = (int) (height / ASPECT_RATIO);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ mInLayout = true;
+ populate();
+ mInLayout = false;
+
+ final int width = r - l;
+ final int height = b - t;
+ mLeftEdge.setSize(width, height);
+ mRightEdge.setSize(width, height);
+ }
+
+ private void populate() {
+ if (getWidth() == 0 || getHeight() == 0) {
+ return;
+ }
+
+ // TODO: Handle size changing
+// final int colCount = mColCount;
+// if (mItemTops == null || mItemTops.length != colCount) {
+// mItemTops = new int[colCount];
+// mItemBottoms = new int[colCount];
+// final int top = getPaddingTop();
+// final int offset = top + Math.min(mRestoreOffset, 0);
+// Arrays.fill(mItemTops, offset);
+// Arrays.fill(mItemBottoms, offset);
+// mLayoutRecords.clear();
+// if (mInLayout) {
+// removeAllViewsInLayout();
+// } else {
+// removeAllViews();
+// }
+// mRestoreOffset = 0;
+// }
+
+ mPopulating = true;
+ layoutChildren(mDataChanged);
+ fillRight(mFirstPosition + getChildCount(), 0);
+ fillLeft(mFirstPosition - 1, 0);
+ mPopulating = false;
+ mDataChanged = false;
+ }
+
+ final void layoutChildren(boolean queryAdapter) {
+// TODO
+// final int childCount = getChildCount();
+// for (int i = 0; i < childCount; i++) {
+// View child = getChildAt(i);
+//
+// if (child.isLayoutRequested()) {
+// final int widthSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY);
+// final int heightSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), MeasureSpec.EXACTLY);
+// child.measure(widthSpec, heightSpec);
+// child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
+// }
+//
+// int childTop = mItemBottoms[col] > Integer.MIN_VALUE ?
+// mItemBottoms[col] + mItemMargin : child.getTop();
+// if (span > 1) {
+// int lowest = childTop;
+// for (int j = col + 1; j < col + span; j++) {
+// final int bottom = mItemBottoms[j] + mItemMargin;
+// if (bottom > lowest) {
+// lowest = bottom;
+// }
+// }
+// childTop = lowest;
+// }
+// final int childHeight = child.getMeasuredHeight();
+// final int childBottom = childTop + childHeight;
+// final int childLeft = paddingLeft + col * (colWidth + itemMargin);
+// final int childRight = childLeft + child.getMeasuredWidth();
+// child.layout(childLeft, childTop, childRight, childBottom);
+// }
+ }
+
+ /**
+ * Obtain the view and add it to our list of children. The view can be made
+ * fresh, converted from an unused view, or used as is if it was in the
+ * recycle bin.
+ *
+ * @param startPosition Logical position in the list to start from
+ * @param x Left or right edge of the view to add
+ * @param forward If true, align left edge to x and increase position.
+ * If false, align right edge to x and decrease position.
+ * @return Number of views added
+ */
+ private int makeAndAddColumn(int startPosition, int x, boolean forward) {
+ int columnWidth = mLargeColumnWidth;
+ int addViews = 0;
+ for (int remaining = mLargeColumnUnitCount, i = 0;
+ remaining > 0 && startPosition + i >= 0 && startPosition + i < mItemCount;
+ i += forward ? 1 : -1, addViews++) {
+ if (mAdapter.getIntrinsicAspectRatio(startPosition + i) >= 1f) {
+ // landscape
+ remaining -= LAND_UNITS;
+ } else {
+ // portrait
+ remaining -= PORT_UNITS;
+ if (remaining < 0) {
+ remaining += (mSmallColumnUnitCount - mLargeColumnUnitCount);
+ columnWidth = mSmallColumnWidth;
+ }
+ }
+ }
+ int nextTop = 0;
+ for (int i = 0; i < addViews; i++) {
+ int position = startPosition + (forward ? i : -i);
+ View child = obtainView(position, null);
+ if (child.getParent() != this) {
+ if (mInLayout) {
+ addViewInLayout(child, forward ? -1 : 0, child.getLayoutParams());
+ } else {
+ addView(child, forward ? -1 : 0);
+ }
+ }
+ int heightSize = (int) (.5f + (mAdapter.getIntrinsicAspectRatio(position) >= 1f
+ ? columnWidth / ASPECT_RATIO
+ : columnWidth * ASPECT_RATIO));
+ int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
+ int widthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY);
+ child.measure(widthSpec, heightSpec);
+ int childLeft = forward ? x : x - columnWidth;
+ child.layout(childLeft, nextTop, childLeft + columnWidth, nextTop + heightSize);
+ nextTop += heightSize;
+ }
+ return addViews;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ mVelocityTracker.addMovement(ev);
+ final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mVelocityTracker.clear();
+ mScroller.abortAnimation();
+ mLastTouchX = ev.getX();
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+ mTouchRemainderX = 0;
+ if (mTouchMode == TOUCH_MODE_FLINGING) {
+ // Catch!
+ mTouchMode = TOUCH_MODE_DRAGGING;
+ return true;
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+ if (index < 0) {
+ Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " +
+ mActivePointerId + " - did StaggeredGridView receive an inconsistent " +
+ "event stream?");
+ return false;
+ }
+ final float x = MotionEventCompat.getX(ev, index);
+ final float dx = x - mLastTouchX + mTouchRemainderX;
+ final int deltaY = (int) dx;
+ mTouchRemainderX = dx - deltaY;
+
+ if (Math.abs(dx) > mTouchSlop) {
+ mTouchMode = TOUCH_MODE_DRAGGING;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ mVelocityTracker.addMovement(ev);
+ final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mVelocityTracker.clear();
+ mScroller.abortAnimation();
+ mLastTouchX = ev.getX();
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+ mTouchRemainderX = 0;
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+ if (index < 0) {
+ Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " +
+ mActivePointerId + " - did StaggeredGridView receive an inconsistent " +
+ "event stream?");
+ return false;
+ }
+ final float x = MotionEventCompat.getX(ev, index);
+ final float dx = x - mLastTouchX + mTouchRemainderX;
+ final int deltaX = (int) dx;
+ mTouchRemainderX = dx - deltaX;
+
+ if (Math.abs(dx) > mTouchSlop) {
+ mTouchMode = TOUCH_MODE_DRAGGING;
+ }
+
+ if (mTouchMode == TOUCH_MODE_DRAGGING) {
+ mLastTouchX = x;
+
+ if (!trackMotionScroll(deltaX, true)) {
+ // Break fling velocity if we impacted an edge.
+ mVelocityTracker.clear();
+ }
+ }
+ } break;
+
+ case MotionEvent.ACTION_CANCEL:
+ mTouchMode = TOUCH_MODE_IDLE;
+ break;
+
+ case MotionEvent.ACTION_UP: {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ final float velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker,
+ mActivePointerId);
+ if (Math.abs(velocity) > mFlingVelocity) { // TODO
+ mTouchMode = TOUCH_MODE_FLINGING;
+ mScroller.fling(0, 0, (int) velocity, 0,
+ Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
+ mLastTouchX = 0;
+ ViewCompat.postInvalidateOnAnimation(this);
+ } else {
+ mTouchMode = TOUCH_MODE_IDLE;
+ }
+
+ } break;
+ }
+ return true;
+ }
+
+ /**
+ *
+ * @param deltaX Pixels that content should move by
+ * @return true if the movement completed, false if it was stopped prematurely.
+ */
+ private boolean trackMotionScroll(int deltaX, boolean allowOverScroll) {
+ final boolean contentFits = contentFits();
+ final int allowOverhang = Math.abs(deltaX);
+
+ final int overScrolledBy;
+ final int movedBy;
+ if (!contentFits) {
+ final int overhang;
+ final boolean up;
+ mPopulating = true;
+ if (deltaX > 0) {
+ overhang = fillLeft(mFirstPosition - 1, allowOverhang);
+ up = true;
+ } else {
+ overhang = fillRight(mFirstPosition + getChildCount(), allowOverhang);
+ up = false;
+ }
+ movedBy = Math.min(overhang, allowOverhang);
+ offsetChildren(up ? movedBy : -movedBy);
+ recycleOffscreenViews();
+ mPopulating = false;
+ overScrolledBy = allowOverhang - overhang;
+ } else {
+ overScrolledBy = allowOverhang;
+ movedBy = 0;
+ }
+
+ if (allowOverScroll) {
+ final int overScrollMode = ViewCompat.getOverScrollMode(this);
+
+ if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+ (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) {
+
+ if (overScrolledBy > 0) {
+ EdgeEffectCompat edge = deltaX > 0 ? mLeftEdge : mRightEdge;
+ edge.onPull((float) Math.abs(deltaX) / getWidth());
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+ }
+
+ return deltaX == 0 || movedBy != 0;
+ }
+
+ /**
+ * Important: this method will leave offscreen views attached if they
+ * are required to maintain the invariant that child view with index i
+ * is always the view corresponding to position mFirstPosition + i.
+ */
+ private void recycleOffscreenViews() {
+ final int height = getHeight();
+ final int clearAbove = 0;
+ final int clearBelow = height;
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+ if (child.getTop() <= clearBelow) {
+ // There may be other offscreen views, but we need to maintain
+ // the invariant documented above.
+ break;
+ }
+
+ if (mInLayout) {
+ removeViewsInLayout(i, 1);
+ } else {
+ removeViewAt(i);
+ }
+
+ mRecycler.addScrap(child);
+ }
+
+ while (getChildCount() > 0) {
+ final View child = getChildAt(0);
+ if (child.getBottom() >= clearAbove) {
+ // There may be other offscreen views, but we need to maintain
+ // the invariant documented above.
+ break;
+ }
+
+ if (mInLayout) {
+ removeViewsInLayout(0, 1);
+ } else {
+ removeViewAt(0);
+ }
+
+ mRecycler.addScrap(child);
+ mFirstPosition++;
+ }
+ }
+
+ final void offsetChildren(int offset) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ child.layout(child.getLeft() + offset, child.getTop(),
+ child.getRight() + offset, child.getBottom());
+ }
+ }
+
+ private boolean contentFits() {
+ final int childCount = getChildCount();
+ if (childCount == 0) return true;
+ if (childCount != mItemCount) return false;
+
+ return getChildAt(0).getLeft() >= getPaddingLeft() &&
+ getChildAt(childCount - 1).getRight() <= getWidth() - getPaddingRight();
+ }
+
+ private void recycleAllViews() {
+ for (int i = 0; i < getChildCount(); i++) {
+ mRecycler.addScrap(getChildAt(i));
+ }
+
+ if (mInLayout) {
+ removeAllViewsInLayout();
+ } else {
+ removeAllViews();
+ }
+ }
+
+ private int fillRight(int pos, int overhang) {
+ int end = (getRight() - getLeft()) + overhang;
+
+ int nextLeft = getChildCount() == 0 ? 0 : getChildAt(getChildCount() - 1).getRight();
+ while (nextLeft < end && pos < mItemCount) {
+ pos += makeAndAddColumn(pos, nextLeft, true);
+ nextLeft = getChildAt(getChildCount() - 1).getRight();
+ }
+ final int gridRight = getWidth() - getPaddingRight();
+ return getChildAt(getChildCount() - 1).getRight() - gridRight;
+ }
+
+ private int fillLeft(int pos, int overhang) {
+ int end = getPaddingLeft() - overhang;
+
+ int nextRight = getChildAt(0).getLeft();
+ while (nextRight > end && pos >= 0) {
+ pos -= makeAndAddColumn(pos, nextRight, false);
+ nextRight = getChildAt(0).getLeft();
+ }
+
+ mFirstPosition = pos + 1;
+ return getPaddingLeft() - getChildAt(0).getLeft();
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScroller.computeScrollOffset()) {
+ final int x = mScroller.getCurrX();
+ final int dx = (int) (x - mLastTouchX);
+ mLastTouchX = x;
+ final boolean stopped = !trackMotionScroll(dx, false);
+
+ if (!stopped && !mScroller.isFinished()) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ } else {
+ if (stopped) {
+ final int overScrollMode = ViewCompat.getOverScrollMode(this);
+ if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) {
+ final EdgeEffectCompat edge;
+ if (dx > 0) {
+ edge = mLeftEdge;
+ } else {
+ edge = mRightEdge;
+ }
+ edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity()));
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ mScroller.abortAnimation();
+ }
+ mTouchMode = TOUCH_MODE_IDLE;
+ }
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (!mLeftEdge.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ canvas.rotate(270);
+ canvas.translate(-height + getPaddingTop(), 0);
+ mLeftEdge.setSize(height, getWidth());
+ if (mLeftEdge.draw(canvas)) {
+ postInvalidateOnAnimation();
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ if (!mRightEdge.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ canvas.rotate(90);
+ canvas.translate(-getPaddingTop(), width);
+ mRightEdge.setSize(height, width);
+ if (mRightEdge.draw(canvas)) {
+ postInvalidateOnAnimation();
+ }
+ canvas.restoreToCount(restoreCount);
+ }
+ }
+
+ /**
+ * Obtain a populated view from the adapter. If optScrap is non-null and is not
+ * reused it will be placed in the recycle bin.
+ *
+ * @param position position to get view for
+ * @param optScrap Optional scrap view; will be reused if possible
+ * @return A new view, a recycled view from mRecycler, or optScrap
+ */
+ private final View obtainView(int position, View optScrap) {
+ View view = mRecycler.getTransientStateView(position);
+ if (view != null) {
+ return view;
+ }
+
+ // Reuse optScrap if it's of the right type (and not null)
+ final int optType = optScrap != null ?
+ ((LayoutParams) optScrap.getLayoutParams()).viewType : -1;
+ final int positionViewType = mAdapter.getItemViewType(position);
+ final View scrap = optType == positionViewType ?
+ optScrap : mRecycler.getScrapView(positionViewType);
+
+ view = mAdapter.getView(position, scrap, this);
+
+ if (view != scrap && scrap != null) {
+ // The adapter didn't use it; put it back.
+ mRecycler.addScrap(scrap);
+ }
+
+ ViewGroup.LayoutParams lp = view.getLayoutParams();
+
+ if (view.getParent() != this) {
+ if (lp == null) {
+ lp = generateDefaultLayoutParams();
+ } else if (!checkLayoutParams(lp)) {
+ lp = generateLayoutParams(lp);
+ }
+ view.setLayoutParams(lp);
+ }
+
+ final LayoutParams sglp = (LayoutParams) lp;
+ sglp.position = position;
+ sglp.viewType = positionViewType;
+
+ return view;
+ }
+
+ public GalleryThumbnailAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ public void setAdapter(GalleryThumbnailAdapter adapter) {
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mObserver);
+ }
+ // TODO: If the new adapter says that there are stable IDs, remove certain layout records
+ // and onscreen views if they have changed instead of removing all of the state here.
+ clearAllState();
+ mAdapter = adapter;
+ mDataChanged = true;
+ mOldItemCount = mItemCount = adapter != null ? adapter.getCount() : 0;
+ if (adapter != null) {
+ adapter.registerDataSetObserver(mObserver);
+ mRecycler.setViewTypeCount(adapter.getViewTypeCount());
+ mHasStableIds = adapter.hasStableIds();
+ } else {
+ mHasStableIds = false;
+ }
+ populate();
+ }
+
+ /**
+ * Clear all state because the grid will be used for a completely different set of data.
+ */
+ private void clearAllState() {
+ // Clear all layout records and views
+ removeAllViews();
+
+ // Reset to the top of the grid
+ mFirstPosition = 0;
+
+ // Clear recycler because there could be different view types now
+ mRecycler.clear();
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ return new LayoutParams(lp);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
+ return lp instanceof LayoutParams;
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ private static final int[] LAYOUT_ATTRS = new int[] {
+ android.R.attr.layout_span
+ };
+
+ private static final int SPAN_INDEX = 0;
+
+ /**
+ * The number of columns this item should span
+ */
+ public int span = 1;
+
+ /**
+ * Item position this view represents
+ */
+ int position;
+
+ /**
+ * Type of this view as reported by the adapter
+ */
+ int viewType;
+
+ /**
+ * The column this view is occupying
+ */
+ int column;
+
+ /**
+ * The stable ID of the item this view displays
+ */
+ long id = -1;
+
+ public LayoutParams(int height) {
+ super(MATCH_PARENT, height);
+
+ if (this.height == MATCH_PARENT) {
+ Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " +
+ "impossible! Falling back to WRAP_CONTENT");
+ this.height = WRAP_CONTENT;
+ }
+ }
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ if (this.width != MATCH_PARENT) {
+ Log.w(TAG, "Inflation setting LayoutParams width to " + this.width +
+ " - must be MATCH_PARENT");
+ this.width = MATCH_PARENT;
+ }
+ if (this.height == MATCH_PARENT) {
+ Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " +
+ "impossible! Falling back to WRAP_CONTENT");
+ this.height = WRAP_CONTENT;
+ }
+
+ TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
+ span = a.getInteger(SPAN_INDEX, 1);
+ a.recycle();
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams other) {
+ super(other);
+
+ if (this.width != MATCH_PARENT) {
+ Log.w(TAG, "Constructing LayoutParams with width " + this.width +
+ " - must be MATCH_PARENT");
+ this.width = MATCH_PARENT;
+ }
+ if (this.height == MATCH_PARENT) {
+ Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " +
+ "impossible! Falling back to WRAP_CONTENT");
+ this.height = WRAP_CONTENT;
+ }
+ }
+ }
+
+ private class RecycleBin {
+ private ArrayList<View>[] mScrapViews;
+ private int mViewTypeCount;
+ private int mMaxScrap;
+
+ private SparseArray<View> mTransientStateViews;
+
+ public void setViewTypeCount(int viewTypeCount) {
+ if (viewTypeCount < 1) {
+ throw new IllegalArgumentException("Must have at least one view type (" +
+ viewTypeCount + " types reported)");
+ }
+ if (viewTypeCount == mViewTypeCount) {
+ return;
+ }
+
+ ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
+ for (int i = 0; i < viewTypeCount; i++) {
+ scrapViews[i] = new ArrayList<View>();
+ }
+ mViewTypeCount = viewTypeCount;
+ mScrapViews = scrapViews;
+ }
+
+ public void clear() {
+ final int typeCount = mViewTypeCount;
+ for (int i = 0; i < typeCount; i++) {
+ mScrapViews[i].clear();
+ }
+ if (mTransientStateViews != null) {
+ mTransientStateViews.clear();
+ }
+ }
+
+ public void clearTransientViews() {
+ if (mTransientStateViews != null) {
+ mTransientStateViews.clear();
+ }
+ }
+
+ public void addScrap(View v) {
+ final LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ if (ViewCompat.hasTransientState(v)) {
+ if (mTransientStateViews == null) {
+ mTransientStateViews = new SparseArray<View>();
+ }
+ mTransientStateViews.put(lp.position, v);
+ return;
+ }
+
+ final int childCount = getChildCount();
+ if (childCount > mMaxScrap) {
+ mMaxScrap = childCount;
+ }
+
+ ArrayList<View> scrap = mScrapViews[lp.viewType];
+ if (scrap.size() < mMaxScrap) {
+ scrap.add(v);
+ }
+ }
+
+ public View getTransientStateView(int position) {
+ if (mTransientStateViews == null) {
+ return null;
+ }
+
+ final View result = mTransientStateViews.get(position);
+ if (result != null) {
+ mTransientStateViews.remove(position);
+ }
+ return result;
+ }
+
+ public View getScrapView(int type) {
+ ArrayList<View> scrap = mScrapViews[type];
+ if (scrap.isEmpty()) {
+ return null;
+ }
+
+ final int index = scrap.size() - 1;
+ final View result = scrap.get(index);
+ scrap.remove(index);
+ return result;
+ }
+ }
+
+ private class AdapterDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ mDataChanged = true;
+ mOldItemCount = mItemCount;
+ mItemCount = mAdapter.getCount();
+
+ // TODO: Consider matching these back up if we have stable IDs.
+ mRecycler.clearTransientViews();
+
+ if (!mHasStableIds) {
+ recycleAllViews();
+ }
+
+ // TODO: consider repopulating in a deferred runnable instead
+ // (so that successive changes may still be batched)
+ requestLayout();
+ }
+
+ @Override
+ public void onInvalidated() {
+ }
+ }
+}
diff --git a/src/com/android/photos/views/HeaderGridView.java b/src/com/android/photos/views/HeaderGridView.java
new file mode 100644
index 000000000..45a5eaf73
--- /dev/null
+++ b/src/com/android/photos/views/HeaderGridView.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2013 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.photos.views;
+
+import android.content.Context;
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.FrameLayout;
+import android.widget.GridView;
+import android.widget.ListAdapter;
+import android.widget.WrapperListAdapter;
+
+import java.util.ArrayList;
+
+/**
+ * A {@link GridView} that supports adding header rows in a
+ * very similar way to {@link ListView}.
+ * See {@link HeaderGridView#addHeaderView(View, Object, boolean)}
+ */
+public class HeaderGridView extends GridView {
+ private static final String TAG = "HeaderGridView";
+
+ /**
+ * A class that represents a fixed view in a list, for example a header at the top
+ * or a footer at the bottom.
+ */
+ private static class FixedViewInfo {
+ /** The view to add to the grid */
+ public View view;
+ public ViewGroup viewContainer;
+ /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */
+ public Object data;
+ /** <code>true</code> if the fixed view should be selectable in the grid */
+ public boolean isSelectable;
+ }
+
+ private ArrayList<FixedViewInfo> mHeaderViewInfos = new ArrayList<FixedViewInfo>();
+
+ private void initHeaderGridView() {
+ super.setClipChildren(false);
+ }
+
+ public HeaderGridView(Context context) {
+ super(context);
+ initHeaderGridView();
+ }
+
+ public HeaderGridView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initHeaderGridView();
+ }
+
+ public HeaderGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initHeaderGridView();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ ListAdapter adapter = getAdapter();
+ if (adapter != null && adapter instanceof HeaderViewGridAdapter) {
+ ((HeaderViewGridAdapter) adapter).setNumColumns(getNumColumns());
+ }
+ }
+
+ @Override
+ public void setClipChildren(boolean clipChildren) {
+ // Ignore, since the header rows depend on not being clipped
+ }
+
+ /**
+ * Add a fixed view to appear at the top of the grid. If addHeaderView is
+ * called more than once, the views will appear in the order they were
+ * added. Views added using this call can take focus if they want.
+ * <p>
+ * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
+ * the supplied cursor with one that will also account for header views.
+ *
+ * @param v The view to add.
+ * @param data Data to associate with this view
+ * @param isSelectable whether the item is selectable
+ */
+ public void addHeaderView(View v, Object data, boolean isSelectable) {
+ ListAdapter adapter = getAdapter();
+
+ if (adapter != null && ! (adapter instanceof HeaderViewGridAdapter)) {
+ throw new IllegalStateException(
+ "Cannot add header view to grid -- setAdapter has already been called.");
+ }
+
+ FixedViewInfo info = new FixedViewInfo();
+ FrameLayout fl = new FullWidthFixedViewLayout(getContext());
+ fl.addView(v);
+ info.view = v;
+ info.viewContainer = fl;
+ info.data = data;
+ info.isSelectable = isSelectable;
+ mHeaderViewInfos.add(info);
+
+ // in the case of re-adding a header view, or adding one later on,
+ // we need to notify the observer
+ if (adapter != null) {
+ ((HeaderViewGridAdapter) adapter).notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Add a fixed view to appear at the top of the grid. If addHeaderView is
+ * called more than once, the views will appear in the order they were
+ * added. Views added using this call can take focus if they want.
+ * <p>
+ * NOTE: Call this before calling setAdapter. This is so HeaderGridView can wrap
+ * the supplied cursor with one that will also account for header views.
+ *
+ * @param v The view to add.
+ */
+ public void addHeaderView(View v) {
+ addHeaderView(v, null, true);
+ }
+
+ public int getHeaderViewCount() {
+ return mHeaderViewInfos.size();
+ }
+
+ /**
+ * Removes a previously-added header view.
+ *
+ * @param v The view to remove
+ * @return true if the view was removed, false if the view was not a header
+ * view
+ */
+ public boolean removeHeaderView(View v) {
+ if (mHeaderViewInfos.size() > 0) {
+ boolean result = false;
+ ListAdapter adapter = getAdapter();
+ if (adapter != null && ((HeaderViewGridAdapter) adapter).removeHeader(v)) {
+ result = true;
+ }
+ removeFixedViewInfo(v, mHeaderViewInfos);
+ return result;
+ }
+ return false;
+ }
+
+ private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) {
+ int len = where.size();
+ for (int i = 0; i < len; ++i) {
+ FixedViewInfo info = where.get(i);
+ if (info.view == v) {
+ where.remove(i);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ if (mHeaderViewInfos.size() > 0) {
+ HeaderViewGridAdapter hadapter = new HeaderViewGridAdapter(mHeaderViewInfos, adapter);
+ int numColumns = getNumColumns();
+ if (numColumns > 1) {
+ hadapter.setNumColumns(numColumns);
+ }
+ super.setAdapter(hadapter);
+ } else {
+ super.setAdapter(adapter);
+ }
+ }
+
+ private class FullWidthFixedViewLayout extends FrameLayout {
+ public FullWidthFixedViewLayout(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int targetWidth = HeaderGridView.this.getMeasuredWidth()
+ - HeaderGridView.this.getPaddingLeft()
+ - HeaderGridView.this.getPaddingRight();
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth,
+ MeasureSpec.getMode(widthMeasureSpec));
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ /**
+ * ListAdapter used when a HeaderGridView has header views. This ListAdapter
+ * wraps another one and also keeps track of the header views and their
+ * associated data objects.
+ *<p>This is intended as a base class; you will probably not need to
+ * use this class directly in your own code.
+ */
+ private static class HeaderViewGridAdapter implements WrapperListAdapter, Filterable {
+
+ // This is used to notify the container of updates relating to number of columns
+ // or headers changing, which changes the number of placeholders needed
+ private final DataSetObservable mDataSetObservable = new DataSetObservable();
+
+ private final ListAdapter mAdapter;
+ private int mNumColumns = 1;
+
+ // This ArrayList is assumed to NOT be null.
+ ArrayList<FixedViewInfo> mHeaderViewInfos;
+
+ boolean mAreAllFixedViewsSelectable;
+
+ private final boolean mIsFilterable;
+
+ public HeaderViewGridAdapter(ArrayList<FixedViewInfo> headerViewInfos, ListAdapter adapter) {
+ mAdapter = adapter;
+ mIsFilterable = adapter instanceof Filterable;
+
+ if (headerViewInfos == null) {
+ throw new IllegalArgumentException("headerViewInfos cannot be null");
+ }
+ mHeaderViewInfos = headerViewInfos;
+
+ mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);
+ }
+
+ public int getHeadersCount() {
+ return mHeaderViewInfos.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return (mAdapter == null || mAdapter.isEmpty()) && getHeadersCount() == 0;
+ }
+
+ public void setNumColumns(int numColumns) {
+ if (numColumns < 1) {
+ throw new IllegalArgumentException("Number of columns must be 1 or more");
+ }
+ if (mNumColumns != numColumns) {
+ mNumColumns = numColumns;
+ notifyDataSetChanged();
+ }
+ }
+
+ private boolean areAllListInfosSelectable(ArrayList<FixedViewInfo> infos) {
+ if (infos != null) {
+ for (FixedViewInfo info : infos) {
+ if (!info.isSelectable) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public boolean removeHeader(View v) {
+ for (int i = 0; i < mHeaderViewInfos.size(); i++) {
+ FixedViewInfo info = mHeaderViewInfos.get(i);
+ if (info.view == v) {
+ mHeaderViewInfos.remove(i);
+
+ mAreAllFixedViewsSelectable = areAllListInfosSelectable(mHeaderViewInfos);
+
+ mDataSetObservable.notifyChanged();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public int getCount() {
+ if (mAdapter != null) {
+ return getHeadersCount() * mNumColumns + mAdapter.getCount();
+ } else {
+ return getHeadersCount() * mNumColumns;
+ }
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ if (mAdapter != null) {
+ return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
+ } else {
+ return true;
+ }
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+ int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+ if (position < numHeadersAndPlaceholders) {
+ return (position % mNumColumns == 0)
+ && mHeaderViewInfos.get(position / mNumColumns).isSelectable;
+ }
+
+ // Adapter
+ final int adjPosition = position - numHeadersAndPlaceholders;
+ int adapterCount = 0;
+ if (mAdapter != null) {
+ adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.isEnabled(adjPosition);
+ }
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ @Override
+ public Object getItem(int position) {
+ // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+ int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+ if (position < numHeadersAndPlaceholders) {
+ if (position % mNumColumns == 0) {
+ return mHeaderViewInfos.get(position / mNumColumns).data;
+ }
+ return null;
+ }
+
+ // Adapter
+ final int adjPosition = position - numHeadersAndPlaceholders;
+ int adapterCount = 0;
+ if (mAdapter != null) {
+ adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItem(adjPosition);
+ }
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+ if (mAdapter != null && position >= numHeadersAndPlaceholders) {
+ int adjPosition = position - numHeadersAndPlaceholders;
+ int adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItemId(adjPosition);
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ if (mAdapter != null) {
+ return mAdapter.hasStableIds();
+ }
+ return false;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+ int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns ;
+ if (position < numHeadersAndPlaceholders) {
+ View headerViewContainer = mHeaderViewInfos
+ .get(position / mNumColumns).viewContainer;
+ if (position % mNumColumns == 0) {
+ return headerViewContainer;
+ } else {
+ if (convertView == null) {
+ convertView = new View(parent.getContext());
+ }
+ // We need to do this because GridView uses the height of the last item
+ // in a row to determine the height for the entire row.
+ convertView.setVisibility(View.INVISIBLE);
+ convertView.setMinimumHeight(headerViewContainer.getHeight());
+ return convertView;
+ }
+ }
+
+ // Adapter
+ final int adjPosition = position - numHeadersAndPlaceholders;
+ int adapterCount = 0;
+ if (mAdapter != null) {
+ adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getView(adjPosition, convertView, parent);
+ }
+ }
+
+ throw new ArrayIndexOutOfBoundsException(position);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ int numHeadersAndPlaceholders = getHeadersCount() * mNumColumns;
+ if (position < numHeadersAndPlaceholders && (position % mNumColumns != 0)) {
+ // Placeholders get the last view type number
+ return mAdapter != null ? mAdapter.getViewTypeCount() : 1;
+ }
+ if (mAdapter != null && position >= numHeadersAndPlaceholders) {
+ int adjPosition = position - numHeadersAndPlaceholders;
+ int adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItemViewType(adjPosition);
+ }
+ }
+
+ return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ if (mAdapter != null) {
+ return mAdapter.getViewTypeCount() + 1;
+ }
+ return 2;
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.registerObserver(observer);
+ if (mAdapter != null) {
+ mAdapter.registerDataSetObserver(observer);
+ }
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ mDataSetObservable.unregisterObserver(observer);
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(observer);
+ }
+ }
+
+ @Override
+ public Filter getFilter() {
+ if (mIsFilterable) {
+ return ((Filterable) mAdapter).getFilter();
+ }
+ return null;
+ }
+
+ @Override
+ public ListAdapter getWrappedAdapter() {
+ return mAdapter;
+ }
+
+ public void notifyDataSetChanged() {
+ mDataSetObservable.notifyChanged();
+ }
+ }
+}
diff --git a/src/com/android/photos/views/SquareImageView.java b/src/com/android/photos/views/SquareImageView.java
new file mode 100644
index 000000000..14eff1077
--- /dev/null
+++ b/src/com/android/photos/views/SquareImageView.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 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.photos.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+
+public class SquareImageView extends ImageView {
+
+ public SquareImageView(Context context) {
+ super(context);
+ }
+
+ public SquareImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SquareImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int height = width;
+ if (heightMode == MeasureSpec.AT_MOST) {
+ height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));
+ }
+ setMeasuredDimension(width, height);
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+}
diff --git a/src/com/android/photos/views/TiledImageRenderer.java b/src/com/android/photos/views/TiledImageRenderer.java
new file mode 100644
index 000000000..c4e493b34
--- /dev/null
+++ b/src/com/android/photos/views/TiledImageRenderer.java
@@ -0,0 +1,825 @@
+/*
+ * Copyright (C) 2013 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.photos.views;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.support.v4.util.LongSparseArray;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Pools.Pool;
+import android.util.Pools.SynchronizedPool;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.GLCanvas;
+import com.android.gallery3d.glrenderer.UploadedTexture;
+
+/**
+ * Handles laying out, decoding, and drawing of tiles in GL
+ */
+public class TiledImageRenderer {
+ public static final int SIZE_UNKNOWN = -1;
+
+ private static final String TAG = "TiledImageRenderer";
+ 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()
+ * --> DECODE_FAIL - by decodeTile()
+ * RECYCLING --> RECYCLED - by decodeTile()
+ * DECODED --> ACTIVATED - (after the decoded bitmap is uploaded)
+ * DECODED --> RECYCLED - by recycleTile()
+ * DECODE_FAIL -> 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_DECODE_FAIL = 0x10;
+ private static final int STATE_RECYCLING = 0x20;
+ private static final int STATE_RECYCLED = 0x40;
+
+ private static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64);
+
+ // TILE_SIZE must be 2^N
+ private int mTileSize;
+
+ private TileSource mModel;
+ private BasicTexture mPreview;
+ 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
+ private int mLevel = 0;
+
+ 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 LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>();
+
+ // The following three queue are guarded by mQueueLock
+ private final Object mQueueLock = new Object();
+ private final TileQueue mRecycledQueue = new TileQueue();
+ private final TileQueue mUploadQueue = new TileQueue();
+ private final 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;
+
+ private boolean mLayoutTiles;
+
+ // Temp variables to avoid memory allocation
+ private final Rect mTileRange = new Rect();
+ private final Rect mActiveRange[] = {new Rect(), new Rect()};
+
+ private TileDecoder mTileDecoder;
+ private boolean mBackgroundTileUploaded;
+
+ private int mViewWidth, mViewHeight;
+ private View mParent;
+
+ /**
+ * Interface for providing tiles to a {@link TiledImageRenderer}
+ */
+ public static interface TileSource {
+
+ /**
+ * If the source does not care about the tile size, it should use
+ * {@link TiledImageRenderer#suggestedTileSize(Context)}
+ */
+ public int getTileSize();
+ public int getImageWidth();
+ public int getImageHeight();
+ public int getRotation();
+
+ /**
+ * Return a Preview image if available. This will be used as the base layer
+ * if higher res tiles are not yet available
+ */
+ public BasicTexture getPreview();
+
+ /**
+ * The tile returned by this method can be specified this way: Assuming
+ * the image size is (width, height), first take the intersection of (0,
+ * 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If
+ * in extending the region, we found some part of the region is outside
+ * the image, those pixels are filled with black.
+ *
+ * If level > 0, it does the same operation on a down-scaled version of
+ * the original image (down-scaled by a factor of 2^level), but (x, y)
+ * still refers to the coordinate on the original image.
+ *
+ * The method would be called by the decoder thread.
+ */
+ public Bitmap getTile(int level, int x, int y, Bitmap reuse);
+ }
+
+ public static int suggestedTileSize(Context context) {
+ return isHighResolution(context) ? 512 : 256;
+ }
+
+ private static boolean isHighResolution(Context context) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ WindowManager wm = (WindowManager)
+ context.getSystemService(Context.WINDOW_SERVICE);
+ wm.getDefaultDisplay().getMetrics(metrics);
+ return metrics.heightPixels > 2048 || metrics.widthPixels > 2048;
+ }
+
+ public TiledImageRenderer(View parent) {
+ mParent = parent;
+ mTileDecoder = new TileDecoder();
+ mTileDecoder.start();
+ }
+
+ public int getViewWidth() {
+ return mViewWidth;
+ }
+
+ public int getViewHeight() {
+ return mViewHeight;
+ }
+
+ private void invalidate() {
+ mParent.postInvalidate();
+ }
+
+ public void setModel(TileSource model, int rotation) {
+ if (mModel != model) {
+ mModel = model;
+ notifyModelInvalidated();
+ }
+ if (mRotation != rotation) {
+ mRotation = rotation;
+ mLayoutTiles = true;
+ }
+ }
+
+ private void calculateLevelCount() {
+ if (mPreview != null) {
+ mLevelCount = Math.max(0, Utils.ceilLog2(
+ mImageWidth / (float) mPreview.getWidth()));
+ } else {
+ int levels = 1;
+ int maxDim = Math.max(mImageWidth, mImageHeight);
+ int t = mTileSize;
+ while (t < maxDim) {
+ t <<= 1;
+ levels++;
+ }
+ mLevelCount = levels;
+ }
+ }
+
+ public void notifyModelInvalidated() {
+ invalidateTiles();
+ if (mModel == null) {
+ mImageWidth = 0;
+ mImageHeight = 0;
+ mLevelCount = 0;
+ mPreview = null;
+ } else {
+ mImageWidth = mModel.getImageWidth();
+ mImageHeight = mModel.getImageHeight();
+ mPreview = mModel.getPreview();
+ mTileSize = mModel.getTileSize();
+ calculateLevelCount();
+ }
+ mLayoutTiles = true;
+ }
+
+ public void setViewSize(int width, int height) {
+ mViewWidth = width;
+ mViewHeight = height;
+ }
+
+ public void setPosition(int centerX, int centerY, float scale) {
+ if (mCenterX == centerX && mCenterY == centerY
+ && mScale == scale) {
+ return;
+ }
+ mCenterX = centerX;
+ mCenterY = centerY;
+ mScale = scale;
+ mLayoutTiles = true;
+ }
+
+ // 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() {
+ if (mViewWidth == 0 || mViewHeight == 0 || !mLayoutTiles) {
+ return;
+ }
+ mLayoutTiles = false;
+
+ // 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 / mScale), 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 the level closest to the current scale.
+ if (mLevel != mLevelCount) {
+ Rect range = mTileRange;
+ getRange(range, mCenterX, mCenterY, mLevel, mScale, mRotation);
+ mOffsetX = Math.round(mViewWidth / 2f + (range.left - mCenterX) * mScale);
+ mOffsetY = Math.round(mViewHeight / 2f + (range.top - mCenterY) * mScale);
+ fromLevel = mScale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel;
+ } else {
+ // Activate the tiles of the smallest two levels.
+ fromLevel = mLevel - 2;
+ mOffsetX = Math.round(mViewWidth / 2f - mCenterX * mScale);
+ mOffsetY = Math.round(mViewHeight / 2f - mCenterY * mScale);
+ }
+
+ 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], mCenterX, mCenterY, i, mRotation);
+ }
+
+ // If rotation is transient, don't update the tile.
+ if (mRotation % 90 != 0) {
+ return;
+ }
+
+ synchronized (mQueueLock) {
+ 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.
+ int n = mActiveTiles.size();
+ for (int i = 0; i < n; i++) {
+ Tile tile = mActiveTiles.valueAt(i);
+ int level = tile.mTileLevel;
+ if (level < fromLevel || level >= endLevel
+ || !range[level - fromLevel].contains(tile.mX, tile.mY)) {
+ mActiveTiles.removeAt(i);
+ i--;
+ n--;
+ recycleTile(tile);
+ }
+ }
+ }
+
+ for (int i = fromLevel; i < endLevel; ++i) {
+ int size = mTileSize << 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();
+ }
+
+ private void invalidateTiles() {
+ synchronized (mQueueLock) {
+ mDecodeQueue.clean();
+ mUploadQueue.clean();
+
+ // TODO(xx): disable decoder
+ int n = mActiveTiles.size();
+ for (int i = 0; i < n; i++) {
+ Tile tile = mActiveTiles.valueAt(i);
+ 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 = mViewWidth;
+ double h = mViewHeight;
+
+ 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 = mTileSize << 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 void freeTextures() {
+ mLayoutTiles = true;
+
+ mTileDecoder.finishAndWait();
+ synchronized (mQueueLock) {
+ mUploadQueue.clean();
+ mDecodeQueue.clean();
+ Tile tile = mRecycledQueue.pop();
+ while (tile != null) {
+ tile.recycle();
+ tile = mRecycledQueue.pop();
+ }
+ }
+
+ int n = mActiveTiles.size();
+ for (int i = 0; i < n; i++) {
+ Tile texture = mActiveTiles.valueAt(i);
+ texture.recycle();
+ }
+ mActiveTiles.clear();
+ mTileRange.set(0, 0, 0, 0);
+
+ while (sTilePool.acquire() != null) {}
+ }
+
+ public boolean draw(GLCanvas canvas) {
+ layoutTiles();
+ uploadTiles(canvas);
+
+ mUploadQuota = UPLOAD_LIMIT;
+ mRenderComplete = true;
+
+ int level = mLevel;
+ int rotation = mRotation;
+ int flags = 0;
+ if (rotation != 0) {
+ flags |= GLCanvas.SAVE_FLAG_MATRIX;
+ }
+
+ if (flags != 0) {
+ canvas.save(flags);
+ if (rotation != 0) {
+ int centerX = mViewWidth / 2, centerY = mViewHeight / 2;
+ canvas.translate(centerX, centerY);
+ canvas.rotate(rotation, 0, 0, 1);
+ canvas.translate(-centerX, -centerY);
+ }
+ }
+ try {
+ if (level != mLevelCount) {
+ int size = (mTileSize << 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 (mPreview != null) {
+ mPreview.draw(canvas, mOffsetX, mOffsetY,
+ Math.round(mImageWidth * mScale),
+ Math.round(mImageHeight * mScale));
+ }
+ } finally {
+ if (flags != 0) {
+ canvas.restore();
+ }
+ }
+
+ if (mRenderComplete) {
+ if (!mBackgroundTileUploaded) {
+ uploadBackgroundTiles(canvas);
+ }
+ } else {
+ invalidate();
+ }
+ return mRenderComplete || mPreview != null;
+ }
+
+ private void uploadBackgroundTiles(GLCanvas canvas) {
+ mBackgroundTileUploaded = true;
+ int n = mActiveTiles.size();
+ for (int i = 0; i < n; i++) {
+ Tile tile = mActiveTiles.valueAt(i);
+ if (!tile.isContentValid()) {
+ queueForDecode(tile);
+ }
+ }
+ }
+
+ private void queueForDecode(Tile tile) {
+ synchronized (mQueueLock) {
+ if (tile.mTileState == STATE_ACTIVATED) {
+ tile.mTileState = STATE_IN_QUEUE;
+ if (mDecodeQueue.push(tile)) {
+ mQueueLock.notifyAll();
+ }
+ }
+ }
+ }
+
+ private void decodeTile(Tile tile) {
+ synchronized (mQueueLock) {
+ if (tile.mTileState != STATE_IN_QUEUE) {
+ return;
+ }
+ tile.mTileState = STATE_DECODING;
+ }
+ boolean decodeComplete = tile.decode();
+ synchronized (mQueueLock) {
+ if (tile.mTileState == STATE_RECYCLING) {
+ tile.mTileState = STATE_RECYCLED;
+ if (tile.mDecodedTile != null) {
+ sTilePool.release(tile.mDecodedTile);
+ tile.mDecodedTile = null;
+ }
+ mRecycledQueue.push(tile);
+ return;
+ }
+ tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL;
+ if (!decodeComplete) {
+ return;
+ }
+ mUploadQueue.push(tile);
+ }
+ invalidate();
+ }
+
+ private Tile obtainTile(int x, int y, int level) {
+ synchronized (mQueueLock) {
+ Tile tile = mRecycledQueue.pop();
+ if (tile != null) {
+ tile.mTileState = STATE_ACTIVATED;
+ tile.update(x, y, level);
+ return tile;
+ }
+ return new Tile(x, y, level);
+ }
+ }
+
+ private void recycleTile(Tile tile) {
+ synchronized (mQueueLock) {
+ if (tile.mTileState == STATE_DECODING) {
+ tile.mTileState = STATE_RECYCLING;
+ return;
+ }
+ tile.mTileState = STATE_RECYCLED;
+ if (tile.mDecodedTile != null) {
+ sTilePool.release(tile.mDecodedTile);
+ 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 result;
+ }
+
+ private void uploadTiles(GLCanvas canvas) {
+ int quota = UPLOAD_LIMIT;
+ Tile tile = null;
+ while (quota > 0) {
+ synchronized (mQueueLock) {
+ tile = mUploadQueue.pop();
+ }
+ if (tile == null) {
+ break;
+ }
+ if (!tile.isContentValid()) {
+ if (tile.mTileState == STATE_DECODED) {
+ tile.updateContent(canvas);
+ --quota;
+ } else {
+ Log.w(TAG, "Tile in upload queue has invalid state: " + tile.mTileState);
+ }
+ }
+ }
+ if (tile != null) {
+ invalidate();
+ }
+ }
+
+ // Draw the tile to a square at canvas that locates at (x, y) and
+ // has a side length of length.
+ private 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, mTileSize, mTileSize);
+
+ Tile tile = getTile(tx, ty, level);
+ if (tile != null) {
+ if (!tile.isContentValid()) {
+ if (tile.mTileState == STATE_DECODED) {
+ if (mUploadQuota > 0) {
+ --mUploadQuota;
+ tile.updateContent(canvas);
+ } else {
+ mRenderComplete = false;
+ }
+ } else if (tile.mTileState != STATE_DECODE_FAIL){
+ mRenderComplete = false;
+ queueForDecode(tile);
+ }
+ }
+ if (drawTile(tile, canvas, source, target)) {
+ return;
+ }
+ }
+ if (mPreview != null) {
+ int size = mTileSize << level;
+ float scaleX = (float) mPreview.getWidth() / mImageWidth;
+ float scaleY = (float) mPreview.getHeight() / mImageHeight;
+ source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX,
+ (ty + size) * scaleY);
+ canvas.drawTexture(mPreview, source, target);
+ }
+ }
+
+ private boolean drawTile(
+ Tile tile, GLCanvas canvas, RectF source, RectF target) {
+ while (true) {
+ if (tile.isContentValid()) {
+ 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 = (mTileSize + source.left) / 2f;
+ source.right = (mTileSize + source.right) / 2f;
+ }
+ if (tile.mY == parent.mY) {
+ source.top /= 2f;
+ source.bottom /= 2f;
+ } else {
+ source.top = (mTileSize + source.top) / 2f;
+ source.bottom = (mTileSize + source.bottom) / 2f;
+ }
+ tile = parent;
+ }
+ }
+
+ private class Tile extends UploadedTexture {
+ public int mX;
+ public int mY;
+ public int mTileLevel;
+ public Tile mNext;
+ public Bitmap mDecodedTile;
+ public 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) {
+ sTilePool.release(bitmap);
+ }
+
+ boolean decode() {
+ // Get a tile from the original image. The tile is down-scaled
+ // by (1 << mTilelevel) from a region in the original image.
+ try {
+ Bitmap reuse = sTilePool.acquire();
+ if (reuse != null && reuse.getWidth() != mTileSize) {
+ reuse = null;
+ }
+ mDecodedTile = mModel.getTile(mTileLevel, mX, mY, reuse);
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to decode tile", t);
+ }
+ return mDecodedTile != null;
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ Utils.assertTrue(mTileState == STATE_DECODED);
+
+ // We need to override the width and height, so that we won't
+ // draw beyond the boundaries.
+ int rightEdge = ((mImageWidth - mX) >> mTileLevel);
+ int bottomEdge = ((mImageHeight - mY) >> mTileLevel);
+ setSize(Math.min(mTileSize, rightEdge), Math.min(mTileSize, bottomEdge));
+
+ Bitmap bitmap = mDecodedTile;
+ mDecodedTile = null;
+ mTileState = STATE_ACTIVATED;
+ return bitmap;
+ }
+
+ // We override getTextureWidth() and getTextureHeight() here, so the
+ // texture can be re-used for different tiles regardless of the actual
+ // size of the tile (which may be small because it is a tile at the
+ // boundary).
+ @Override
+ public int getTextureWidth() {
+ return mTileSize;
+ }
+
+ @Override
+ public int getTextureHeight() {
+ return mTileSize;
+ }
+
+ 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 = mTileSize << (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 / mTileSize, mY / mTileSize, 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) {
+ if (contains(tile)) {
+ Log.w(TAG, "Attempting to add a tile already in the queue!");
+ return false;
+ }
+ boolean wasEmpty = mHead == null;
+ tile.mNext = mHead;
+ mHead = tile;
+ return wasEmpty;
+ }
+
+ private boolean contains(Tile tile) {
+ Tile other = mHead;
+ while (other != null) {
+ if (other == tile) {
+ return true;
+ }
+ other = other.mNext;
+ }
+ return false;
+ }
+
+ public void clean() {
+ mHead = null;
+ }
+ }
+
+ private class TileDecoder extends Thread {
+
+ public void finishAndWait() {
+ interrupt();
+ try {
+ join();
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Interrupted while waiting for TileDecoder thread to finish!");
+ }
+ }
+
+ private Tile waitForTile() throws InterruptedException {
+ synchronized (mQueueLock) {
+ while (true) {
+ Tile tile = mDecodeQueue.pop();
+ if (tile != null) {
+ return tile;
+ }
+ mQueueLock.wait();
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (!isInterrupted()) {
+ Tile tile = waitForTile();
+ decodeTile(tile);
+ }
+ } catch (InterruptedException ex) {
+ // We were finished
+ }
+ }
+
+ }
+}
diff --git a/src/com/android/photos/views/TiledImageView.java b/src/com/android/photos/views/TiledImageView.java
new file mode 100644
index 000000000..8bc07c051
--- /dev/null
+++ b/src/com/android/photos/views/TiledImageView.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2013 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.photos.views;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.RectF;
+import android.opengl.GLSurfaceView;
+import android.opengl.GLSurfaceView.Renderer;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.GLES20Canvas;
+import com.android.photos.views.TiledImageRenderer.TileSource;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView}
+ * or {@link BlockingGLTextureView}.
+ */
+public class TiledImageView extends FrameLayout {
+
+ private static final boolean USE_TEXTURE_VIEW = false;
+ private static final boolean IS_SUPPORTED =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
+ private static final boolean USE_CHOREOGRAPHER =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
+
+ private BlockingGLTextureView mTextureView;
+ private GLSurfaceView mGLSurfaceView;
+ private boolean mInvalPending = false;
+ private FrameCallback mFrameCallback;
+
+ private static class ImageRendererWrapper {
+ // Guarded by locks
+ float scale;
+ int centerX, centerY;
+ int rotation;
+ TileSource source;
+ Runnable isReadyCallback;
+
+ // GL thread only
+ TiledImageRenderer image;
+ }
+
+ private float[] mValues = new float[9];
+
+ // -------------------------
+ // Guarded by mLock
+ // -------------------------
+ private Object mLock = new Object();
+ private ImageRendererWrapper mRenderer;
+
+ public TiledImageView(Context context) {
+ this(context, null);
+ }
+
+ public TiledImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ if (!IS_SUPPORTED) {
+ return;
+ }
+
+ mRenderer = new ImageRendererWrapper();
+ mRenderer.image = new TiledImageRenderer(this);
+ View view;
+ if (USE_TEXTURE_VIEW) {
+ mTextureView = new BlockingGLTextureView(context);
+ mTextureView.setRenderer(new TileRenderer());
+ view = mTextureView;
+ } else {
+ mGLSurfaceView = new GLSurfaceView(context);
+ mGLSurfaceView.setEGLContextClientVersion(2);
+ mGLSurfaceView.setRenderer(new TileRenderer());
+ mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+ view = mGLSurfaceView;
+ }
+ addView(view, new LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ //setTileSource(new ColoredTiles());
+ }
+
+ public void destroy() {
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ if (USE_TEXTURE_VIEW) {
+ mTextureView.destroy();
+ } else {
+ mGLSurfaceView.queueEvent(mFreeTextures);
+ }
+ }
+
+ private Runnable mFreeTextures = new Runnable() {
+
+ @Override
+ public void run() {
+ mRenderer.image.freeTextures();
+ }
+ };
+
+ public void onPause() {
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ if (!USE_TEXTURE_VIEW) {
+ mGLSurfaceView.onPause();
+ }
+ }
+
+ public void onResume() {
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ if (!USE_TEXTURE_VIEW) {
+ mGLSurfaceView.onResume();
+ }
+ }
+
+ public void setTileSource(TileSource source, Runnable isReadyCallback) {
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ synchronized (mLock) {
+ mRenderer.source = source;
+ mRenderer.isReadyCallback = isReadyCallback;
+ mRenderer.centerX = source != null ? source.getImageWidth() / 2 : 0;
+ mRenderer.centerY = source != null ? source.getImageHeight() / 2 : 0;
+ mRenderer.rotation = source != null ? source.getRotation() : 0;
+ mRenderer.scale = 0;
+ updateScaleIfNecessaryLocked(mRenderer);
+ }
+ invalidate();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right,
+ int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ synchronized (mLock) {
+ updateScaleIfNecessaryLocked(mRenderer);
+ }
+ }
+
+ private void updateScaleIfNecessaryLocked(ImageRendererWrapper renderer) {
+ if (renderer == null || renderer.source == null
+ || renderer.scale > 0 || getWidth() == 0) {
+ return;
+ }
+ renderer.scale = Math.min(
+ (float) getWidth() / (float) renderer.source.getImageWidth(),
+ (float) getHeight() / (float) renderer.source.getImageHeight());
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ if (USE_TEXTURE_VIEW) {
+ mTextureView.render();
+ }
+ super.dispatchDraw(canvas);
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public void setTranslationX(float translationX) {
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ super.setTranslationX(translationX);
+ }
+
+ @Override
+ public void invalidate() {
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ if (USE_TEXTURE_VIEW) {
+ super.invalidate();
+ mTextureView.invalidate();
+ } else {
+ if (USE_CHOREOGRAPHER) {
+ invalOnVsync();
+ } else {
+ mGLSurfaceView.requestRender();
+ }
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void invalOnVsync() {
+ if (!mInvalPending) {
+ mInvalPending = true;
+ if (mFrameCallback == null) {
+ mFrameCallback = new FrameCallback() {
+ @Override
+ public void doFrame(long frameTimeNanos) {
+ mInvalPending = false;
+ mGLSurfaceView.requestRender();
+ }
+ };
+ }
+ Choreographer.getInstance().postFrameCallback(mFrameCallback);
+ }
+ }
+
+ private RectF mTempRectF = new RectF();
+ public void positionFromMatrix(Matrix matrix) {
+ if (!IS_SUPPORTED) {
+ return;
+ }
+ if (mRenderer.source != null) {
+ final int rotation = mRenderer.source.getRotation();
+ final boolean swap = !(rotation % 180 == 0);
+ final int width = swap ? mRenderer.source.getImageHeight()
+ : mRenderer.source.getImageWidth();
+ final int height = swap ? mRenderer.source.getImageWidth()
+ : mRenderer.source.getImageHeight();
+ mTempRectF.set(0, 0, width, height);
+ matrix.mapRect(mTempRectF);
+ matrix.getValues(mValues);
+ int cx = width / 2;
+ int cy = height / 2;
+ float scale = mValues[Matrix.MSCALE_X];
+ int xoffset = Math.round((getWidth() - mTempRectF.width()) / 2 / scale);
+ int yoffset = Math.round((getHeight() - mTempRectF.height()) / 2 / scale);
+ if (rotation == 90 || rotation == 180) {
+ cx += (mTempRectF.left / scale) - xoffset;
+ } else {
+ cx -= (mTempRectF.left / scale) - xoffset;
+ }
+ if (rotation == 180 || rotation == 270) {
+ cy += (mTempRectF.top / scale) - yoffset;
+ } else {
+ cy -= (mTempRectF.top / scale) - yoffset;
+ }
+ mRenderer.scale = scale;
+ mRenderer.centerX = swap ? cy : cx;
+ mRenderer.centerY = swap ? cx : cy;
+ invalidate();
+ }
+ }
+
+ private class TileRenderer implements Renderer {
+
+ private GLES20Canvas mCanvas;
+
+ @Override
+ public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+ mCanvas = new GLES20Canvas();
+ BasicTexture.invalidateAllTextures();
+ mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl, int width, int height) {
+ mCanvas.setSize(width, height);
+ mRenderer.image.setViewSize(width, height);
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ mCanvas.clearBuffer();
+ Runnable readyCallback;
+ synchronized (mLock) {
+ readyCallback = mRenderer.isReadyCallback;
+ mRenderer.image.setModel(mRenderer.source, mRenderer.rotation);
+ mRenderer.image.setPosition(mRenderer.centerX, mRenderer.centerY,
+ mRenderer.scale);
+ }
+ boolean complete = mRenderer.image.draw(mCanvas);
+ if (complete && readyCallback != null) {
+ synchronized (mLock) {
+ // Make sure we don't trample on a newly set callback/source
+ // if it changed while we were rendering
+ if (mRenderer.isReadyCallback == readyCallback) {
+ mRenderer.isReadyCallback = null;
+ }
+ }
+ if (readyCallback != null) {
+ post(readyCallback);
+ }
+ }
+ }
+
+ }
+
+ @SuppressWarnings("unused")
+ private static class ColoredTiles implements TileSource {
+ private static final int[] COLORS = new int[] {
+ Color.RED,
+ Color.BLUE,
+ Color.YELLOW,
+ Color.GREEN,
+ Color.CYAN,
+ Color.MAGENTA,
+ Color.WHITE,
+ };
+
+ private Paint mPaint = new Paint();
+ private Canvas mCanvas = new Canvas();
+
+ @Override
+ public int getTileSize() {
+ return 256;
+ }
+
+ @Override
+ public int getImageWidth() {
+ return 16384;
+ }
+
+ @Override
+ public int getImageHeight() {
+ return 8192;
+ }
+
+ @Override
+ public int getRotation() {
+ return 0;
+ }
+
+ @Override
+ public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
+ int tileSize = getTileSize();
+ if (bitmap == null) {
+ bitmap = Bitmap.createBitmap(tileSize, tileSize,
+ Bitmap.Config.ARGB_8888);
+ }
+ mCanvas.setBitmap(bitmap);
+ mCanvas.drawColor(COLORS[level]);
+ mPaint.setColor(Color.BLACK);
+ mPaint.setTextSize(20);
+ mPaint.setTextAlign(Align.CENTER);
+ mCanvas.drawText(x + "x" + y, 128, 128, mPaint);
+ tileSize <<= level;
+ x /= tileSize;
+ y /= tileSize;
+ mCanvas.drawText(x + "x" + y + " @ " + level, 128, 30, mPaint);
+ mCanvas.setBitmap(null);
+ return bitmap;
+ }
+
+ @Override
+ public BasicTexture getPreview() {
+ return null;
+ }
+ }
+}