summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml56
-rwxr-xr-xres/drawable-hdpi/ic_actionbar_accept.pngbin0 -> 1335 bytes
-rw-r--r--res/drawable-hdpi/ic_images.pngbin0 -> 354 bytes
-rw-r--r--res/drawable-hdpi/tile_shadow_bottom.9.pngbin0 -> 159 bytes
-rw-r--r--res/drawable-hdpi/tile_shadow_top.9.pngbin0 -> 148 bytes
-rwxr-xr-xres/drawable-mdpi/ic_actionbar_accept.pngbin0 -> 1191 bytes
-rw-r--r--res/drawable-mdpi/ic_images.pngbin0 -> 249 bytes
-rw-r--r--res/drawable-mdpi/tile_shadow_bottom.9.pngbin0 -> 140 bytes
-rw-r--r--res/drawable-mdpi/tile_shadow_top.9.pngbin0 -> 138 bytes
-rw-r--r--res/drawable-v21/ic_tick.xml31
-rw-r--r--res/drawable-v21/wallpaper_tile_fg.xml39
-rwxr-xr-xres/drawable-xhdpi/ic_actionbar_accept.pngbin0 -> 1599 bytes
-rw-r--r--res/drawable-xhdpi/ic_images.pngbin0 -> 431 bytes
-rw-r--r--res/drawable-xhdpi/tile_shadow_bottom.9.pngbin0 -> 164 bytes
-rw-r--r--res/drawable-xhdpi/tile_shadow_top.9.pngbin0 -> 151 bytes
-rwxr-xr-xres/drawable-xxhdpi/ic_actionbar_accept.pngbin0 -> 4962 bytes
-rw-r--r--res/drawable-xxhdpi/ic_images.pngbin0 -> 621 bytes
-rw-r--r--res/drawable-xxhdpi/tile_shadow_bottom.9.pngbin0 -> 185 bytes
-rw-r--r--res/drawable-xxhdpi/tile_shadow_top.9.pngbin0 -> 169 bytes
-rw-r--r--res/drawable-xxxhdpi/ic_images.pngbin0 -> 845 bytes
-rw-r--r--res/drawable/black.pngbin0 -> 67 bytes
-rw-r--r--res/drawable/wallpaper_tile_fg.xml47
-rw-r--r--res/layout/actionbar_set_wallpaper.xml32
-rw-r--r--res/layout/wallpaper_cropper.xml38
-rw-r--r--res/layout/wallpaper_picker.xml97
-rw-r--r--res/layout/wallpaper_picker_image_picker_item.xml41
-rw-r--r--res/layout/wallpaper_picker_item.xml29
-rw-r--r--res/layout/wallpaper_picker_live_wallpaper_item.xml48
-rw-r--r--res/layout/wallpaper_picker_no_wallpaper_item.xml40
-rw-r--r--res/layout/wallpaper_picker_third_party_item.xml39
-rw-r--r--res/menu/cab_delete_wallpapers.xml27
-rw-r--r--res/mipmap-hdpi/ic_launcher_wallpaper.pngbin0 -> 4418 bytes
-rw-r--r--res/mipmap-mdpi/ic_launcher_wallpaper.pngbin0 -> 2871 bytes
-rw-r--r--res/mipmap-xhdpi/ic_launcher_wallpaper.pngbin0 -> 6061 bytes
-rw-r--r--res/mipmap-xxhdpi/ic_launcher_wallpaper.pngbin0 -> 7596 bytes
-rw-r--r--res/values-af/strings.xml36
-rw-r--r--res/values-am/strings.xml36
-rw-r--r--res/values-ar/strings.xml36
-rw-r--r--res/values-az-rAZ/strings.xml36
-rw-r--r--res/values-bg/strings.xml36
-rw-r--r--res/values-bn-rBD/strings.xml36
-rw-r--r--res/values-ca/strings.xml36
-rw-r--r--res/values-cs/strings.xml36
-rw-r--r--res/values-da/strings.xml36
-rw-r--r--res/values-de/strings.xml36
-rw-r--r--res/values-el/strings.xml36
-rw-r--r--res/values-en-rAU/strings.xml36
-rw-r--r--res/values-en-rGB/strings.xml36
-rw-r--r--res/values-en-rIN/strings.xml36
-rw-r--r--res/values-es-rUS/strings.xml36
-rw-r--r--res/values-es/strings.xml36
-rw-r--r--res/values-et-rEE/strings.xml36
-rw-r--r--res/values-eu-rES/strings.xml36
-rw-r--r--res/values-fa/strings.xml36
-rw-r--r--res/values-fi/strings.xml36
-rw-r--r--res/values-fr-rCA/strings.xml36
-rw-r--r--res/values-fr/strings.xml36
-rw-r--r--res/values-gl-rES/strings.xml36
-rw-r--r--res/values-gu-rIN/strings.xml36
-rw-r--r--res/values-hi/strings.xml36
-rw-r--r--res/values-hr/strings.xml36
-rw-r--r--res/values-hu/strings.xml36
-rw-r--r--res/values-hy-rAM/strings.xml36
-rw-r--r--res/values-in/strings.xml36
-rw-r--r--res/values-is-rIS/strings.xml36
-rw-r--r--res/values-it/strings.xml36
-rw-r--r--res/values-iw/strings.xml36
-rw-r--r--res/values-ja/strings.xml36
-rw-r--r--res/values-ka-rGE/strings.xml36
-rw-r--r--res/values-kk-rKZ/strings.xml36
-rw-r--r--res/values-km-rKH/strings.xml36
-rw-r--r--res/values-kn-rIN/strings.xml36
-rw-r--r--res/values-ko/strings.xml36
-rw-r--r--res/values-ky-rKG/strings.xml36
-rw-r--r--res/values-lo-rLA/strings.xml36
-rw-r--r--res/values-lt/strings.xml36
-rw-r--r--res/values-lv/strings.xml36
-rw-r--r--res/values-mk-rMK/strings.xml36
-rw-r--r--res/values-ml-rIN/strings.xml36
-rw-r--r--res/values-mn-rMN/strings.xml36
-rw-r--r--res/values-mr-rIN/strings.xml36
-rw-r--r--res/values-ms-rMY/strings.xml36
-rw-r--r--res/values-my-rMM/strings.xml36
-rw-r--r--res/values-nb/strings.xml36
-rw-r--r--res/values-ne-rNP/strings.xml36
-rw-r--r--res/values-nl/strings.xml36
-rw-r--r--res/values-nodpi/wallpapers.xml21
-rw-r--r--res/values-pa-rIN/strings.xml36
-rw-r--r--res/values-pl/strings.xml36
-rw-r--r--res/values-pt-rPT/strings.xml36
-rw-r--r--res/values-pt/strings.xml36
-rw-r--r--res/values-ro/strings.xml36
-rw-r--r--res/values-ru/strings.xml36
-rw-r--r--res/values-si-rLK/strings.xml38
-rw-r--r--res/values-sk/strings.xml36
-rw-r--r--res/values-sl/strings.xml36
-rw-r--r--res/values-sq-rAL/strings.xml36
-rw-r--r--res/values-sr/strings.xml36
-rw-r--r--res/values-sv/strings.xml36
-rw-r--r--res/values-sw/strings.xml36
-rw-r--r--res/values-sw720dp-v19/styles.xml26
-rw-r--r--res/values-sw720dp/styles.xml24
-rw-r--r--res/values-ta-rIN/strings.xml36
-rw-r--r--res/values-te-rIN/strings.xml36
-rw-r--r--res/values-th/strings.xml36
-rw-r--r--res/values-tl/strings.xml36
-rw-r--r--res/values-tr/strings.xml36
-rw-r--r--res/values-uk/strings.xml36
-rw-r--r--res/values-ur-rPK/strings.xml36
-rw-r--r--res/values-uz-rUZ/strings.xml36
-rw-r--r--res/values-v19/styles.xml32
-rw-r--r--res/values-v21/styles.xml43
-rw-r--r--res/values-vi/strings.xml36
-rw-r--r--res/values-zh-rCN/strings.xml36
-rw-r--r--res/values-zh-rHK/strings.xml36
-rw-r--r--res/values-zh-rTW/strings.xml36
-rw-r--r--res/values-zu/strings.xml36
-rw-r--r--res/values/colors.xml24
-rw-r--r--res/values/config.xml20
-rw-r--r--res/values/dimens.xml22
-rw-r--r--res/values/strings.xml64
-rw-r--r--res/values/styles.xml51
-rw-r--r--src/com/android/gallery3d/common/BitmapCropTask.java445
-rw-r--r--src/com/android/gallery3d/common/BitmapUtils.java81
-rw-r--r--src/com/android/gallery3d/common/Utils.java129
-rw-r--r--src/com/android/gallery3d/exif/ByteBufferInputStream.java48
-rw-r--r--src/com/android/gallery3d/exif/CountedDataInputStream.java136
-rw-r--r--src/com/android/gallery3d/exif/ExifData.java348
-rw-r--r--src/com/android/gallery3d/exif/ExifInterface.java2407
-rw-r--r--src/com/android/gallery3d/exif/ExifInvalidFormatException.java23
-rw-r--r--src/com/android/gallery3d/exif/ExifModifier.java195
-rw-r--r--src/com/android/gallery3d/exif/ExifOutputStream.java518
-rw-r--r--src/com/android/gallery3d/exif/ExifParser.java916
-rw-r--r--src/com/android/gallery3d/exif/ExifReader.java92
-rw-r--r--src/com/android/gallery3d/exif/ExifTag.java1008
-rw-r--r--src/com/android/gallery3d/exif/IfdData.java152
-rw-r--r--src/com/android/gallery3d/exif/IfdId.java31
-rw-r--r--src/com/android/gallery3d/exif/JpegHeader.java39
-rw-r--r--src/com/android/gallery3d/exif/OrderedDataOutputStream.java56
-rw-r--r--src/com/android/gallery3d/exif/Rational.java88
-rw-r--r--src/com/android/gallery3d/glrenderer/BasicTexture.java211
-rw-r--r--src/com/android/gallery3d/glrenderer/BitmapTexture.java54
-rw-r--r--src/com/android/gallery3d/glrenderer/GLCanvas.java215
-rw-r--r--src/com/android/gallery3d/glrenderer/GLES20Canvas.java1008
-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/IntArray.java60
-rw-r--r--src/com/android/gallery3d/glrenderer/RawTexture.java73
-rw-r--r--src/com/android/gallery3d/glrenderer/Texture.java44
-rw-r--r--src/com/android/gallery3d/glrenderer/UploadedTexture.java300
-rw-r--r--src/com/android/photos/BitmapRegionTileSource.java563
-rw-r--r--src/com/android/photos/views/TiledImageRenderer.java828
-rw-r--r--src/com/android/photos/views/TiledImageView.java314
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/AlphaDisableableButton.java50
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/CheckableFrameLayout.java63
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/CropView.java321
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/DrawableTileSource.java102
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/LiveWallpaperListAdapter.java203
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/Partner.java109
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/PickerFiles.java30
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/SavedWallpaperImages.java227
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/StylusEventHelper.java82
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/ThirdPartyWallpaperPickerListAdapter.java137
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/Utilities.java549
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/WallpaperCropActivity.java540
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/WallpaperPickerActivity.java1410
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/base/BaseActivity.java21
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/util/Constants.java24
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/util/Thunk.java43
-rw-r--r--src/org/cyanogenmod/wallpaperpicker/util/WallpaperUtils.java125
171 files changed, 18199 insertions, 0 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..c19886d
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.cyanogenmod.wallpaperpicker"
+ android:versionCode="1"
+ android:versionName="1.0"
+ >
+
+ <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23" />
+
+ <!-- Permissions for setting the wallpaper -->
+ <uses-permission android:name="android.permission.SET_WALLPAPER" />
+ <uses-permission android:name="android.permission.SET_WALLPAPER_HINTS" />
+ <!-- Permissions for setting lock screen wallpaper -->
+ <uses-permission android:name="android.permission.SET_KEYGUARD_WALLPAPER" />
+ <!-- Permissions for setting images from external storage as wallpaper -->
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <!-- Permissions for reading themes -->
+ <uses-permission android:name="cyanogenmod.permission.READ_THEMES" />
+
+ <application
+ android:label="@string/app_name"
+ android:restoreAnyVersion="true"
+ android:supportsRtl="true"
+ android:icon="@mipmap/ic_launcher_wallpaper">
+
+ <activity
+ android:name="org.cyanogenmod.wallpaperpicker.WallpaperPickerActivity"
+ android:theme="@style/Theme.WallpaperPicker"
+ android:label="@string/pick_wallpaper"
+ android:icon="@mipmap/ic_launcher_wallpaper"
+ android:finishOnCloseSystemDialogs="true">
+ <intent-filter>
+ <action android:name="android.intent.action.SET_WALLPAPER" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.SET_KEYGUARD_WALLPAPER" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name="org.cyanogenmod.wallpaperpicker.WallpaperCropActivity"
+ android:theme="@style/Theme.WallpaperCropper"
+ android:label="@string/crop_wallpaper"
+ android:icon="@mipmap/ic_launcher_wallpaper"
+ android:finishOnCloseSystemDialogs="true">
+ <intent-filter>
+ <action android:name="android.service.wallpaper.CROP_AND_SET_WALLPAPER" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="image/*" />
+ </intent-filter>
+ </activity>
+
+ </application>
+</manifest>
diff --git a/res/drawable-hdpi/ic_actionbar_accept.png b/res/drawable-hdpi/ic_actionbar_accept.png
new file mode 100755
index 0000000..53cf687
--- /dev/null
+++ b/res/drawable-hdpi/ic_actionbar_accept.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_images.png b/res/drawable-hdpi/ic_images.png
new file mode 100644
index 0000000..15e511c
--- /dev/null
+++ b/res/drawable-hdpi/ic_images.png
Binary files differ
diff --git a/res/drawable-hdpi/tile_shadow_bottom.9.png b/res/drawable-hdpi/tile_shadow_bottom.9.png
new file mode 100644
index 0000000..e80558b
--- /dev/null
+++ b/res/drawable-hdpi/tile_shadow_bottom.9.png
Binary files differ
diff --git a/res/drawable-hdpi/tile_shadow_top.9.png b/res/drawable-hdpi/tile_shadow_top.9.png
new file mode 100644
index 0000000..7e93865
--- /dev/null
+++ b/res/drawable-hdpi/tile_shadow_top.9.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_actionbar_accept.png b/res/drawable-mdpi/ic_actionbar_accept.png
new file mode 100755
index 0000000..35cda8e
--- /dev/null
+++ b/res/drawable-mdpi/ic_actionbar_accept.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_images.png b/res/drawable-mdpi/ic_images.png
new file mode 100644
index 0000000..c4a2229
--- /dev/null
+++ b/res/drawable-mdpi/ic_images.png
Binary files differ
diff --git a/res/drawable-mdpi/tile_shadow_bottom.9.png b/res/drawable-mdpi/tile_shadow_bottom.9.png
new file mode 100644
index 0000000..d95787b
--- /dev/null
+++ b/res/drawable-mdpi/tile_shadow_bottom.9.png
Binary files differ
diff --git a/res/drawable-mdpi/tile_shadow_top.9.png b/res/drawable-mdpi/tile_shadow_top.9.png
new file mode 100644
index 0000000..8da913c
--- /dev/null
+++ b/res/drawable-mdpi/tile_shadow_top.9.png
Binary files differ
diff --git a/res/drawable-v21/ic_tick.xml b/res/drawable-v21/ic_tick.xml
new file mode 100644
index 0000000..5b27027
--- /dev/null
+++ b/res/drawable-v21/ic_tick.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="48dp"
+ android:viewportHeight="48"
+ android:viewportWidth="48"
+ android:width="48dp" >
+
+ <group>
+ <path
+ android:name="tick"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M18 32.34l-8.34-8.34-2.83 2.83 11.17 11.17 24-24-2.83-2.83z" />
+ </group>
+
+</vector> \ No newline at end of file
diff --git a/res/drawable-v21/wallpaper_tile_fg.xml b/res/drawable-v21/wallpaper_tile_fg.xml
new file mode 100644
index 0000000..97cdcd6
--- /dev/null
+++ b/res/drawable-v21/wallpaper_tile_fg.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#66FFFFFF" >
+
+ <item
+ android:id="@android:id/mask"
+ android:drawable="@android:color/white"/>
+ <item
+ android:bottom="23.25dp"
+ android:left="29.25dp"
+ android:right="29.25dp"
+ android:top="23.25dp">
+ <selector>
+ <item
+ android:drawable="@drawable/ic_tick"
+ android:state_selected="true"/>
+ <item
+ android:drawable="@drawable/ic_tick"
+ android:state_checked="true"/>
+ </selector>
+ </item>
+
+</ripple> \ No newline at end of file
diff --git a/res/drawable-xhdpi/ic_actionbar_accept.png b/res/drawable-xhdpi/ic_actionbar_accept.png
new file mode 100755
index 0000000..b52dc37
--- /dev/null
+++ b/res/drawable-xhdpi/ic_actionbar_accept.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_images.png b/res/drawable-xhdpi/ic_images.png
new file mode 100644
index 0000000..4974792
--- /dev/null
+++ b/res/drawable-xhdpi/ic_images.png
Binary files differ
diff --git a/res/drawable-xhdpi/tile_shadow_bottom.9.png b/res/drawable-xhdpi/tile_shadow_bottom.9.png
new file mode 100644
index 0000000..81571f3
--- /dev/null
+++ b/res/drawable-xhdpi/tile_shadow_bottom.9.png
Binary files differ
diff --git a/res/drawable-xhdpi/tile_shadow_top.9.png b/res/drawable-xhdpi/tile_shadow_top.9.png
new file mode 100644
index 0000000..8503a59
--- /dev/null
+++ b/res/drawable-xhdpi/tile_shadow_top.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_actionbar_accept.png b/res/drawable-xxhdpi/ic_actionbar_accept.png
new file mode 100755
index 0000000..d9ad51c
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_actionbar_accept.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_images.png b/res/drawable-xxhdpi/ic_images.png
new file mode 100644
index 0000000..c8b9f75
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_images.png
Binary files differ
diff --git a/res/drawable-xxhdpi/tile_shadow_bottom.9.png b/res/drawable-xxhdpi/tile_shadow_bottom.9.png
new file mode 100644
index 0000000..55250f0
--- /dev/null
+++ b/res/drawable-xxhdpi/tile_shadow_bottom.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi/tile_shadow_top.9.png b/res/drawable-xxhdpi/tile_shadow_top.9.png
new file mode 100644
index 0000000..3f22633
--- /dev/null
+++ b/res/drawable-xxhdpi/tile_shadow_top.9.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_images.png b/res/drawable-xxxhdpi/ic_images.png
new file mode 100644
index 0000000..a19002e
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_images.png
Binary files differ
diff --git a/res/drawable/black.png b/res/drawable/black.png
new file mode 100644
index 0000000..6aaccb2
--- /dev/null
+++ b/res/drawable/black.png
Binary files differ
diff --git a/res/drawable/wallpaper_tile_fg.xml b/res/drawable/wallpaper_tile_fg.xml
new file mode 100644
index 0000000..c66fa50
--- /dev/null
+++ b/res/drawable/wallpaper_tile_fg.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_checked="true" >
+ <shape>
+ <stroke
+ android:width="2dp"
+ android:color="#FFFFFFFF" />
+ <solid android:color="#33FFFFFF"/>
+ </shape>
+ </item>
+ <item android:state_focused="true" >
+ <shape>
+ <stroke
+ android:width="2dp"
+ android:color="#FFFFFFFF" />
+ </shape>
+ </item>
+ <item android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <solid android:color="#33FFFFFF"/>
+ </shape>
+ </item>
+ <item android:state_selected="true" >
+ <shape>
+ <stroke
+ android:width="2dp"
+ android:color="#FFFFFFFF" />
+ <solid android:color="#33FFFFFF"/>
+ </shape>
+ </item>
+ <item android:drawable="@android:color/transparent" />
+</selector>
diff --git a/res/layout/actionbar_set_wallpaper.xml b/res/layout/actionbar_set_wallpaper.xml
new file mode 100644
index 0000000..d3a6560
--- /dev/null
+++ b/res/layout/actionbar_set_wallpaper.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<org.cyanogenmod.wallpaperpicker.AlphaDisableableButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/ActionBarSetWallpaperStyle"
+ android:id="@+id/set_wallpaper_button"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingRight="20dp"
+ android:drawableLeft="@drawable/ic_actionbar_accept"
+ android:drawablePadding="8dp"
+ android:gravity="start|center_vertical"
+ android:text="@string/wallpaper_instructions">
+</org.cyanogenmod.wallpaperpicker.AlphaDisableableButton>
diff --git a/res/layout/wallpaper_cropper.xml b/res/layout/wallpaper_cropper.xml
new file mode 100644
index 0000000..84d6ec2
--- /dev/null
+++ b/res/layout/wallpaper_cropper.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <org.cyanogenmod.wallpaperpicker.CropView
+ android:id="@+id/cropView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+ <ProgressBar
+ android:id="@+id/loading"
+ style="?android:attr/progressBarStyleLarge"
+ android:visibility="invisible"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:indeterminate="true"
+ android:indeterminateOnly="true"
+ android:background="@android:color/transparent" />
+</RelativeLayout>
diff --git a/res/layout/wallpaper_picker.xml b/res/layout/wallpaper_picker.xml
new file mode 100644
index 0000000..0a8cfad
--- /dev/null
+++ b/res/layout/wallpaper_picker.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:launcher="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <org.cyanogenmod.wallpaperpicker.CropView
+ android:id="@+id/cropView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <ProgressBar
+ android:id="@+id/loading"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true"
+ android:indeterminateOnly="true"
+ android:visibility="invisible" />
+
+ <LinearLayout
+ android:id="@+id/wallpaper_strip"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:fitsSystemWindows="true"
+ android:orientation="vertical" >
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="2dp"
+ android:background="@drawable/tile_shadow_top" />
+
+ <HorizontalScrollView
+ android:id="@+id/wallpaper_scroll_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <LinearLayout
+ android:id="@+id/master_wallpaper_list"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+
+ <LinearLayout
+ android:id="@+id/wallpaper_list"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" />
+
+ <LinearLayout
+ android:id="@+id/theme_wallpaper_list"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" />
+
+ <LinearLayout
+ android:id="@+id/live_wallpaper_list"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" />
+
+ <LinearLayout
+ android:id="@+id/third_party_wallpaper_list"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" />
+ </LinearLayout>
+ </HorizontalScrollView>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="2dp"
+ android:background="@drawable/tile_shadow_bottom" />
+ </LinearLayout>
+
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/wallpaper_picker_image_picker_item.xml b/res/layout/wallpaper_picker_image_picker_item.xml
new file mode 100644
index 0000000..5da3288
--- /dev/null
+++ b/res/layout/wallpaper_picker_image_picker_item.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<org.cyanogenmod.wallpaperpicker.CheckableFrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/wallpaperThumbnailWidth"
+ android:layout_height="@dimen/wallpaperThumbnailHeight"
+ android:focusable="true"
+ android:clickable="true"
+ android:foreground="@drawable/wallpaper_tile_fg">
+ <ImageView
+ android:id="@+id/wallpaper_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/wallpaper_picker_translucent_gray"
+ android:scaleType="centerCrop" />
+ <TextView
+ android:id="@+id/wallpaper_item_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:gravity="center"
+ android:layout_gravity="center"
+ android:text="@string/pick_image"
+ android:drawableTop="@drawable/ic_images"
+ android:drawablePadding="4dp"
+ android:textColor="@android:color/white"/>
+</org.cyanogenmod.wallpaperpicker.CheckableFrameLayout>
diff --git a/res/layout/wallpaper_picker_item.xml b/res/layout/wallpaper_picker_item.xml
new file mode 100644
index 0000000..fc7e348
--- /dev/null
+++ b/res/layout/wallpaper_picker_item.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<org.cyanogenmod.wallpaperpicker.CheckableFrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/wallpaperThumbnailWidth"
+ android:layout_height="@dimen/wallpaperThumbnailHeight"
+ android:focusable="true"
+ android:clickable="true"
+ android:foreground="@drawable/wallpaper_tile_fg">
+ <ImageView
+ android:id="@+id/wallpaper_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop" />
+</org.cyanogenmod.wallpaperpicker.CheckableFrameLayout>
diff --git a/res/layout/wallpaper_picker_live_wallpaper_item.xml b/res/layout/wallpaper_picker_live_wallpaper_item.xml
new file mode 100644
index 0000000..a7afe2c
--- /dev/null
+++ b/res/layout/wallpaper_picker_live_wallpaper_item.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<org.cyanogenmod.wallpaperpicker.CheckableFrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/wallpaperThumbnailWidth"
+ android:layout_height="@dimen/wallpaperThumbnailHeight"
+ android:focusable="true"
+ android:clickable="true"
+ android:foreground="@drawable/wallpaper_tile_fg">
+ <ImageView
+ android:id="@+id/wallpaper_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:background="@android:color/black"
+ android:scaleType="centerCrop" />
+ <ImageView
+ android:id="@+id/wallpaper_icon"
+ android:layout_width="@dimen/wallpaperItemIconSize"
+ android:layout_height="@dimen/wallpaperItemIconSize"
+ android:layout_gravity="center"
+ android:visibility="gone" />
+ <TextView
+ android:id="@+id/wallpaper_item_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:gravity="center"
+ android:padding="4dp"
+ android:layout_gravity="bottom"
+ android:background="@color/wallpaper_picker_translucent_gray"
+ android:textColor="@android:color/white"/>
+</org.cyanogenmod.wallpaperpicker.CheckableFrameLayout>
diff --git a/res/layout/wallpaper_picker_no_wallpaper_item.xml b/res/layout/wallpaper_picker_no_wallpaper_item.xml
new file mode 100644
index 0000000..0743c44
--- /dev/null
+++ b/res/layout/wallpaper_picker_no_wallpaper_item.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<org.cyanogenmod.wallpaperpicker.CheckableFrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/wallpaperThumbnailWidth"
+ android:layout_height="@dimen/wallpaperThumbnailHeight"
+ android:focusable="true"
+ android:clickable="true"
+ android:foreground="@drawable/wallpaper_tile_fg">
+ <ImageView
+ android:id="@+id/wallpa1per_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/wallpaper_picker_translucent_gray"
+ android:scaleType="centerCrop"
+ android:src="@drawable/black"/>
+ <TextView
+ android:id="@+id/wallpaper_item_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:gravity="center"
+ android:layout_gravity="center"
+ android:text="@string/no_wallpaper"
+ android:textColor="@android:color/white"/>
+</org.cyanogenmod.wallpaperpicker.CheckableFrameLayout>
diff --git a/res/layout/wallpaper_picker_third_party_item.xml b/res/layout/wallpaper_picker_third_party_item.xml
new file mode 100644
index 0000000..45c22f7
--- /dev/null
+++ b/res/layout/wallpaper_picker_third_party_item.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<org.cyanogenmod.wallpaperpicker.CheckableFrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/wallpaperThumbnailWidth"
+ android:layout_height="@dimen/wallpaperThumbnailHeight"
+ android:focusable="true"
+ android:clickable="true"
+ android:foreground="@drawable/wallpaper_tile_fg">
+ <ImageView
+ android:id="@+id/wallpaper_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/wallpaper_picker_translucent_gray"
+ android:scaleType="centerCrop" />
+ <TextView
+ android:id="@+id/wallpaper_item_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:gravity="center"
+ android:layout_gravity="center"
+ android:drawablePadding="4dp"
+ android:textColor="@android:color/white"/>
+</org.cyanogenmod.wallpaperpicker.CheckableFrameLayout>
diff --git a/res/menu/cab_delete_wallpapers.xml b/res/menu/cab_delete_wallpapers.xml
new file mode 100644
index 0000000..38ac5c4
--- /dev/null
+++ b/res/menu/cab_delete_wallpapers.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:id="@+id/menu_delete"
+ android:title="@string/wallpaper_delete"
+ android:showAsAction="always"
+ android:icon="@android:drawable/ic_menu_delete" />
+</menu>
diff --git a/res/mipmap-hdpi/ic_launcher_wallpaper.png b/res/mipmap-hdpi/ic_launcher_wallpaper.png
new file mode 100644
index 0000000..affee85
--- /dev/null
+++ b/res/mipmap-hdpi/ic_launcher_wallpaper.png
Binary files differ
diff --git a/res/mipmap-mdpi/ic_launcher_wallpaper.png b/res/mipmap-mdpi/ic_launcher_wallpaper.png
new file mode 100644
index 0000000..cb4443b
--- /dev/null
+++ b/res/mipmap-mdpi/ic_launcher_wallpaper.png
Binary files differ
diff --git a/res/mipmap-xhdpi/ic_launcher_wallpaper.png b/res/mipmap-xhdpi/ic_launcher_wallpaper.png
new file mode 100644
index 0000000..60f8dce
--- /dev/null
+++ b/res/mipmap-xhdpi/ic_launcher_wallpaper.png
Binary files differ
diff --git a/res/mipmap-xxhdpi/ic_launcher_wallpaper.png b/res/mipmap-xxhdpi/ic_launcher_wallpaper.png
new file mode 100644
index 0000000..023fb58
--- /dev/null
+++ b/res/mipmap-xxhdpi/ic_launcher_wallpaper.png
Binary files differ
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
new file mode 100644
index 0000000..eb81640
--- /dev/null
+++ b/res/values-af/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Stel muurpapier"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Kon nie prent laai nie"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Kon nie prent as muurpapier laai nie"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d gekies"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d gekies"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d gekies"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Muurpapier %1$d van %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Het <xliff:g id="LABEL">%1$s</xliff:g> gekies"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Vee uit"</string>
+ <string name="pick_image" msgid="3189640419551368385">"My foto\'s"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Muurpapiere"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Snoei muurpapier"</string>
+</resources>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
new file mode 100644
index 0000000..7e79385
--- /dev/null
+++ b/res/values-am/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"ልጣፍ አዘጋጅ"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"ምስሉን መጫን አልተቻለም"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"ምስሉን እንደ ግድግዳ ወረቀት መጫን አልተቻለም"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d ተመርጧል"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d ተመርጧል"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d ተመርጧል"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"ልጣፍ %1$d የ%2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> ተመርጧል"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"ሰርዝ"</string>
+ <string name="pick_image" msgid="3189640419551368385">"የእኔ ፎቶዎች"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"የግድግዳ ወረቀቶች"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"ልጣፍ ይከርክሙ"</string>
+</resources>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
new file mode 100644
index 0000000..84fa104
--- /dev/null
+++ b/res/values-ar/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"تعيين الخلفية"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"تعذر تحميل الصورة"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"تعذر تحميل الصورة كخلفية"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"‏تم تحديد %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"‏تم تحديد %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"‏تم تحديد %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"‏الخلفية %1$d من %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"تم تحديد <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"حذف"</string>
+ <string name="pick_image" msgid="3189640419551368385">"صوري"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"الخلفيات"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"اقتصاص الخلفية"</string>
+</resources>
diff --git a/res/values-az-rAZ/strings.xml b/res/values-az-rAZ/strings.xml
new file mode 100644
index 0000000..883673d
--- /dev/null
+++ b/res/values-az-rAZ/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Divar kağı seçin"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Şəkli yükləmək alınmadı"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Şəkli divar kağızı olaraq yükləmək alınmadı"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d seçilib"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d seçilib"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d seçilib"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Divar kağızı %1$d of %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> seçilib"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Sil"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Fotolarım"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Divar kağızları"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Divar kağızını kəsin"</string>
+</resources>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
new file mode 100644
index 0000000..60ae302
--- /dev/null
+++ b/res/values-bg/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Задаване на тапета"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Изображението не можа да бъде заредено"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Изображението не можа да бъде заредено като тапет"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Избрахте %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Избрахте %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Избрахте %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Тапет %1$d от %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Избрахте <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Изтриване"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Моите снимки"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Тапети"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Подрязване на тапета"</string>
+</resources>
diff --git a/res/values-bn-rBD/strings.xml b/res/values-bn-rBD/strings.xml
new file mode 100644
index 0000000..74d7e57
--- /dev/null
+++ b/res/values-bn-rBD/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"ওয়ালপেপার সেট করুন"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"চিত্র লোড করা যায়নি"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"ওয়ালপেপার হিসাবে চিত্র লোড করা যায়নি"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$dটি নির্বাচন করা হয়েছে"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$dটি নির্বাচন করা হয়েছে"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$dটি নির্বাচন করা হয়েছে"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%2$dটির মধ্যে %1$dটি ওয়ালপেপার"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> নির্বাচন করা হয়েছে"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"মুছুন"</string>
+ <string name="pick_image" msgid="3189640419551368385">"আমার ফটো"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"ওয়ালপেপারগুলি"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"ওয়ালপেপার কাটছাঁট করুন"</string>
+</resources>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
new file mode 100644
index 0000000..cbec762
--- /dev/null
+++ b/res/values-ca/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Estableix el fons de pantalla"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"No s\'ha pogut carregar la imatge."</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"No s\'ha pogut carregar la imatge com a fons de pantalla."</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Seleccionats: %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Seleccionats: %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Seleccionats: %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Fons de pantalla %1$d de %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"S\'ha seleccionat <xliff:g id="LABEL">%1$s</xliff:g>."</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Suprimeix"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Les meves fotos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Fons de pantalla"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Retallar fons de pantalla"</string>
+</resources>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
new file mode 100644
index 0000000..da0758e
--- /dev/null
+++ b/res/values-cs/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Nastavit jako tapetu"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Obrázek nelze načíst."</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Obrázek nelze načíst jako tapetu."</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Vybráno: %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Vybráno: %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Vybráno: %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Tapeta %1$d z %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Vybrána položka <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Smazat"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Moje fotografie"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Tapety"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Oříznutí tapety"</string>
+</resources>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
new file mode 100644
index 0000000..12c0fe8
--- /dev/null
+++ b/res/values-da/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Angiv baggrund"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Billedet kunne ikke indlæses"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Billedet kunne ikke indlæses som baggrund"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d er valgt"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d er valgt"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d er valgt"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Baggrund %1$d af %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> blev valgt"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Slet"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Mine billeder"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Baggrunde"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Beskær baggrunden"</string>
+</resources>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
new file mode 100644
index 0000000..3171b3f
--- /dev/null
+++ b/res/values-de/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Hintergrund auswählen"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Bild konnte nicht geladen werden."</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Bild konnte nicht als Hintergrund geladen werden."</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d ausgewählt"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d ausgewählt"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d ausgewählt"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Hintergrund %1$d von %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> ausgewählt"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Löschen"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Meine Fotos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Hintergründe"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Hintergrund zuschneiden"</string>
+</resources>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
new file mode 100644
index 0000000..2988bec
--- /dev/null
+++ b/res/values-el/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Ορισμός ταπετσαρίας"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Δεν ήταν δυνατή η φόρτωση της εικόνας"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Δεν ήταν δυνατή η φόρτωση της εικόνας ως ταπετσαρία"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d επιλεγμένα"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d επιλεγμένα"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d επιλεγμένα"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Ταπετσαρία %1$d από %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Επιλέχθηκε το <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Διαγραφή"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Οι φωτογραφίες μου"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Ταπετσαρίες"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Περικοπή ταπετσαρίας"</string>
+</resources>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..30450ee
--- /dev/null
+++ b/res/values-en-rAU/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Set wallpaper"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Couldn\'t load image"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Couldn\'t load image as wallpaper"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d selected"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d selected"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d selected"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Wallpaper %1$d of %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Selected <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Delete"</string>
+ <string name="pick_image" msgid="3189640419551368385">"My photos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Wallpapers"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Crop wallpaper"</string>
+</resources>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..30450ee
--- /dev/null
+++ b/res/values-en-rGB/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Set wallpaper"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Couldn\'t load image"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Couldn\'t load image as wallpaper"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d selected"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d selected"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d selected"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Wallpaper %1$d of %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Selected <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Delete"</string>
+ <string name="pick_image" msgid="3189640419551368385">"My photos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Wallpapers"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Crop wallpaper"</string>
+</resources>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..30450ee
--- /dev/null
+++ b/res/values-en-rIN/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Set wallpaper"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Couldn\'t load image"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Couldn\'t load image as wallpaper"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d selected"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d selected"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d selected"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Wallpaper %1$d of %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Selected <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Delete"</string>
+ <string name="pick_image" msgid="3189640419551368385">"My photos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Wallpapers"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Crop wallpaper"</string>
+</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..c18ecb2
--- /dev/null
+++ b/res/values-es-rUS/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Establecer como fondo de pantalla"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"No se pudo cargar la imagen."</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"No se pudo cargar la imagen como fondo de pantalla."</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d seleccionado"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d seleccionado"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d seleccionados"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Fondo de pantalla %1$d de %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> seleccionado"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Eliminar"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Mis fotos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Fondos de pantalla"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Recortar fondo de pantalla"</string>
+</resources>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
new file mode 100644
index 0000000..b7221af
--- /dev/null
+++ b/res/values-es/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Establecer fondo"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"No se ha podido cargar la imagen"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"No se ha podido cargar la imagen como fondo de pantalla"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Seleccionados: %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Seleccionados: %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Seleccionados: %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Fondo de pantalla %1$d de %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> seleccionado"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Eliminar"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Mis fotos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Fondos de pantalla"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Recortar fondo de pantalla"</string>
+</resources>
diff --git a/res/values-et-rEE/strings.xml b/res/values-et-rEE/strings.xml
new file mode 100644
index 0000000..571a0ff
--- /dev/null
+++ b/res/values-et-rEE/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Määra taustapilt"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Kujutist ei õnnestunud laadida"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Kujutist ei õnnestunud taustapildina laadida"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Valitud on %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Valitud on %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Valitud on %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%1$d/%2$d taustapildist"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Valitud on <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Kustuta"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Minu fotod"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Taustapildid"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Taustapildi kärpimine"</string>
+</resources>
diff --git a/res/values-eu-rES/strings.xml b/res/values-eu-rES/strings.xml
new file mode 100644
index 0000000..45bf7a1
--- /dev/null
+++ b/res/values-eu-rES/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Ezarri horma-papera"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Ezin izan da irudia kargatu"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Ezin izan da irudia horma-paper gisa kargatu"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d hautatuta"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d hautatuta"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d hautatuta"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%1$d/%2$d horma-papera"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> hautatu da"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Ezabatu"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Nire argazkiak"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Horma-paperak"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Ebaki horma-papera"</string>
+</resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
new file mode 100644
index 0000000..469ec12
--- /dev/null
+++ b/res/values-fa/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"تنظیم کاغذدیواری"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"تصویر بارگیری نشد"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"تصویر به‌عنوان کاغذدیواری بارگیری نشد"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"‏%1$d انتخاب شد"</item>
+ <item quantity="one" msgid="8409622005831789373">"‏%1$d انتخاب شد"</item>
+ <item quantity="other" msgid="479468347731745357">"‏%1$d انتخاب شد"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"‏کاغذدیواری %1$d از %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> انتخاب شد"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"حذف"</string>
+ <string name="pick_image" msgid="3189640419551368385">"عکس‌های من"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"کاغذدیواری‌ها"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"برش کاغذدیواری"</string>
+</resources>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
new file mode 100644
index 0000000..c82d3e0
--- /dev/null
+++ b/res/values-fi/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Aseta taustakuva"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Kuvan lataus epäonnistui"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Kuvaa ei voitu ladata taustakuvaksi"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d valittu"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d valittu"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d valittu"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Taustakuva %1$d/%2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Valittu: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Poista"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Omat valokuvat"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Taustakuvat"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Rajaa taustakuva"</string>
+</resources>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..9256896
--- /dev/null
+++ b/res/values-fr-rCA/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Définir le fond d\'écran"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Impossible de charger l\'image"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Impossible de charger l\'image comme fond d\'écran"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d sélectionné"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d sélectionné"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d sélectionné(s)"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Fond d\'écran %1$d de %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Sélection : <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Supprimer"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Mes photos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Fonds d\'écran"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Rogner le fond d\'écran"</string>
+</resources>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
new file mode 100644
index 0000000..479a7e4
--- /dev/null
+++ b/res/values-fr/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Définir comme fond d\'écran"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Impossible de charger l\'image."</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Impossible de charger l\'image comme fond d\'écran."</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d élément sélectionné"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d élément sélectionné"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d éléments sélectionnés"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Fond d\'écran %1$d sur %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> sélectionné"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Supprimer"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Mes photos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Fonds d\'écran"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Recadrer le fond d\'écran"</string>
+</resources>
diff --git a/res/values-gl-rES/strings.xml b/res/values-gl-rES/strings.xml
new file mode 100644
index 0000000..0396b2f
--- /dev/null
+++ b/res/values-gl-rES/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Establecer fondo de pantalla"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Non se puido cargar a imaxe"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Non se puido cargar a imaxe como fondo de pantalla"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Seleccionaches %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Seleccionaches %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Seleccionaches %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Fondo de pantalla %1$d de %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Seleccionaches <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Eliminar"</string>
+ <string name="pick_image" msgid="3189640419551368385">"As miñas fotos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Fondos de pantalla"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Recortar fondo de pantalla"</string>
+</resources>
diff --git a/res/values-gu-rIN/strings.xml b/res/values-gu-rIN/strings.xml
new file mode 100644
index 0000000..e201d52
--- /dev/null
+++ b/res/values-gu-rIN/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"વૉલપેપર સેટ કરો"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"છબી લોડ કરી શકાઈ નથી"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"વૉલપેપર તરીકે છબી લોડ કરી શકાઈ નથી"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d પસંદ કર્યો"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d પસંદ કર્યો"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d પસંદ કર્યો"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%2$d માંથી %1$d વૉલપેપર"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> પસંદ કર્યો"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"કાઢી નાખો"</string>
+ <string name="pick_image" msgid="3189640419551368385">"મારા ફોટા"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"વૉલપેપર્સ"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"વૉલપેપર કાપો"</string>
+</resources>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
new file mode 100644
index 0000000..60834e1
--- /dev/null
+++ b/res/values-hi/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"वॉलपेपर सेट करें"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"चित्र लोड नहीं किया जा सका"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"चित्र को वॉलपेपर के रूप में लोड नहीं किया जा सका"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d चयनित"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d चयनित"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d चयनित"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"वॉलपेपर %2$d में से %1$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"चयनित <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"हटाएं"</string>
+ <string name="pick_image" msgid="3189640419551368385">"मेरी फ़ोटो"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"वॉलपेपर"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"वॉलपेपर काटें"</string>
+</resources>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
new file mode 100644
index 0000000..9ed702c
--- /dev/null
+++ b/res/values-hr/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Postavi pozadinu"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Nije moguće učitati sliku"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Nije moguće učitati sliku kao pozadinu"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Odabrano je %1$d stavki"</item>
+ <item quantity="one" msgid="8409622005831789373">"Odabrana je %1$d stavka"</item>
+ <item quantity="other" msgid="479468347731745357">"Odabrano stavki: %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%1$d. pozadinska slika od %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Odabrana je <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Izbriši"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Moje fotografije"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Pozadine"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Obrezivanje pozadinske slike"</string>
+</resources>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
new file mode 100644
index 0000000..d8b08fd
--- /dev/null
+++ b/res/values-hu/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Háttérkép beállítása"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"A kép betöltése nem sikerült"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"A kép betöltése háttérképként nem sikerült"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d kiválasztva"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d kiválasztva"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d kiválasztva"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%1$d/%2$d. háttérkép"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> kiválasztva"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Törlés"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Saját fotók"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Háttérképek"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Háttérkép körbevágása"</string>
+</resources>
diff --git a/res/values-hy-rAM/strings.xml b/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000..027b841
--- /dev/null
+++ b/res/values-hy-rAM/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Սահմանել պաստառը"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Չհաջողվեց բեռնել նկարը"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Չհաջողվեց նկարը սահմանել որպես պաստառ"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d ընտրված"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d ընտրված"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d ընտրված"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%1$d պաստառ՝ %2$d-ից"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Ընտրված է <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Ջնջել"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Իմ լուսանկարները"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Պաստառներ"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Եզրատել պաստառը"</string>
+</resources>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
new file mode 100644
index 0000000..9155452
--- /dev/null
+++ b/res/values-in/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Setel wallpaper"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Tidak dapat memuat gambar"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Tidak dapat memuat gambar sebagai wallpaper"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d dipilih"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d dipilih"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d dipilih"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Wallpaper %1$d dari %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> terpilih"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Hapus"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Foto saya"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Wallpaper"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Pangkas wallpaper"</string>
+</resources>
diff --git a/res/values-is-rIS/strings.xml b/res/values-is-rIS/strings.xml
new file mode 100644
index 0000000..69e6cd4
--- /dev/null
+++ b/res/values-is-rIS/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Velja veggfóður"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Ekki var hægt að hlaða mynd"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Ekki var hægt að hlaða mynd sem veggfóður"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d valin"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d valið"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d valin"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Veggfóður %1$d af %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> valið"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Eyða"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Myndirnar mínar"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Veggfóður"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Skera veggfóður"</string>
+</resources>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
new file mode 100644
index 0000000..f4d38d1
--- /dev/null
+++ b/res/values-it/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Imposta sfondo"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Impossibile caricare l\'immagine"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Impossibile caricare l\'immagine come sfondo"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d selezionati"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d selezionato"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d selezionati"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Sfondo %1$d di %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Elemento selezionato: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Elimina"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Le mie foto"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Sfondi"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Ritaglia sfondo"</string>
+</resources>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
new file mode 100644
index 0000000..e29f237
--- /dev/null
+++ b/res/values-iw/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"הגדר טפט"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"לא ניתן היה לטעון את התמונה"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"לא ניתן היה לטעון את התמונה כטפט"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"‏%1$d נבחרו"</item>
+ <item quantity="one" msgid="8409622005831789373">"‏%1$d נבחרו"</item>
+ <item quantity="other" msgid="479468347731745357">"‏%1$d נבחרו"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"‏טפט %1$d מתוך %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"בחרת <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"מחק"</string>
+ <string name="pick_image" msgid="3189640419551368385">"התמונות שלי"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"טפטים"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"חתוך את הטפט"</string>
+</resources>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
new file mode 100644
index 0000000..e86026b
--- /dev/null
+++ b/res/values-ja/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"壁紙を設定"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"画像を読み込めませんでした"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"画像を壁紙として読み込めませんでした"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d個選択済み"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d個選択済み"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d個選択済み"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"壁紙: %1$d/%2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"選択: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"削除"</string>
+ <string name="pick_image" msgid="3189640419551368385">"マイフォト"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"壁紙"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"壁紙のトリミング"</string>
+</resources>
diff --git a/res/values-ka-rGE/strings.xml b/res/values-ka-rGE/strings.xml
new file mode 100644
index 0000000..1ae1021
--- /dev/null
+++ b/res/values-ka-rGE/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"ფონის დაყენება"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"სურათი ვერ ჩაიტვირთა."</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"სურათი ფონად ვერ ჩაიტვირთა."</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"არჩეულია %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"არჩეულია %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"არჩეულია %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"ფონი %1$d %2$d-დან"</string>
+ <string name="announce_selection" msgid="123723511662250539">"არჩეული <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"წაშლა"</string>
+ <string name="pick_image" msgid="3189640419551368385">"ჩემი ფოტოები"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"ფონები"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"ფონის ჩამოჭრა"</string>
+</resources>
diff --git a/res/values-kk-rKZ/strings.xml b/res/values-kk-rKZ/strings.xml
new file mode 100644
index 0000000..a10e273
--- /dev/null
+++ b/res/values-kk-rKZ/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Тұсқағаз орнату"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Суретті жүктей алмады"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Суретті артқы фон ретінде жүктей алмады"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d таңдалған"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d таңдалған"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d таңдалған"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%1$d артқы фон, барлығы %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> таңдалған"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Жою"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Менің фотосуреттерім"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Артқы фондар"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Артқы фонды кесу"</string>
+</resources>
diff --git a/res/values-km-rKH/strings.xml b/res/values-km-rKH/strings.xml
new file mode 100644
index 0000000..2a605e8
--- /dev/null
+++ b/res/values-km-rKH/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"កំណត់​ផ្ទាំង​រូបភាព"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"មិន​អាច​ផ្ទុក​រូបភាព"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"មិន​អាច​ផ្ទុក​រូបភាព​ជា​ផ្ទាំង​រូបភាព"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"បាន​ជ្រើស %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"បាន​ជ្រើស %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"បាន​ជ្រើស %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"ផ្ទាំង​រូបភាព %1$d នៃ %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"បាន​ជ្រើស <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"លុប"</string>
+ <string name="pick_image" msgid="3189640419551368385">"រូបថតរបស់ខ្ញុំ"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"ផ្ទាំង​រូបភាព"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"ច្រឹប​ផ្ទាំង​រូបភាព"</string>
+</resources>
diff --git a/res/values-kn-rIN/strings.xml b/res/values-kn-rIN/strings.xml
new file mode 100644
index 0000000..b69bda5
--- /dev/null
+++ b/res/values-kn-rIN/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"ವಾಲ್‌ಪೇಪರ್ ಹೊಂದಿಸಿ"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"ಚಿತ್ರವನ್ನು ಲೋಡ್‌ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"ಚಿತ್ರವನ್ನು ವಾಲ್‌ಪೇಪರ್‌ ರೂಪದಲ್ಲಿ ಲೋಡ್‌ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d ಅನ್ನು ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d ಅನ್ನು ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d ಅನ್ನು ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%2$d ರಲ್ಲಿ %1$d ವಾಲ್‌ಪೇಪರ್‌"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"ಅಳಿಸು"</string>
+ <string name="pick_image" msgid="3189640419551368385">"ನನ್ನ ಫೋಟೋಗಳು"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"ವಾಲ್‌ಪೇಪರ್‌ಗಳು"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"ವಾಲ್‌ಪೇಪರ್‌ ಕತ್ತರಿಸಿ"</string>
+</resources>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
new file mode 100644
index 0000000..30e7cfa
--- /dev/null
+++ b/res/values-ko/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"배경화면 설정"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"이미지를 로드할 수 없습니다."</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"이미지를 배경화면으로 로드할 수 없습니다."</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d개 선택됨"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d개 선택됨"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d개 선택됨"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"배경화면 %1$d/%2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> 선택함"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"삭제"</string>
+ <string name="pick_image" msgid="3189640419551368385">"내 사진"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"배경화면"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"배경화면 잘라내기"</string>
+</resources>
diff --git a/res/values-ky-rKG/strings.xml b/res/values-ky-rKG/strings.xml
new file mode 100644
index 0000000..f1ebf09
--- /dev/null
+++ b/res/values-ky-rKG/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Тушкагаз орнотуу"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Сүрөт жүктөө мүмкүн болбоду"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Сүрөттү тушкагаз катары жүктөө кыйрады"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d тандалды"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d тандалды"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d тандалды"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%2$d ичинен %1$d тушкагаз"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> тандалды"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Жок кылуу"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Менин сүрөттөрүм"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Тушкагаздар"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Тушкагазды тегиздөө"</string>
+</resources>
diff --git a/res/values-lo-rLA/strings.xml b/res/values-lo-rLA/strings.xml
new file mode 100644
index 0000000..e32cafb
--- /dev/null
+++ b/res/values-lo-rLA/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"ຕັ້ງເປັນພາບພື້ນຫຼັງ"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"ບໍ່ສາມາດໂຫຼດຮູບໄດ້"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"ບໍ່ສາມາດໂຫຼດຮູບເປັນພາບພື້ນຫຼັງໄດ້"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"ເລືອກ %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"ເລືອກ %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"ເລືອກ %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"ພາບພື້ນຫຼັງ %1$d ໃນ %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"ເລືອກ <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"ລຶບ"</string>
+ <string name="pick_image" msgid="3189640419551368385">"ຮູບຂອງຂ້ອຍ"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"ພາບພື້ນຫຼັງ"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"ຕັດພາບພື້ນຫຼັງ"</string>
+</resources>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
new file mode 100644
index 0000000..c442e3a
--- /dev/null
+++ b/res/values-lt/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Nustatyti ekrano foną"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Nepavyko įkelti vaizdo"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Nepavyko įkelti vaizdo kaip ekrano fono"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Pasirinkta: %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Pasirinkta: %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Pasirinkta: %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%1$d ekrano fonas iš %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Pasirinkta: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Ištrinti"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Mano nuotraukos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Ekrano fonai"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Apkirpti ekrano foną"</string>
+</resources>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
new file mode 100644
index 0000000..d05a175
--- /dev/null
+++ b/res/values-lv/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Iestatīt fona tapeti"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Nevarēja ielādēt attēlu."</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Nevarēja ielādēt attēlu kā fona tapeti."</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Atlasīts: %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Atlasīta: %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Atlasītas: %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%1$d. fona tapete no %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Atlasīta: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Dzēst"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Mani fotoattēli"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Fona tapetes"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Apgriezt fona tapeti"</string>
+</resources>
diff --git a/res/values-mk-rMK/strings.xml b/res/values-mk-rMK/strings.xml
new file mode 100644
index 0000000..f9e0963
--- /dev/null
+++ b/res/values-mk-rMK/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Подеси тапет"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Сликата не можеше да се вчита"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Сликата не можеше да се вчита како тапет"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Избрано %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Избрано %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Избрано %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Тапет %1$d од %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Избран <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Избриши"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Моите фотографии"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Тапети"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Исечи тапет"</string>
+</resources>
diff --git a/res/values-ml-rIN/strings.xml b/res/values-ml-rIN/strings.xml
new file mode 100644
index 0000000..6be771c
--- /dev/null
+++ b/res/values-ml-rIN/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"വാൾപേപ്പർ സജ്ജീകരിക്കുക"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"ചിത്രം ലോഡുചെയ്യാനായില്ല"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"വാൾപേപ്പറായി ചിത്രം ലോഡുചെയ്യാനായില്ല"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d തിരഞ്ഞെടുത്തു"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d തിരഞ്ഞെടുത്തു"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d തിരഞ്ഞെടുത്തു"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%1$d / %2$d വാൾപേപ്പർ"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> തിരഞ്ഞെടുത്തു"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"ഇല്ലാതാക്കുക"</string>
+ <string name="pick_image" msgid="3189640419551368385">"എന്റെ ഫോട്ടോകൾ"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"വാൾപേപ്പറുകൾ"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"വാൾപേപ്പറിന്റെ വലുപ്പം മാറ്റൽ"</string>
+</resources>
diff --git a/res/values-mn-rMN/strings.xml b/res/values-mn-rMN/strings.xml
new file mode 100644
index 0000000..b88da5a
--- /dev/null
+++ b/res/values-mn-rMN/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Ханын зургийг тохируулах"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Зургийг ачаалж чадсангүй"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Зургийг ханын зураг болгож ачаалж чадсангүй"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d сонгогдсон"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d сонгогдсон"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d сонгогдсон"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%2$d ханын цаасны %1$d нь"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> сонгогдсон"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Устгах"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Миний зураг"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Ханын зураг"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Ханын зургийг тайрах"</string>
+</resources>
diff --git a/res/values-mr-rIN/strings.xml b/res/values-mr-rIN/strings.xml
new file mode 100644
index 0000000..128b21c
--- /dev/null
+++ b/res/values-mr-rIN/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"वॉलपेपर सेट करा"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"प्रतिमा लोड करू शकलो नाही"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"वॉलपेपर म्हणून प्रतिमा लोड करू शकलो नाही"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d निवडले"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d निवडले"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d निवडले"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%2$d पैकी %1$d वॉलपेपर"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> निवडले"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"हटवा"</string>
+ <string name="pick_image" msgid="3189640419551368385">"माझे फोटो"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"वॉलपेपर"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"वॉलपेपर कापा"</string>
+</resources>
diff --git a/res/values-ms-rMY/strings.xml b/res/values-ms-rMY/strings.xml
new file mode 100644
index 0000000..9d824e4
--- /dev/null
+++ b/res/values-ms-rMY/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Tetapkan kertas dinding"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Tidak dapat memuatkan imej"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Tidak dapat memuatkan imej sebagai kertas dinding"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d dipilih"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d dipilih"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d dipilih"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Kertas dinding %1$d daripada %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Memilih <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Padam"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Foto saya"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Kertas dinding"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Pangkas kertas dinding"</string>
+</resources>
diff --git a/res/values-my-rMM/strings.xml b/res/values-my-rMM/strings.xml
new file mode 100644
index 0000000..2336114
--- /dev/null
+++ b/res/values-my-rMM/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"နောက်ခံအား သတ်မှတ်ရန်"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"ပုံရိပ် တင် မရပါ"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"ပုံရိပ်အား နောက်ခံအဖြစ် တင် မရပါ"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d ရွေးချယ်ပြီး"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d ရွေးချယ်ပြီး"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d ရွေးချယ်ပြီး"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"နောက်ခံ %1$d မှ %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"ရွေးချယ်ထားသော <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"ဖျက်ပါ"</string>
+ <string name="pick_image" msgid="3189640419551368385">"ကျွန်ုပ်၏ ဓာတ်ပုံများ"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"နောက်ခံများ"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"နောက်ခံအား ဖြတ်ခြင်း"</string>
+</resources>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
new file mode 100644
index 0000000..9ae0b98
--- /dev/null
+++ b/res/values-nb/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Angi bakgrunn"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Kunne ikke laste inn bildet"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Kunne ikke laste inn bildet som bakgrunn"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d valgt"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d valgt"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d valgt"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Bakgrunn %1$d av %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Valgt <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Slett"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Mine bilder"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Bakgrunner"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Beskjær bakgrunnen"</string>
+</resources>
diff --git a/res/values-ne-rNP/strings.xml b/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000..5bca8d8
--- /dev/null
+++ b/res/values-ne-rNP/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"वालपेपर मिलाउनुहोस्"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"तस्बिर लोड गर्न सकिएन"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"तस्बिरलाई वालपेपरका रूपमा लोड गर्न सकिएन"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d चयन भयो"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d चयन भयो"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d चयन भयो"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%2$d को %1$d वालपेपर"</string>
+ <string name="announce_selection" msgid="123723511662250539">"चयन गरिएको <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"मेट्नुहोस्"</string>
+ <string name="pick_image" msgid="3189640419551368385">"मेरा तस्बिरहरू"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"वालपेपरहरु"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"वालपेपर काँटछाट गर्नुहोस्"</string>
+</resources>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
new file mode 100644
index 0000000..a86f0f6
--- /dev/null
+++ b/res/values-nl/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Achtergrond instellen"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Kan afbeelding niet laden"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Kan afbeelding niet laden als achtergrond"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d geselecteerd"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d geselecteerd"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d geselecteerd"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Achtergrond %1$d van %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> is geselecteerd"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Verwijderen"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Mijn foto\'s"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Achtergronden"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Achtergrond bijsnijden"</string>
+</resources>
diff --git a/res/values-nodpi/wallpapers.xml b/res/values-nodpi/wallpapers.xml
new file mode 100644
index 0000000..1e340e4
--- /dev/null
+++ b/res/values-nodpi/wallpapers.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<resources>
+ <string-array name="wallpapers" translatable="false">
+ </string-array>
+</resources>
diff --git a/res/values-pa-rIN/strings.xml b/res/values-pa-rIN/strings.xml
new file mode 100644
index 0000000..e4225e0
--- /dev/null
+++ b/res/values-pa-rIN/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"ਵਾਲਪੇਪਰ ਸੈਟ ਕਰੋ"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"ਚਿੱਤਰ ਲੋਡ ਨਹੀਂ ਕਰ ਸਕਿਆ"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"ਵਾਲਪੇਪਰ ਦੇ ਤੌਰ ਤੇ ਚਿੱਤਰ ਲੋਡ ਨਹੀਂ ਕਰ ਸਕਿਆ"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d ਚੁਣਿਆ ਗਿਆ"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d ਚੁਣਿਆ ਗਿਆ"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d ਚੁਣਿਆ ਗਿਆ"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"ਵਾਲਪੇਪਰ %2$d ਦਾ %1$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> ਚੁਣਿਆ ਗਿਆ"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"ਮਿਟਾਓ"</string>
+ <string name="pick_image" msgid="3189640419551368385">"ਮੇਰੀਆਂ ਫੋਟੋਆਂ"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"ਵਾਲਪੇਪਰ"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"ਵਾਲਪੇਪਰ ਕੱਟੋ"</string>
+</resources>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
new file mode 100644
index 0000000..cd52082
--- /dev/null
+++ b/res/values-pl/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Ustaw tapetę"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Nie udało się załadować obrazu"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Nie udało się załadować obrazu jako tapety"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Wybranych %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Wybrana %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Wybrane: %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Tapeta %1$d z %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Wybrano <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Usuń"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Moje zdjęcia"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Tapety"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Przytnij tapetę"</string>
+</resources>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..82aa469
--- /dev/null
+++ b/res/values-pt-rPT/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Definir imagem fundo"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Não foi possível carregar a imagem"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Não foi possível carregar a imagem como imagem de fundo"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d selecionadas"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d selecionada"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d selecionadas"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Imagem de fundo %1$d de %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> selecionada"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Eliminar"</string>
+ <string name="pick_image" msgid="3189640419551368385">"As minhas fotos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Imagens de fundo"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Recortar imagem de fundo"</string>
+</resources>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
new file mode 100644
index 0000000..74ff310
--- /dev/null
+++ b/res/values-pt/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Definir plano de fundo"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Não foi possível carregar a imagem"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Não foi possível carregar a imagem como plano de fundo"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d selecionados"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d selecionado"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d selecionados"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Plano de fundo %1$d de %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> selecionado"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Excluir"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Minhas fotos"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Planos de fundo"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Cortar plano de fundo"</string>
+</resources>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
new file mode 100644
index 0000000..6281b80
--- /dev/null
+++ b/res/values-ro/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Setați imaginea de fundal"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Nu s-a putut încărca imaginea"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Nu s-a putut încărca imaginea ca fundal"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d selectate"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d selectată"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d selectate"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Imaginea de fundal %1$d din %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"S-a selectat <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Ștergeți"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Fotografiile mele"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Imagini de fundal"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Decupați imaginea de fundal"</string>
+</resources>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
new file mode 100644
index 0000000..959205a
--- /dev/null
+++ b/res/values-ru/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Установить как обои"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Не удалось загрузить изображение"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Не удалось загрузить изображение"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Выбрано: %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Выбрано: %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Выбрано: %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Обои %1$d из %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Выбран элемент \"<xliff:g id="LABEL">%1$s</xliff:g>\""</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Удалить"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Мои фото"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Обои"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Кадрировать обои"</string>
+</resources>
diff --git a/res/values-si-rLK/strings.xml b/res/values-si-rLK/strings.xml
new file mode 100644
index 0000000..37ce1fc
--- /dev/null
+++ b/res/values-si-rLK/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"වෝල්පේපරය සකසන්න"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"පින්තූරය පූරණය කිරීමට නොහැකි විය"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"පින්තූරය වෝල්පේපරයක් ලෙස පූරණය කිරීමට නොහැකි විය"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d තෝරා ගන්නා ලදි"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d තෝරා ගන්නා ලදි"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d තෝරා ගන්නා ලදි"</item>
+ </plurals>
+ <!-- String.format failed for translation -->
+ <!-- no translation found for wallpaper_accessibility_name (4093221025304876354) -->
+ <skip />
+ <string name="announce_selection" msgid="123723511662250539">"තෝරාගත්තේ <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"මකන්න"</string>
+ <string name="pick_image" msgid="3189640419551368385">"මගේ ඡායාරූප"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"වෝල්පේපර"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"වෝල්පේපරය කප්පාදු කිරීම"</string>
+</resources>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
new file mode 100644
index 0000000..9d38de4
--- /dev/null
+++ b/res/values-sk/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Nastaviť tapetu"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Obrázok nie je možné načítať"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Obrázok nie je možné načítať ako tapetu"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Počet vybratých položiek: %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Počet vybratých položiek: %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Počet vybratých položiek: %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Tapeta %1$d z %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Vybratá položka <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Odstrániť"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Moje fotky"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Tapety"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Orezanie tapety"</string>
+</resources>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
new file mode 100644
index 0000000..e776cc7
--- /dev/null
+++ b/res/values-sl/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Nastavi ozadje"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Slike ni bilo mogoče naložiti"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Slike ni bilo mogoče naložiti kot ozadje"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Št. izbranih: %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Št. izbranih: %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Št. izbranih: %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%1$d. ozadje od %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Izbrano: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Izbriši"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Moje fotografije"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Ozadja"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Obrezovanje ozadja"</string>
+</resources>
diff --git a/res/values-sq-rAL/strings.xml b/res/values-sq-rAL/strings.xml
new file mode 100644
index 0000000..8a9983b
--- /dev/null
+++ b/res/values-sq-rAL/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Cakto imazhin e sfondit"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Nuk mund të ngarkonte imazhin"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Nuk mundi të ngarkonte imazhin si imazh sfondi"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Të përzgjedhur: %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Të përzgjedhur: %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Të përzgjedhur: %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Imazhi i sfondit: %1$d nga gjithsej %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> u përzgjodh"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Fshi"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Fotografitë e mia"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Imazhet e sfondit"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Prit imazhin e sfondit"</string>
+</resources>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
new file mode 100644
index 0000000..c37bc06
--- /dev/null
+++ b/res/values-sr/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Подеси позадину"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Није могуће учитати слику"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Није могуће учитати слику као позадину"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Изабрано је %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Изабрана је %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Изабраних: %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Позадина %1$d од %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Изабрана је <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Избриши"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Моје фотографије"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Позадине"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Опсеци позадину"</string>
+</resources>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
new file mode 100644
index 0000000..044508b
--- /dev/null
+++ b/res/values-sv/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Ange bakgrund"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Det gick inte att läsa in bilden"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Det gick inte att läsa in bilden som bakgrund"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d har valts"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d har valts"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d har valts"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Bakgrund %1$d av %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> har valts"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Ta bort"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Mina foton"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Bakgrunder"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Beskär bakgrund"</string>
+</resources>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
new file mode 100644
index 0000000..367912a
--- /dev/null
+++ b/res/values-sw/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Weka mandhari"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Haikuweza kupakia picha"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Haikuweza kupakia picha iwe mandhari"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d zimechaguliwa"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d zimechaguliwa"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d zimechaguliwa"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Mandhari %1$d ya %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> iliyochaguliwa"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Futa"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Picha zangu"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Mandhari"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Punguza mandhari"</string>
+</resources>
diff --git a/res/values-sw720dp-v19/styles.xml b/res/values-sw720dp-v19/styles.xml
new file mode 100644
index 0000000..d8dab22
--- /dev/null
+++ b/res/values-sw720dp-v19/styles.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* 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.
+*/
+-->
+
+<resources>
+ <style name="Theme" parent="@android:style/Theme.DeviceDefault.Wallpaper.NoTitleBar">
+ <item name="android:windowActionModeOverlay">true</item>
+ <item name="android:windowTranslucentStatus">true</item>
+ <item name="android:windowTranslucentNavigation">true</item>
+ </style>
+</resources>
diff --git a/res/values-sw720dp/styles.xml b/res/values-sw720dp/styles.xml
new file mode 100644
index 0000000..12f8884
--- /dev/null
+++ b/res/values-sw720dp/styles.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* 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.
+*/
+-->
+
+<resources>
+ <style name="Theme" parent="@android:style/Theme.DeviceDefault.Wallpaper.NoTitleBar">
+ <item name="android:windowActionModeOverlay">true</item>
+ </style>
+</resources>
diff --git a/res/values-ta-rIN/strings.xml b/res/values-ta-rIN/strings.xml
new file mode 100644
index 0000000..7284da8
--- /dev/null
+++ b/res/values-ta-rIN/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"வால்பேப்பரை அமை"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"படத்தை ஏற்ற முடியவில்லை"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"படத்தை வால்பேப்பராக ஏற்ற முடியவில்லை"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d தேர்ந்தெடுக்கப்பட்டன"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d தேர்ந்தெடுக்கப்பட்டது"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d தேர்ந்தெடுக்கப்பட்டன"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"வால்பேப்பர் %1$d / %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> தேர்ந்தெடுக்கப்பட்டது"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"நீக்கு"</string>
+ <string name="pick_image" msgid="3189640419551368385">"எனது படங்கள்"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"வால்பேப்பர்கள்"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"வால்பேப்பரைச் செதுக்கு"</string>
+</resources>
diff --git a/res/values-te-rIN/strings.xml b/res/values-te-rIN/strings.xml
new file mode 100644
index 0000000..4c5ad2f
--- /dev/null
+++ b/res/values-te-rIN/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"వాల్‌పేపర్‌ను సెట్ చేయి"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"చిత్రాన్ని లోడ్ చేయడం సాధ్యపడలేదు"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"చిత్రాన్ని వాల్‌పేపర్‌గా లోడ్ చేయడం సాధ్యపడలేదు"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d ఎంచుకోబడింది"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d ఎంచుకోబడింది"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d ఎంచుకోబడింది"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%2$dలో %1$dవ వాల్‌పేపర్"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> ఎంచుకోబడింది"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"తొలగించు"</string>
+ <string name="pick_image" msgid="3189640419551368385">"నా ఫోటోలు"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"వాల్‌పేపర్‌లు"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"వాల్‌పేపర్‌ను కత్తిరించండి"</string>
+</resources>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
new file mode 100644
index 0000000..b3d7a8b
--- /dev/null
+++ b/res/values-th/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"ตั้งวอลเปเปอร์"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"ไม่สามารถโหลดรูปภาพ"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"ไม่สามารถโหลดรูปภาพเป็นวอลเปเปอร์"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"เลือกไว้ %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"เลือกไว้ %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"เลือกไว้ %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"วอลเปเปอร์ %1$d จาก %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"เลือก <xliff:g id="LABEL">%1$s</xliff:g> แล้ว"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"ลบ"</string>
+ <string name="pick_image" msgid="3189640419551368385">"รูปภาพของฉัน"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"วอลเปเปอร์"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"ครอบตัดวอลเปเปอร์"</string>
+</resources>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
new file mode 100644
index 0000000..db8f4d5
--- /dev/null
+++ b/res/values-tl/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Itakda ang wallpaper"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Hindi ma-load ang larawan"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Hindi ma-load ang larawan bilang wallpaper"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d ang napili"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d ang napili"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d ang napili"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Wallpaper %1$d ng %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Napili ang <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Tanggalin"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Aking mga larawan"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Mga Wallpaper"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"I-crop ang wallpaper"</string>
+</resources>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
new file mode 100644
index 0000000..d1e57ed
--- /dev/null
+++ b/res/values-tr/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Duvar kağıdını ayarla"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Resim yüklenemedi"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Resim duvar kağıdı olarak yüklenemedi"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d tane seçildi"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d tane seçildi"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d tane seçildi"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"%2$d duvar kağıdı arasından duvar kağıdı %1$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> seçildi"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Sil"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Fotoğraflarım"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Duvar kağıtları"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Duvar kağıdını kırp"</string>
+</resources>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
new file mode 100644
index 0000000..756e7cf
--- /dev/null
+++ b/res/values-uk/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Установити фон"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Не вдалося завантажити зображення"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Не вдалося завантажити зображення як фоновий малюнок"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Вибрано %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Вибрано %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Вибрано %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Фоновий малюнок %1$d з %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"Вибрано <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Видалити"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Мої фото"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Фонові малюнки"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Обрізати фоновий малюнок"</string>
+</resources>
diff --git a/res/values-ur-rPK/strings.xml b/res/values-ur-rPK/strings.xml
new file mode 100644
index 0000000..86d8163
--- /dev/null
+++ b/res/values-ur-rPK/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"وال پیپر سیٹ کریں"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"تصویر کو لوڈ نہیں کیا جا سکا"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"تصویر کو وال پیپر کے بطور لوڈ نہیں کیا جا سکا"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"‏%1$d کو منتخب کیا گیا"</item>
+ <item quantity="one" msgid="8409622005831789373">"‏%1$d کو منتخب کیا گیا"</item>
+ <item quantity="other" msgid="479468347731745357">"‏%1$d کو منتخب کیا گیا"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"‏وال پیپر ‎%1$d از ‎%2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> کو منتخب کیا گیا"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"حذف کریں"</string>
+ <string name="pick_image" msgid="3189640419551368385">"میری تصاویر"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"وال پیپرز"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"وال پیپر کو تراشیں"</string>
+</resources>
diff --git a/res/values-uz-rUZ/strings.xml b/res/values-uz-rUZ/strings.xml
new file mode 100644
index 0000000..97d3938
--- /dev/null
+++ b/res/values-uz-rUZ/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Fonga rasm o‘rnatish"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Rasm yuklanmadi"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Fon rasmi sifatida rasm yuklanmadi"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d ta tanlandi"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d ta tanlandi"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d ta tanlandi"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Fon rasmi %2$ddan %1$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> tanlandi"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"O‘chirish"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Mening rasmlarim"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Fon rasmlari"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Fon rasmini kesish"</string>
+</resources>
diff --git a/res/values-v19/styles.xml b/res/values-v19/styles.xml
new file mode 100644
index 0000000..15fb0ea
--- /dev/null
+++ b/res/values-v19/styles.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* 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.
+*/
+-->
+
+<resources>
+ <style name="Theme.WallpaperCropper" parent="@android:style/Theme.DeviceDefault">
+ <item name="android:actionBarStyle">@style/WallpaperCropperActionBar</item>
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowActionBarOverlay">true</item>
+ <item name="android:windowTranslucentNavigation">true</item>
+ </style>
+
+ <style name="Theme" parent="@style/BaseWallpaperTheme">
+ <item name="android:windowTranslucentStatus">true</item>
+ <item name="android:windowTranslucentNavigation">true</item>
+ </style>
+</resources>
diff --git a/res/values-v21/styles.xml b/res/values-v21/styles.xml
new file mode 100644
index 0000000..70220ed
--- /dev/null
+++ b/res/values-v21/styles.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright (C) 2015 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="WallpaperCropperActionBar" parent="@android:style/Widget.DeviceDefault.ActionBar">
+ <item name="android:displayOptions">showCustom</item>
+ <item name="android:background">#88000000</item>
+ <item name="android:contentInsetEnd">0dp</item>
+ <item name="android:contentInsetLeft">0dp</item>
+ <item name="android:contentInsetRight">0dp</item>
+ <item name="android:contentInsetStart">0dp</item>
+ </style>
+
+ <style name="ActionBarSetWallpaperStyle" parent="@android:style/Widget.DeviceDefault.ActionButton">
+ <item name="android:textColor">#ffffffff</item>
+ <item name="android:background">?android:attr/selectableItemBackgroundBorderless</item>
+ </style>
+
+ <style name="Theme" parent="@style/BaseWallpaperTheme">
+ <item name="android:windowTranslucentStatus">true</item>
+ <item name="android:windowTranslucentNavigation">true</item>
+ <item name="android:colorControlActivated">@color/launcher_accent_color</item>
+ <item name="android:colorAccent">@color/launcher_accent_color</item>
+ <item name="android:colorPrimary">@color/launcher_accent_color</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
new file mode 100644
index 0000000..ae00d58
--- /dev/null
+++ b/res/values-vi/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Đặt hình nền"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Không thể tải hình ảnh"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Không thể tải hình ảnh làm hình nền"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"Đã chọn %1$d"</item>
+ <item quantity="one" msgid="8409622005831789373">"Đã chọn %1$d"</item>
+ <item quantity="other" msgid="479468347731745357">"Đã chọn %1$d"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Hình nền %1$d / %2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"<xliff:g id="LABEL">%1$s</xliff:g> được chọn"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Xóa"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Ảnh của tôi"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Hình nền"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Cắt hình nền"</string>
+</resources>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..8bd5342
--- /dev/null
+++ b/res/values-zh-rCN/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"设置壁纸"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"无法加载图片"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"无法加载要设为壁纸的图片"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"已选择%1$d项"</item>
+ <item quantity="one" msgid="8409622005831789373">"已选择%1$d项"</item>
+ <item quantity="other" msgid="479468347731745357">"已选择%1$d项"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"第%1$d张壁纸,共%2$d张"</string>
+ <string name="announce_selection" msgid="123723511662250539">"已选择<xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"删除"</string>
+ <string name="pick_image" msgid="3189640419551368385">"我的照片"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"壁纸"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"剪裁壁纸"</string>
+</resources>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..e51d60a
--- /dev/null
+++ b/res/values-zh-rHK/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"設定桌布"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"無法載入圖片"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"無法載入圖片設為桌布"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"已選取 %1$d 張"</item>
+ <item quantity="one" msgid="8409622005831789373">"已選取 %1$d 張"</item>
+ <item quantity="other" msgid="479468347731745357">"已選取 %1$d 張"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"第 %1$d 張桌布,共 %2$d 張"</string>
+ <string name="announce_selection" msgid="123723511662250539">"已選取<xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"刪除"</string>
+ <string name="pick_image" msgid="3189640419551368385">"我的相片"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"桌布"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"裁剪桌布"</string>
+</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..c12350a
--- /dev/null
+++ b/res/values-zh-rTW/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"設定桌布"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"無法載入圖片"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"無法載入您要設為桌布的圖片"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"已選取 %1$d 個"</item>
+ <item quantity="one" msgid="8409622005831789373">"已選取 %1$d 個"</item>
+ <item quantity="other" msgid="479468347731745357">"已選取 %1$d 個"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"第 %1$d 張桌布,共 %2$d 張"</string>
+ <string name="announce_selection" msgid="123723511662250539">"已選取<xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"刪除"</string>
+ <string name="pick_image" msgid="3189640419551368385">"我的相片"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"桌布"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"裁剪桌布"</string>
+</resources>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
new file mode 100644
index 0000000..537b2f3
--- /dev/null
+++ b/res/values-zu/strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+* 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.
+*/
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="wallpaper_instructions" msgid="3524143401182707094">"Setha isithombe sangemuva"</string>
+ <string name="image_load_fail" msgid="7538534580694411837">"Ayikwazanga ukulayisha isithombe"</string>
+ <string name="wallpaper_load_fail" msgid="4800700444605404650">"Ayikwazanga ukulayisha isithombe njengesithombe sangemuva"</string>
+ <plurals name="number_of_items_selected">
+ <item quantity="zero" msgid="9015111147509924344">"%1$d khethiwe"</item>
+ <item quantity="one" msgid="8409622005831789373">"%1$d khethiwe"</item>
+ <item quantity="other" msgid="479468347731745357">"%1$d khethiwe"</item>
+ </plurals>
+ <string name="wallpaper_accessibility_name" msgid="4093221025304876354">"Isithombe sangemuva esingu-%1$d kwezingu-%2$d"</string>
+ <string name="announce_selection" msgid="123723511662250539">"I-<xliff:g id="LABEL">%1$s</xliff:g> ekhethiwe"</string>
+ <string name="wallpaper_delete" msgid="1459353972739215344">"Susa"</string>
+ <string name="pick_image" msgid="3189640419551368385">"Izithombe zami"</string>
+ <string name="pick_wallpaper" msgid="4628969645948454559">"Izithombe zangemuva"</string>
+ <string name="crop_wallpaper" msgid="4882870800623585836">"Nqampuna isithombe sangemuva"</string>
+</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 0000000..6ba32f0
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* //device/apps/common/assets/res/any/colors.xml
+**
+** Copyright 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.
+*/
+-->
+<resources>
+ <color name="wallpaper_picker_translucent_gray">#66000000</color>
+
+ <color name="launcher_accent_color">#ff009688</color>
+</resources>
diff --git a/res/values/config.xml b/res/values/config.xml
new file mode 100644
index 0000000..2f5174c
--- /dev/null
+++ b/res/values/config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+ <!-- Specifies whether to expand the cropped area on both sides (rather
+ than just to one side) -->
+ <bool name="center_crop">false</bool>
+</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
new file mode 100644
index 0000000..0447c6d
--- /dev/null
+++ b/res/values/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources>
+<!-- Wallpaper picker -->
+ <dimen name="wallpaperThumbnailWidth">106.5dp</dimen>
+ <dimen name="wallpaperThumbnailHeight">94.5dp</dimen>
+ <dimen name="wallpaperItemIconSize">32dp</dimen>
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..2b8208a
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* 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.
+*/
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Application name -->
+ <string name="app_name">Wallpaper Picker</string>
+
+ <!-- Button label on Wallpaper picker screen; user selects this button to set a specific wallpaper -->
+ <string name="wallpaper_instructions">Set wallpaper</string>
+ <!-- Error message when an image is selected as a wallpaper,
+ but the wallpaper picker cannot load it -->
+ <string name="image_load_fail">Coudn\'t load image</string>
+ <!-- Error message when an image is selected as a wallpaper,
+ but the wallpaper cropper cannot load it. The user will
+ usually see this when using another app and trying to set
+ an image as the wallpaper -->
+ <string name="wallpaper_load_fail">Couldn\'t load image as wallpaper</string>
+ <!-- Shown when wallpapers are selected in Wallpaper picker -->
+ <!-- String indicating how many media item(s) is(are) selected
+ eg. 1 selected [CHAR LIMIT=30] -->
+ <plurals name="number_of_items_selected">
+ <item quantity="zero">%1$d selected</item>
+ <item quantity="one">%1$d selected</item>
+ <item quantity="other">%1$d selected</item>
+ </plurals>
+ <!-- Accessibility string used as a label for a particular wallpaper in the Wallpaper Picker list.
+ e.g. "Wallpaper 3 of 10" -->
+ <string name="wallpaper_accessibility_name">Wallpaper %1$d of %2$d</string>
+ <!-- Accessibility string used to announce that a wallpaper has been selected. -->
+ <string name="announce_selection">Selected <xliff:g id="label" example="Wallpaper 3 of 10">%1$s</xliff:g></string>
+
+ <!-- Label on button to delete wallpaper(s) -->
+ <string name="wallpaper_delete">Delete</string>
+ <!-- Label on button in Wallpaper Picker to pick an image -->
+ <string name="pick_image">My photos</string>
+ <!-- Option in "Select wallpaper from" dialog box -->
+ <string name="pick_wallpaper">Wallpapers</string>
+ <!-- Title of activity for cropping wallpapers -->
+ <string name="crop_wallpaper">Crop wallpaper</string>
+
+ <!-- Title of toast shown when external storage permission is not granted -->
+ <string name="storage_permission_denied">Can\'t access storage</string>
+
+ <!-- Displayed when user selects a shortcut for an app that was uninstalled [CHAR_LIMIT=none]-->
+ <string name="activity_not_found">App isn\'t installed.</string>
+
+ <string name="no_wallpaper">No wallpaper</string>
+</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
new file mode 100644
index 0000000..86b68f7
--- /dev/null
+++ b/res/values/styles.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* 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.
+*/
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <style name="Theme.WallpaperCropper" parent="@android:style/Theme.DeviceDefault">
+ <item name="android:actionBarStyle">@style/WallpaperCropperActionBar</item>
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowActionBarOverlay">true</item>
+ </style>
+
+ <style name="Theme.WallpaperPicker" parent="@style/Theme.WallpaperCropper">
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:colorBackgroundCacheHint">@null</item>
+ <item name="android:windowShowWallpaper">true</item>
+ </style>
+
+ <style name="WallpaperCropperActionBar" parent="@android:style/Widget.DeviceDefault.ActionBar">
+ <item name="android:displayOptions">showCustom</item>
+ <item name="android:background">#88000000</item>
+ </style>
+
+ <style name="BaseWallpaperTheme" parent="@android:style/Theme.DeviceDefault.Light.NoActionBar">
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:colorBackgroundCacheHint">@null</item>
+ <item name="android:windowShowWallpaper">true</item>
+ <item name="android:windowNoTitle">true</item>
+ </style>
+
+ <style name="Theme" parent="@style/BaseWallpaperTheme"></style>
+
+ <style name="ActionBarSetWallpaperStyle" parent="@android:style/Widget.DeviceDefault.ActionButton">
+ <item name="android:textColor">#ffffffff</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ </style>
+</resources>
diff --git a/src/com/android/gallery3d/common/BitmapCropTask.java b/src/com/android/gallery3d/common/BitmapCropTask.java
new file mode 100644
index 0000000..9d3241b
--- /dev/null
+++ b/src/com/android/gallery3d/common/BitmapCropTask.java
@@ -0,0 +1,445 @@
+/**
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.gallery3d.common;
+
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+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.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
+
+ public interface OnBitmapCroppedHandler {
+ public void onBitmapCropped(byte[] imageBytes);
+ }
+
+ private static final int DEFAULT_COMPRESS_QUALITY = 90;
+ private static final String LOGTAG = "BitmapCropTask";
+
+ Uri mInUri = null;
+ Context mContext;
+ String mInFilePath;
+ byte[] mInImageBytes;
+ int mInResId = 0;
+ RectF mCropBounds = null;
+ int mOutWidth, mOutHeight;
+ int mRotation;
+ boolean mSetWallpaper;
+ boolean mSaveCroppedBitmap;
+ Bitmap mCroppedBitmap;
+ Runnable mOnEndRunnable;
+ Resources mResources;
+ BitmapCropTask.OnBitmapCroppedHandler mOnBitmapCroppedHandler;
+ boolean mNoCrop;
+ boolean mImageFromAsset;
+ boolean mIslockScreenPicker;
+
+ public BitmapCropTask(Context c, String filePath,
+ RectF cropBounds, int rotation, int outWidth, int outHeight,
+ boolean setWallpaper, boolean isLockScreenPicker, boolean saveCroppedBitmap,
+ Runnable onEndRunnable) {
+ mContext = c;
+ mInFilePath = filePath;
+ init(cropBounds, rotation, outWidth, outHeight, setWallpaper, isLockScreenPicker,
+ saveCroppedBitmap, onEndRunnable);
+ }
+
+ public BitmapCropTask(byte[] imageBytes,
+ RectF cropBounds, int rotation, int outWidth, int outHeight,
+ boolean setWallpaper, boolean isLockScreenPicker, boolean saveCroppedBitmap,
+ Runnable onEndRunnable) {
+ mInImageBytes = imageBytes;
+ init(cropBounds, rotation, outWidth, outHeight, setWallpaper, isLockScreenPicker,
+ saveCroppedBitmap, onEndRunnable);
+ }
+
+ public BitmapCropTask(Context c, Uri inUri,
+ RectF cropBounds, int rotation, int outWidth, int outHeight,
+ boolean setWallpaper, boolean isLockScreenPicker, boolean saveCroppedBitmap,
+ Runnable onEndRunnable) {
+ mContext = c;
+ mInUri = inUri;
+ init(cropBounds, rotation, outWidth, outHeight, setWallpaper, isLockScreenPicker,
+ saveCroppedBitmap, onEndRunnable);
+ }
+
+ public BitmapCropTask(Context c, Resources res, int inResId,
+ RectF cropBounds, int rotation, int outWidth, int outHeight,
+ boolean setWallpaper, boolean isLockScreenPicker, boolean saveCroppedBitmap,
+ Runnable onEndRunnable) {
+ mContext = c;
+ mInResId = inResId;
+ mResources = res;
+ init(cropBounds, rotation,
+ outWidth, outHeight, setWallpaper, isLockScreenPicker, saveCroppedBitmap, onEndRunnable);
+ }
+
+ public BitmapCropTask(Context c, Resources res , String assetPath,
+ RectF cropBounds, int rotation, int outWidth, int outHeight,
+ boolean setWallpaper, boolean isLockScreenPicker, boolean saveCroppedBitmap,
+ Runnable onEndRunnable) {
+ mContext = c;
+ mResources = res;
+ mInFilePath = assetPath;
+ mImageFromAsset = true;
+ init(cropBounds, rotation, outWidth, outHeight, setWallpaper, isLockScreenPicker,
+ saveCroppedBitmap, onEndRunnable);
+ }
+
+ private void init(RectF cropBounds, int rotation, int outWidth, int outHeight,
+ boolean setWallpaper, boolean isLockScreenPicker, boolean saveCroppedBitmap,
+ Runnable onEndRunnable) {
+ mCropBounds = cropBounds;
+ mRotation = rotation;
+ mOutWidth = outWidth;
+ mOutHeight = outHeight;
+ mSetWallpaper = setWallpaper;
+ mSaveCroppedBitmap = saveCroppedBitmap;
+ mOnEndRunnable = onEndRunnable;
+ mIslockScreenPicker = isLockScreenPicker;
+ }
+
+ public void setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler) {
+ mOnBitmapCroppedHandler = handler;
+ }
+
+ public void setNoCrop(boolean value) {
+ mNoCrop = value;
+ }
+
+ public void setOnEndRunnable(Runnable onEndRunnable) {
+ mOnEndRunnable = onEndRunnable;
+ }
+
+ // Helper to setup input stream
+ private InputStream regenerateInputStream() {
+ if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null
+ && !mImageFromAsset) {
+ Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " +
+ "image byte array given");
+ } else {
+ try {
+ if (mImageFromAsset) {
+ AssetManager am = mResources.getAssets();
+ String[] pathImages = am.list(mInFilePath);
+ if (pathImages == null || pathImages.length == 0) {
+ throw new IOException("did not find any images in path: " + mInFilePath);
+ }
+ InputStream is = am.open(mInFilePath + File.separator + pathImages[0]);
+ return new BufferedInputStream(is);
+ } else if (mInUri != null) {
+ return new BufferedInputStream(
+ mContext.getContentResolver().openInputStream(mInUri));
+ } else if (mInFilePath != null) {
+ return mContext.openFileInput(mInFilePath);
+ } else if (mInImageBytes != null) {
+ return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes));
+ } else {
+ return new BufferedInputStream(mResources.openRawResource(mInResId));
+ }
+ } catch (IOException e) {
+ Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
+ }
+ }
+ return null;
+ }
+
+ public Point getImageBounds() {
+ InputStream is = regenerateInputStream();
+ if (is != null) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(is, null, options);
+ Utils.closeSilently(is);
+ if (options.outWidth != 0 && options.outHeight != 0) {
+ return new Point(options.outWidth, options.outHeight);
+ }
+ }
+ return null;
+ }
+
+ public void setCropBounds(RectF cropBounds) {
+ mCropBounds = cropBounds;
+ }
+
+ public Bitmap getCroppedBitmap() {
+ return mCroppedBitmap;
+ }
+ public boolean cropBitmap() {
+ boolean failure = false;
+
+
+ WallpaperManager wallpaperManager = null;
+ if (mSetWallpaper) {
+ wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext());
+ }
+
+
+ if (mSetWallpaper && (mNoCrop || mIslockScreenPicker)) {
+ try {
+ InputStream is = regenerateInputStream();
+ if (is != null) {
+ if (!mIslockScreenPicker) {
+ wallpaperManager.setStream(is);
+ } else {
+ wallpaperManager.setKeyguardStream(is);
+ }
+ Utils.closeSilently(is);
+ }
+ } catch (IOException e) {
+ Log.w(LOGTAG, "cannot write stream to wallpaper", e);
+ failure = true;
+ }
+ return !failure;
+ } else {
+ // Find crop bounds (scaled to original image size)
+ Rect roundedTrueCrop = new Rect();
+ Matrix rotateMatrix = new Matrix();
+ Matrix inverseRotateMatrix = new Matrix();
+
+ Point bounds = getImageBounds();
+ if (mRotation > 0) {
+ rotateMatrix.setRotate(mRotation);
+ inverseRotateMatrix.setRotate(-mRotation);
+
+ mCropBounds.roundOut(roundedTrueCrop);
+ mCropBounds = new RectF(roundedTrueCrop);
+
+ if (bounds == null) {
+ Log.w(LOGTAG, "cannot get bounds for image");
+ failure = true;
+ return false;
+ }
+
+ float[] rotatedBounds = new float[] { bounds.x, bounds.y };
+ rotateMatrix.mapPoints(rotatedBounds);
+ rotatedBounds[0] = Math.abs(rotatedBounds[0]);
+ rotatedBounds[1] = Math.abs(rotatedBounds[1]);
+
+ mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2);
+ inverseRotateMatrix.mapRect(mCropBounds);
+ mCropBounds.offset(bounds.x/2, bounds.y/2);
+
+ }
+
+ mCropBounds.roundOut(roundedTrueCrop);
+
+ if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
+ Log.w(LOGTAG, "crop has bad values for full size image");
+ failure = true;
+ return false;
+ }
+
+ // See how much we're reducing the size of the image
+ int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth,
+ roundedTrueCrop.height() / mOutHeight));
+ // Attempt to open a region decoder
+ BitmapRegionDecoder decoder = null;
+ InputStream is = null;
+ try {
+ is = regenerateInputStream();
+ if (is == null) {
+ Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString());
+ failure = true;
+ return false;
+ }
+ decoder = BitmapRegionDecoder.newInstance(is, false);
+ Utils.closeSilently(is);
+ } catch (IOException e) {
+ Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
+ } finally {
+ Utils.closeSilently(is);
+ is = null;
+ }
+
+ Bitmap crop = null;
+ if (decoder != null) {
+ // Do region decoding to get crop bitmap
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ if (scaleDownSampleSize > 1) {
+ options.inSampleSize = scaleDownSampleSize;
+ }
+ crop = decoder.decodeRegion(roundedTrueCrop, options);
+ decoder.recycle();
+ }
+
+ if (crop == null) {
+ // BitmapRegionDecoder has failed, try to crop in-memory
+ is = regenerateInputStream();
+ Bitmap fullSize = null;
+ if (is != null) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ if (scaleDownSampleSize > 1) {
+ options.inSampleSize = scaleDownSampleSize;
+ }
+ fullSize = BitmapFactory.decodeStream(is, null, options);
+ Utils.closeSilently(is);
+ }
+ if (fullSize != null) {
+ // Find out the true sample size that was used by the decoder
+ scaleDownSampleSize = bounds.x / fullSize.getWidth();
+ mCropBounds.left /= scaleDownSampleSize;
+ mCropBounds.top /= scaleDownSampleSize;
+ mCropBounds.bottom /= scaleDownSampleSize;
+ mCropBounds.right /= scaleDownSampleSize;
+ mCropBounds.roundOut(roundedTrueCrop);
+
+ // Adjust values to account for issues related to rounding
+ if (roundedTrueCrop.width() > fullSize.getWidth()) {
+ // Adjust the width
+ roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth();
+ }
+ if (roundedTrueCrop.right > fullSize.getWidth()) {
+ // Adjust the left value
+ int adjustment = roundedTrueCrop.left -
+ Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width());
+ roundedTrueCrop.left -= adjustment;
+ roundedTrueCrop.right -= adjustment;
+ }
+ if (roundedTrueCrop.height() > fullSize.getHeight()) {
+ // Adjust the height
+ roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight();
+ }
+ if (roundedTrueCrop.bottom > fullSize.getHeight()) {
+ // Adjust the top value
+ int adjustment = roundedTrueCrop.top -
+ Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height());
+ roundedTrueCrop.top -= adjustment;
+ roundedTrueCrop.bottom -= adjustment;
+ }
+
+ 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 (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) {
+ float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() };
+ rotateMatrix.mapPoints(dimsAfter);
+ dimsAfter[0] = Math.abs(dimsAfter[0]);
+ dimsAfter[1] = Math.abs(dimsAfter[1]);
+
+ if (!(mOutWidth > 0 && mOutHeight > 0)) {
+ mOutWidth = Math.round(dimsAfter[0]);
+ mOutHeight = Math.round(dimsAfter[1]);
+ }
+
+ RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]);
+ RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight);
+
+ Matrix m = new Matrix();
+ if (mRotation == 0) {
+ m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
+ } else {
+ Matrix m1 = new Matrix();
+ m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f);
+ Matrix m2 = new Matrix();
+ m2.setRotate(mRotation);
+ Matrix m3 = new Matrix();
+ m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f);
+ Matrix m4 = new Matrix();
+ m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
+
+ Matrix c1 = new Matrix();
+ c1.setConcat(m2, m1);
+ Matrix c2 = new Matrix();
+ c2.setConcat(m4, m3);
+ m.setConcat(c2, c1);
+ }
+
+ Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
+ (int) returnRect.height(), Bitmap.Config.ARGB_8888);
+ if (tmp != null) {
+ Canvas c = new Canvas(tmp);
+ Paint p = new Paint();
+ p.setFilterBitmap(true);
+ c.drawBitmap(crop, m, p);
+ crop = tmp;
+ }
+ }
+
+ if (mSaveCroppedBitmap) {
+ mCroppedBitmap = crop;
+ }
+
+ // Compress to byte array
+ ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
+ if (crop.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
+ // If we need to set to the wallpaper, set it
+ if (mSetWallpaper && wallpaperManager != null) {
+ try {
+ byte[] outByteArray = tmpOut.toByteArray();
+ InputStream stream = new ByteArrayInputStream(outByteArray);
+ if (!mIslockScreenPicker) {
+ wallpaperManager.setStream(stream);
+ } else {
+ wallpaperManager.setKeyguardStream(stream);
+ }
+ if (mOnBitmapCroppedHandler != null) {
+ mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
+ }
+ } 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 Boolean doInBackground(Void... params) {
+ return cropBitmap();
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (mOnEndRunnable != null) {
+ mOnEndRunnable.run();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/common/BitmapUtils.java b/src/com/android/gallery3d/common/BitmapUtils.java
new file mode 100644
index 0000000..9ac5c1b
--- /dev/null
+++ b/src/com/android/gallery3d/common/BitmapUtils.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.common;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.util.Log;
+
+import com.android.gallery3d.exif.ExifInterface;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class BitmapUtils {
+
+ private static final String TAG = "BitmapUtils";
+
+ // Find the min x that 1 / x >= scale
+ public static int computeSampleSizeLarger(float scale) {
+ int initialSize = (int) Math.floor(1f / scale);
+ if (initialSize <= 1) return 1;
+
+ return initialSize <= 8
+ ? Utils.prevPowerOf2(initialSize)
+ : initialSize / 8 * 8;
+ }
+
+ public static int getRotationFromExif(Context context, Uri uri) {
+ return BitmapUtils.getRotationFromExifHelper(null, 0, context, uri);
+ }
+
+ public static int getRotationFromExif(Resources res, int resId) {
+ return BitmapUtils.getRotationFromExifHelper(res, resId, null, null);
+ }
+
+ private static int getRotationFromExifHelper(Resources res, int resId, Context context, Uri uri) {
+ ExifInterface ei = new ExifInterface();
+ InputStream is = null;
+ BufferedInputStream bis = null;
+ try {
+ if (uri != null) {
+ is = context.getContentResolver().openInputStream(uri);
+ bis = new BufferedInputStream(is);
+ ei.readExif(bis);
+ } else {
+ is = res.openRawResource(resId);
+ bis = new BufferedInputStream(is);
+ ei.readExif(bis);
+ }
+ Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+ if (ori != null) {
+ return ExifInterface.getRotationForOrientationValue(ori.shortValue());
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Getting exif data failed", e);
+ } catch (NullPointerException e) {
+ // Sometimes the ExifInterface has an internal NPE if Exif data isn't valid
+ Log.w(TAG, "Getting exif data failed", e);
+ } finally {
+ Utils.closeSilently(bis);
+ Utils.closeSilently(is);
+ }
+ return 0;
+ }
+}
diff --git a/src/com/android/gallery3d/common/Utils.java b/src/com/android/gallery3d/common/Utils.java
new file mode 100644
index 0000000..8466c22
--- /dev/null
+++ b/src/com/android/gallery3d/common/Utils.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.common;
+
+import android.database.Cursor;
+import android.graphics.RectF;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+public class Utils {
+ private static final String TAG = "Utils";
+
+ // Throws AssertionError if the input is false.
+ public static void assertTrue(boolean cond) {
+ if (!cond) {
+ throw new AssertionError();
+ }
+ }
+
+ // Returns the next power of two.
+ // Returns the input if it is already power of 2.
+ // Throws IllegalArgumentException if the input is <= 0 or
+ // the answer overflows.
+ public static int nextPowerOf2(int n) {
+ if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException("n is invalid: " + n);
+ n -= 1;
+ n |= n >> 16;
+ n |= n >> 8;
+ n |= n >> 4;
+ n |= n >> 2;
+ n |= n >> 1;
+ return n + 1;
+ }
+
+ // Returns the previous power of two.
+ // Returns the input if it is already power of 2.
+ // Throws IllegalArgumentException if the input is <= 0
+ public static int prevPowerOf2(int n) {
+ if (n <= 0) throw new IllegalArgumentException();
+ return Integer.highestOneBit(n);
+ }
+
+ // Returns the input value x clamped to the range [min, max].
+ 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 ceilLog2(float value) {
+ int i;
+ for (i = 0; i < 31; i++) {
+ if ((1 << i) >= value) break;
+ }
+ return i;
+ }
+
+ public static int floorLog2(float value) {
+ int i;
+ for (i = 0; i < 31; i++) {
+ if ((1 << i) > value) break;
+ }
+ return i - 1;
+ }
+
+ public static void closeSilently(Closeable c) {
+ if (c == null) return;
+ try {
+ c.close();
+ } catch (IOException t) {
+ Log.w(TAG, "close fail ", t);
+ }
+ }
+
+ public static void closeSilently(ParcelFileDescriptor fd) {
+ try {
+ if (fd != null) fd.close();
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to close", t);
+ }
+ }
+
+ public static void closeSilently(Cursor cursor) {
+ try {
+ if (cursor != null) cursor.close();
+ } catch (Throwable t) {
+ Log.w(TAG, "fail to close", t);
+ }
+ }
+
+ public static RectF getMaxCropRect(
+ int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) {
+ RectF cropRect = new RectF();
+ // Get a crop rect that will fit this
+ if (inWidth / (float) inHeight > outWidth / (float) outHeight) {
+ cropRect.top = 0;
+ cropRect.bottom = inHeight;
+ cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2;
+ cropRect.right = inWidth - cropRect.left;
+ if (leftAligned) {
+ cropRect.right -= cropRect.left;
+ cropRect.left = 0;
+ }
+ } else {
+ cropRect.left = 0;
+ cropRect.right = inWidth;
+ cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2;
+ cropRect.bottom = inHeight - cropRect.top;
+ }
+ return cropRect;
+ }
+}
diff --git a/src/com/android/gallery3d/exif/ByteBufferInputStream.java b/src/com/android/gallery3d/exif/ByteBufferInputStream.java
new file mode 100644
index 0000000..7fb9f22
--- /dev/null
+++ b/src/com/android/gallery3d/exif/ByteBufferInputStream.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.exif;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+class ByteBufferInputStream extends InputStream {
+
+ private ByteBuffer mBuf;
+
+ public ByteBufferInputStream(ByteBuffer buf) {
+ mBuf = buf;
+ }
+
+ @Override
+ public int read() {
+ if (!mBuf.hasRemaining()) {
+ return -1;
+ }
+ return mBuf.get() & 0xFF;
+ }
+
+ @Override
+ public int read(byte[] bytes, int off, int len) {
+ if (!mBuf.hasRemaining()) {
+ return -1;
+ }
+
+ len = Math.min(len, mBuf.remaining());
+ mBuf.get(bytes, off, len);
+ return len;
+ }
+}
diff --git a/src/com/android/gallery3d/exif/CountedDataInputStream.java b/src/com/android/gallery3d/exif/CountedDataInputStream.java
new file mode 100644
index 0000000..dfd4a1a
--- /dev/null
+++ b/src/com/android/gallery3d/exif/CountedDataInputStream.java
@@ -0,0 +1,136 @@
+/*
+ * 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.exif;
+
+import java.io.EOFException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+
+class CountedDataInputStream extends FilterInputStream {
+
+ private int mCount = 0;
+
+ // allocate a byte buffer for a long value;
+ private final byte mByteArray[] = new byte[8];
+ private final ByteBuffer mByteBuffer = ByteBuffer.wrap(mByteArray);
+
+ protected CountedDataInputStream(InputStream in) {
+ super(in);
+ }
+
+ public int getReadByteCount() {
+ return mCount;
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ int r = in.read(b);
+ mCount += (r >= 0) ? r : 0;
+ return r;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ int r = in.read(b, off, len);
+ mCount += (r >= 0) ? r : 0;
+ return r;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int r = in.read();
+ mCount += (r >= 0) ? 1 : 0;
+ return r;
+ }
+
+ @Override
+ public long skip(long length) throws IOException {
+ long skip = in.skip(length);
+ mCount += skip;
+ return skip;
+ }
+
+ public void skipOrThrow(long length) throws IOException {
+ if (skip(length) != length) throw new EOFException();
+ }
+
+ public void skipTo(long target) throws IOException {
+ long cur = mCount;
+ long diff = target - cur;
+ assert(diff >= 0);
+ skipOrThrow(diff);
+ }
+
+ public void readOrThrow(byte[] b, int off, int len) throws IOException {
+ int r = read(b, off, len);
+ if (r != len) throw new EOFException();
+ }
+
+ public void readOrThrow(byte[] b) throws IOException {
+ readOrThrow(b, 0, b.length);
+ }
+
+ public void setByteOrder(ByteOrder order) {
+ mByteBuffer.order(order);
+ }
+
+ public ByteOrder getByteOrder() {
+ return mByteBuffer.order();
+ }
+
+ public short readShort() throws IOException {
+ readOrThrow(mByteArray, 0 ,2);
+ mByteBuffer.rewind();
+ return mByteBuffer.getShort();
+ }
+
+ public int readUnsignedShort() throws IOException {
+ return readShort() & 0xffff;
+ }
+
+ public int readInt() throws IOException {
+ readOrThrow(mByteArray, 0 , 4);
+ mByteBuffer.rewind();
+ return mByteBuffer.getInt();
+ }
+
+ public long readUnsignedInt() throws IOException {
+ return readInt() & 0xffffffffL;
+ }
+
+ public long readLong() throws IOException {
+ readOrThrow(mByteArray, 0 , 8);
+ mByteBuffer.rewind();
+ return mByteBuffer.getLong();
+ }
+
+ public String readString(int n) throws IOException {
+ byte buf[] = new byte[n];
+ readOrThrow(buf);
+ return new String(buf, "UTF8");
+ }
+
+ public String readString(int n, Charset charset) throws IOException {
+ byte buf[] = new byte[n];
+ readOrThrow(buf);
+ return new String(buf, charset);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/exif/ExifData.java b/src/com/android/gallery3d/exif/ExifData.java
new file mode 100644
index 0000000..8422382
--- /dev/null
+++ b/src/com/android/gallery3d/exif/ExifData.java
@@ -0,0 +1,348 @@
+/*
+ * 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.exif;
+
+import android.util.Log;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This class stores the EXIF header in IFDs according to the JPEG
+ * specification. It is the result produced by {@link ExifReader}.
+ *
+ * @see ExifReader
+ * @see IfdData
+ */
+class ExifData {
+ private static final String TAG = "ExifData";
+ private static final byte[] USER_COMMENT_ASCII = {
+ 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00
+ };
+ private static final byte[] USER_COMMENT_JIS = {
+ 0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00
+ };
+ private static final byte[] USER_COMMENT_UNICODE = {
+ 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00
+ };
+
+ private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT];
+ private byte[] mThumbnail;
+ private ArrayList<byte[]> mStripBytes = new ArrayList<byte[]>();
+ private final ByteOrder mByteOrder;
+
+ ExifData(ByteOrder order) {
+ mByteOrder = order;
+ }
+
+ /**
+ * Gets the compressed thumbnail. Returns null if there is no compressed
+ * thumbnail.
+ *
+ * @see #hasCompressedThumbnail()
+ */
+ protected byte[] getCompressedThumbnail() {
+ return mThumbnail;
+ }
+
+ /**
+ * Sets the compressed thumbnail.
+ */
+ protected void setCompressedThumbnail(byte[] thumbnail) {
+ mThumbnail = thumbnail;
+ }
+
+ /**
+ * Returns true it this header contains a compressed thumbnail.
+ */
+ protected boolean hasCompressedThumbnail() {
+ return mThumbnail != null;
+ }
+
+ /**
+ * Adds an uncompressed strip.
+ */
+ protected void setStripBytes(int index, byte[] strip) {
+ if (index < mStripBytes.size()) {
+ mStripBytes.set(index, strip);
+ } else {
+ for (int i = mStripBytes.size(); i < index; i++) {
+ mStripBytes.add(null);
+ }
+ mStripBytes.add(strip);
+ }
+ }
+
+ /**
+ * Gets the strip count.
+ */
+ protected int getStripCount() {
+ return mStripBytes.size();
+ }
+
+ /**
+ * Gets the strip at the specified index.
+ *
+ * @exceptions #IndexOutOfBoundException
+ */
+ protected byte[] getStrip(int index) {
+ return mStripBytes.get(index);
+ }
+
+ /**
+ * Returns true if this header contains uncompressed strip.
+ */
+ protected boolean hasUncompressedStrip() {
+ return mStripBytes.size() != 0;
+ }
+
+ /**
+ * Gets the byte order.
+ */
+ protected ByteOrder getByteOrder() {
+ return mByteOrder;
+ }
+
+ /**
+ * Returns the {@link IfdData} object corresponding to a given IFD if it
+ * exists or null.
+ */
+ protected IfdData getIfdData(int ifdId) {
+ if (ExifTag.isValidIfd(ifdId)) {
+ return mIfdDatas[ifdId];
+ }
+ return null;
+ }
+
+ /**
+ * Adds IFD data. If IFD data of the same type already exists, it will be
+ * replaced by the new data.
+ */
+ protected void addIfdData(IfdData data) {
+ mIfdDatas[data.getId()] = data;
+ }
+
+ /**
+ * Returns the {@link IfdData} object corresponding to a given IFD or
+ * generates one if none exist.
+ */
+ protected IfdData getOrCreateIfdData(int ifdId) {
+ IfdData ifdData = mIfdDatas[ifdId];
+ if (ifdData == null) {
+ ifdData = new IfdData(ifdId);
+ mIfdDatas[ifdId] = ifdData;
+ }
+ return ifdData;
+ }
+
+ /**
+ * Returns the tag with a given TID in the given IFD if the tag exists.
+ * Otherwise returns null.
+ */
+ protected ExifTag getTag(short tag, int ifd) {
+ IfdData ifdData = mIfdDatas[ifd];
+ return (ifdData == null) ? null : ifdData.getTag(tag);
+ }
+
+ /**
+ * Adds the given ExifTag to its default IFD and returns an existing ExifTag
+ * with the same TID or null if none exist.
+ */
+ protected ExifTag addTag(ExifTag tag) {
+ if (tag != null) {
+ int ifd = tag.getIfd();
+ return addTag(tag, ifd);
+ }
+ return null;
+ }
+
+ /**
+ * Adds the given ExifTag to the given IFD and returns an existing ExifTag
+ * with the same TID or null if none exist.
+ */
+ protected ExifTag addTag(ExifTag tag, int ifdId) {
+ if (tag != null && ExifTag.isValidIfd(ifdId)) {
+ IfdData ifdData = getOrCreateIfdData(ifdId);
+ return ifdData.setTag(tag);
+ }
+ return null;
+ }
+
+ protected void clearThumbnailAndStrips() {
+ mThumbnail = null;
+ mStripBytes.clear();
+ }
+
+ /**
+ * Removes the thumbnail and its related tags. IFD1 will be removed.
+ */
+ protected void removeThumbnailData() {
+ clearThumbnailAndStrips();
+ mIfdDatas[IfdId.TYPE_IFD_1] = null;
+ }
+
+ /**
+ * Removes the tag with a given TID and IFD.
+ */
+ protected void removeTag(short tagId, int ifdId) {
+ IfdData ifdData = mIfdDatas[ifdId];
+ if (ifdData == null) {
+ return;
+ }
+ ifdData.removeTag(tagId);
+ }
+
+ /**
+ * Decodes the user comment tag into string as specified in the EXIF
+ * standard. Returns null if decoding failed.
+ */
+ protected String getUserComment() {
+ IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_0];
+ if (ifdData == null) {
+ return null;
+ }
+ ExifTag tag = ifdData.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT));
+ if (tag == null) {
+ return null;
+ }
+ if (tag.getComponentCount() < 8) {
+ return null;
+ }
+
+ byte[] buf = new byte[tag.getComponentCount()];
+ tag.getBytes(buf);
+
+ byte[] code = new byte[8];
+ System.arraycopy(buf, 0, code, 0, 8);
+
+ try {
+ if (Arrays.equals(code, USER_COMMENT_ASCII)) {
+ return new String(buf, 8, buf.length - 8, "US-ASCII");
+ } else if (Arrays.equals(code, USER_COMMENT_JIS)) {
+ return new String(buf, 8, buf.length - 8, "EUC-JP");
+ } else if (Arrays.equals(code, USER_COMMENT_UNICODE)) {
+ return new String(buf, 8, buf.length - 8, "UTF-16");
+ } else {
+ return null;
+ }
+ } catch (UnsupportedEncodingException e) {
+ Log.w(TAG, "Failed to decode the user comment");
+ return null;
+ }
+ }
+
+ /**
+ * Returns a list of all {@link ExifTag}s in the ExifData or null if there
+ * are none.
+ */
+ protected List<ExifTag> getAllTags() {
+ ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
+ for (IfdData d : mIfdDatas) {
+ if (d != null) {
+ ExifTag[] tags = d.getAllTags();
+ if (tags != null) {
+ for (ExifTag t : tags) {
+ ret.add(t);
+ }
+ }
+ }
+ }
+ if (ret.size() == 0) {
+ return null;
+ }
+ return ret;
+ }
+
+ /**
+ * Returns a list of all {@link ExifTag}s in a given IFD or null if there
+ * are none.
+ */
+ protected List<ExifTag> getAllTagsForIfd(int ifd) {
+ IfdData d = mIfdDatas[ifd];
+ if (d == null) {
+ return null;
+ }
+ ExifTag[] tags = d.getAllTags();
+ if (tags == null) {
+ return null;
+ }
+ ArrayList<ExifTag> ret = new ArrayList<ExifTag>(tags.length);
+ for (ExifTag t : tags) {
+ ret.add(t);
+ }
+ if (ret.size() == 0) {
+ return null;
+ }
+ return ret;
+ }
+
+ /**
+ * Returns a list of all {@link ExifTag}s with a given TID or null if there
+ * are none.
+ */
+ protected List<ExifTag> getAllTagsForTagId(short tag) {
+ ArrayList<ExifTag> ret = new ArrayList<ExifTag>();
+ for (IfdData d : mIfdDatas) {
+ if (d != null) {
+ ExifTag t = d.getTag(tag);
+ if (t != null) {
+ ret.add(t);
+ }
+ }
+ }
+ if (ret.size() == 0) {
+ return null;
+ }
+ return ret;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (obj instanceof ExifData) {
+ ExifData data = (ExifData) obj;
+ if (data.mByteOrder != mByteOrder ||
+ data.mStripBytes.size() != mStripBytes.size() ||
+ !Arrays.equals(data.mThumbnail, mThumbnail)) {
+ return false;
+ }
+ for (int i = 0; i < mStripBytes.size(); i++) {
+ if (!Arrays.equals(data.mStripBytes.get(i), mStripBytes.get(i))) {
+ return false;
+ }
+ }
+ for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+ IfdData ifd1 = data.getIfdData(i);
+ IfdData ifd2 = getIfdData(i);
+ if (ifd1 != ifd2 && ifd1 != null && !ifd1.equals(ifd2)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/src/com/android/gallery3d/exif/ExifInterface.java b/src/com/android/gallery3d/exif/ExifInterface.java
new file mode 100644
index 0000000..9247e87
--- /dev/null
+++ b/src/com/android/gallery3d/exif/ExifInterface.java
@@ -0,0 +1,2407 @@
+/*
+ * 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.exif;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.SparseIntArray;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel.MapMode;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.TimeZone;
+
+/**
+ * This class provides methods and constants for reading and writing jpeg file
+ * metadata. It contains a collection of ExifTags, and a collection of
+ * definitions for creating valid ExifTags. The collection of ExifTags can be
+ * updated by: reading new ones from a file, deleting or adding existing ones,
+ * or building new ExifTags from a tag definition. These ExifTags can be written
+ * to a valid jpeg image as exif metadata.
+ * <p>
+ * Each ExifTag has a tag ID (TID) and is stored in a specific image file
+ * directory (IFD) as specified by the exif standard. A tag definition can be
+ * looked up with a constant that is a combination of TID and IFD. This
+ * definition has information about the type, number of components, and valid
+ * IFDs for a tag.
+ *
+ * @see ExifTag
+ */
+public class ExifInterface {
+ public static final int TAG_NULL = -1;
+ public static final int IFD_NULL = -1;
+ public static final int DEFINITION_NULL = 0;
+
+ /**
+ * Tag constants for Jeita EXIF 2.2
+ */
+
+ // IFD 0
+ public static final int TAG_IMAGE_WIDTH =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0100);
+ public static final int TAG_IMAGE_LENGTH =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0101); // Image height
+ public static final int TAG_BITS_PER_SAMPLE =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0102);
+ public static final int TAG_COMPRESSION =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0103);
+ public static final int TAG_PHOTOMETRIC_INTERPRETATION =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0106);
+ public static final int TAG_IMAGE_DESCRIPTION =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x010E);
+ public static final int TAG_MAKE =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x010F);
+ public static final int TAG_MODEL =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0110);
+ public static final int TAG_STRIP_OFFSETS =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0111);
+ public static final int TAG_ORIENTATION =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0112);
+ public static final int TAG_SAMPLES_PER_PIXEL =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0115);
+ public static final int TAG_ROWS_PER_STRIP =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0116);
+ public static final int TAG_STRIP_BYTE_COUNTS =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0117);
+ public static final int TAG_X_RESOLUTION =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x011A);
+ public static final int TAG_Y_RESOLUTION =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x011B);
+ public static final int TAG_PLANAR_CONFIGURATION =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x011C);
+ public static final int TAG_RESOLUTION_UNIT =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0128);
+ public static final int TAG_TRANSFER_FUNCTION =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x012D);
+ public static final int TAG_SOFTWARE =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0131);
+ public static final int TAG_DATE_TIME =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0132);
+ public static final int TAG_ARTIST =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x013B);
+ public static final int TAG_WHITE_POINT =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x013E);
+ public static final int TAG_PRIMARY_CHROMATICITIES =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x013F);
+ public static final int TAG_Y_CB_CR_COEFFICIENTS =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0211);
+ public static final int TAG_Y_CB_CR_SUB_SAMPLING =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0212);
+ public static final int TAG_Y_CB_CR_POSITIONING =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0213);
+ public static final int TAG_REFERENCE_BLACK_WHITE =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x0214);
+ public static final int TAG_COPYRIGHT =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x8298);
+ public static final int TAG_EXIF_IFD =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x8769);
+ public static final int TAG_GPS_IFD =
+ defineTag(IfdId.TYPE_IFD_0, (short) 0x8825);
+ // IFD 1
+ public static final int TAG_JPEG_INTERCHANGE_FORMAT =
+ defineTag(IfdId.TYPE_IFD_1, (short) 0x0201);
+ public static final int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH =
+ defineTag(IfdId.TYPE_IFD_1, (short) 0x0202);
+ // IFD Exif Tags
+ public static final int TAG_EXPOSURE_TIME =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x829A);
+ public static final int TAG_F_NUMBER =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x829D);
+ public static final int TAG_EXPOSURE_PROGRAM =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8822);
+ public static final int TAG_SPECTRAL_SENSITIVITY =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8824);
+ public static final int TAG_ISO_SPEED_RATINGS =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8827);
+ public static final int TAG_OECF =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8828);
+ public static final int TAG_EXIF_VERSION =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9000);
+ public static final int TAG_DATE_TIME_ORIGINAL =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9003);
+ public static final int TAG_DATE_TIME_DIGITIZED =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9004);
+ public static final int TAG_COMPONENTS_CONFIGURATION =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9101);
+ public static final int TAG_COMPRESSED_BITS_PER_PIXEL =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9102);
+ public static final int TAG_SHUTTER_SPEED_VALUE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9201);
+ public static final int TAG_APERTURE_VALUE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9202);
+ public static final int TAG_BRIGHTNESS_VALUE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9203);
+ public static final int TAG_EXPOSURE_BIAS_VALUE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9204);
+ public static final int TAG_MAX_APERTURE_VALUE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9205);
+ public static final int TAG_SUBJECT_DISTANCE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9206);
+ public static final int TAG_METERING_MODE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9207);
+ public static final int TAG_LIGHT_SOURCE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9208);
+ public static final int TAG_FLASH =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9209);
+ public static final int TAG_FOCAL_LENGTH =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x920A);
+ public static final int TAG_SUBJECT_AREA =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9214);
+ public static final int TAG_MAKER_NOTE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x927C);
+ public static final int TAG_USER_COMMENT =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9286);
+ public static final int TAG_SUB_SEC_TIME =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9290);
+ public static final int TAG_SUB_SEC_TIME_ORIGINAL =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9291);
+ public static final int TAG_SUB_SEC_TIME_DIGITIZED =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9292);
+ public static final int TAG_FLASHPIX_VERSION =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA000);
+ public static final int TAG_COLOR_SPACE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA001);
+ public static final int TAG_PIXEL_X_DIMENSION =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA002);
+ public static final int TAG_PIXEL_Y_DIMENSION =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA003);
+ public static final int TAG_RELATED_SOUND_FILE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA004);
+ public static final int TAG_INTEROPERABILITY_IFD =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA005);
+ public static final int TAG_FLASH_ENERGY =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20B);
+ public static final int TAG_SPATIAL_FREQUENCY_RESPONSE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20C);
+ public static final int TAG_FOCAL_PLANE_X_RESOLUTION =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20E);
+ public static final int TAG_FOCAL_PLANE_Y_RESOLUTION =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20F);
+ public static final int TAG_FOCAL_PLANE_RESOLUTION_UNIT =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA210);
+ public static final int TAG_SUBJECT_LOCATION =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA214);
+ public static final int TAG_EXPOSURE_INDEX =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA215);
+ public static final int TAG_SENSING_METHOD =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA217);
+ public static final int TAG_FILE_SOURCE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA300);
+ public static final int TAG_SCENE_TYPE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA301);
+ public static final int TAG_CFA_PATTERN =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA302);
+ public static final int TAG_CUSTOM_RENDERED =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA401);
+ public static final int TAG_EXPOSURE_MODE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA402);
+ public static final int TAG_WHITE_BALANCE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA403);
+ public static final int TAG_DIGITAL_ZOOM_RATIO =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA404);
+ public static final int TAG_FOCAL_LENGTH_IN_35_MM_FILE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA405);
+ public static final int TAG_SCENE_CAPTURE_TYPE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA406);
+ public static final int TAG_GAIN_CONTROL =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA407);
+ public static final int TAG_CONTRAST =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA408);
+ public static final int TAG_SATURATION =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA409);
+ public static final int TAG_SHARPNESS =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40A);
+ public static final int TAG_DEVICE_SETTING_DESCRIPTION =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40B);
+ public static final int TAG_SUBJECT_DISTANCE_RANGE =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40C);
+ public static final int TAG_IMAGE_UNIQUE_ID =
+ defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA420);
+ // IFD GPS tags
+ public static final int TAG_GPS_VERSION_ID =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 0);
+ public static final int TAG_GPS_LATITUDE_REF =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 1);
+ public static final int TAG_GPS_LATITUDE =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 2);
+ public static final int TAG_GPS_LONGITUDE_REF =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 3);
+ public static final int TAG_GPS_LONGITUDE =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 4);
+ public static final int TAG_GPS_ALTITUDE_REF =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 5);
+ public static final int TAG_GPS_ALTITUDE =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 6);
+ public static final int TAG_GPS_TIME_STAMP =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 7);
+ public static final int TAG_GPS_SATTELLITES =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 8);
+ public static final int TAG_GPS_STATUS =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 9);
+ public static final int TAG_GPS_MEASURE_MODE =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 10);
+ public static final int TAG_GPS_DOP =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 11);
+ public static final int TAG_GPS_SPEED_REF =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 12);
+ public static final int TAG_GPS_SPEED =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 13);
+ public static final int TAG_GPS_TRACK_REF =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 14);
+ public static final int TAG_GPS_TRACK =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 15);
+ public static final int TAG_GPS_IMG_DIRECTION_REF =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 16);
+ public static final int TAG_GPS_IMG_DIRECTION =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 17);
+ public static final int TAG_GPS_MAP_DATUM =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 18);
+ public static final int TAG_GPS_DEST_LATITUDE_REF =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 19);
+ public static final int TAG_GPS_DEST_LATITUDE =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 20);
+ public static final int TAG_GPS_DEST_LONGITUDE_REF =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 21);
+ public static final int TAG_GPS_DEST_LONGITUDE =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 22);
+ public static final int TAG_GPS_DEST_BEARING_REF =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 23);
+ public static final int TAG_GPS_DEST_BEARING =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 24);
+ public static final int TAG_GPS_DEST_DISTANCE_REF =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 25);
+ public static final int TAG_GPS_DEST_DISTANCE =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 26);
+ public static final int TAG_GPS_PROCESSING_METHOD =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 27);
+ public static final int TAG_GPS_AREA_INFORMATION =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 28);
+ public static final int TAG_GPS_DATE_STAMP =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 29);
+ public static final int TAG_GPS_DIFFERENTIAL =
+ defineTag(IfdId.TYPE_IFD_GPS, (short) 30);
+ // IFD Interoperability tags
+ public static final int TAG_INTEROPERABILITY_INDEX =
+ defineTag(IfdId.TYPE_IFD_INTEROPERABILITY, (short) 1);
+
+ /**
+ * Tags that contain offset markers. These are included in the banned
+ * defines.
+ */
+ private static HashSet<Short> sOffsetTags = new HashSet<Short>();
+ static {
+ sOffsetTags.add(getTrueTagKey(TAG_GPS_IFD));
+ sOffsetTags.add(getTrueTagKey(TAG_EXIF_IFD));
+ sOffsetTags.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT));
+ sOffsetTags.add(getTrueTagKey(TAG_INTEROPERABILITY_IFD));
+ sOffsetTags.add(getTrueTagKey(TAG_STRIP_OFFSETS));
+ }
+
+ /**
+ * Tags with definitions that cannot be overridden (banned defines).
+ */
+ protected static HashSet<Short> sBannedDefines = new HashSet<Short>(sOffsetTags);
+ static {
+ sBannedDefines.add(getTrueTagKey(TAG_NULL));
+ sBannedDefines.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
+ sBannedDefines.add(getTrueTagKey(TAG_STRIP_BYTE_COUNTS));
+ }
+
+ /**
+ * Returns the constant representing a tag with a given TID and default IFD.
+ */
+ public static int defineTag(int ifdId, short tagId) {
+ return (tagId & 0x0000ffff) | (ifdId << 16);
+ }
+
+ /**
+ * Returns the TID for a tag constant.
+ */
+ public static short getTrueTagKey(int tag) {
+ // Truncate
+ return (short) tag;
+ }
+
+ /**
+ * Returns the default IFD for a tag constant.
+ */
+ public static int getTrueIfd(int tag) {
+ return tag >>> 16;
+ }
+
+ /**
+ * Constants for {@link TAG_ORIENTATION}. They can be interpreted as
+ * follows:
+ * <ul>
+ * <li>TOP_LEFT is the normal orientation.</li>
+ * <li>TOP_RIGHT is a left-right mirror.</li>
+ * <li>BOTTOM_LEFT is a 180 degree rotation.</li>
+ * <li>BOTTOM_RIGHT is a top-bottom mirror.</li>
+ * <li>LEFT_TOP is mirrored about the top-left<->bottom-right axis.</li>
+ * <li>RIGHT_TOP is a 90 degree clockwise rotation.</li>
+ * <li>LEFT_BOTTOM is mirrored about the top-right<->bottom-left axis.</li>
+ * <li>RIGHT_BOTTOM is a 270 degree clockwise rotation.</li>
+ * </ul>
+ */
+ public static interface Orientation {
+ public static final short TOP_LEFT = 1;
+ public static final short TOP_RIGHT = 2;
+ public static final short BOTTOM_LEFT = 3;
+ public static final short BOTTOM_RIGHT = 4;
+ public static final short LEFT_TOP = 5;
+ public static final short RIGHT_TOP = 6;
+ public static final short LEFT_BOTTOM = 7;
+ public static final short RIGHT_BOTTOM = 8;
+ }
+
+ /**
+ * Constants for {@link TAG_Y_CB_CR_POSITIONING}
+ */
+ public static interface YCbCrPositioning {
+ public static final short CENTERED = 1;
+ public static final short CO_SITED = 2;
+ }
+
+ /**
+ * Constants for {@link TAG_COMPRESSION}
+ */
+ public static interface Compression {
+ public static final short UNCOMPRESSION = 1;
+ public static final short JPEG = 6;
+ }
+
+ /**
+ * Constants for {@link TAG_RESOLUTION_UNIT}
+ */
+ public static interface ResolutionUnit {
+ public static final short INCHES = 2;
+ public static final short CENTIMETERS = 3;
+ }
+
+ /**
+ * Constants for {@link TAG_PHOTOMETRIC_INTERPRETATION}
+ */
+ public static interface PhotometricInterpretation {
+ public static final short RGB = 2;
+ public static final short YCBCR = 6;
+ }
+
+ /**
+ * Constants for {@link TAG_PLANAR_CONFIGURATION}
+ */
+ public static interface PlanarConfiguration {
+ public static final short CHUNKY = 1;
+ public static final short PLANAR = 2;
+ }
+
+ /**
+ * Constants for {@link TAG_EXPOSURE_PROGRAM}
+ */
+ public static interface ExposureProgram {
+ public static final short NOT_DEFINED = 0;
+ public static final short MANUAL = 1;
+ public static final short NORMAL_PROGRAM = 2;
+ public static final short APERTURE_PRIORITY = 3;
+ public static final short SHUTTER_PRIORITY = 4;
+ public static final short CREATIVE_PROGRAM = 5;
+ public static final short ACTION_PROGRAM = 6;
+ public static final short PROTRAIT_MODE = 7;
+ public static final short LANDSCAPE_MODE = 8;
+ }
+
+ /**
+ * Constants for {@link TAG_METERING_MODE}
+ */
+ public static interface MeteringMode {
+ public static final short UNKNOWN = 0;
+ public static final short AVERAGE = 1;
+ public static final short CENTER_WEIGHTED_AVERAGE = 2;
+ public static final short SPOT = 3;
+ public static final short MULTISPOT = 4;
+ public static final short PATTERN = 5;
+ public static final short PARTAIL = 6;
+ public static final short OTHER = 255;
+ }
+
+ /**
+ * Constants for {@link TAG_FLASH} As the definition in Jeita EXIF 2.2
+ * standard, we can treat this constant as bitwise flag.
+ * <p>
+ * e.g.
+ * <p>
+ * short flash = FIRED | RETURN_STROBE_RETURN_LIGHT_DETECTED |
+ * MODE_AUTO_MODE
+ */
+ public static interface Flash {
+ // LSB
+ public static final short DID_NOT_FIRED = 0;
+ public static final short FIRED = 1;
+ // 1st~2nd bits
+ public static final short RETURN_NO_STROBE_RETURN_DETECTION_FUNCTION = 0 << 1;
+ public static final short RETURN_STROBE_RETURN_LIGHT_NOT_DETECTED = 2 << 1;
+ public static final short RETURN_STROBE_RETURN_LIGHT_DETECTED = 3 << 1;
+ // 3rd~4th bits
+ public static final short MODE_UNKNOWN = 0 << 3;
+ public static final short MODE_COMPULSORY_FLASH_FIRING = 1 << 3;
+ public static final short MODE_COMPULSORY_FLASH_SUPPRESSION = 2 << 3;
+ public static final short MODE_AUTO_MODE = 3 << 3;
+ // 5th bit
+ public static final short FUNCTION_PRESENT = 0 << 5;
+ public static final short FUNCTION_NO_FUNCTION = 1 << 5;
+ // 6th bit
+ public static final short RED_EYE_REDUCTION_NO_OR_UNKNOWN = 0 << 6;
+ public static final short RED_EYE_REDUCTION_SUPPORT = 1 << 6;
+ }
+
+ /**
+ * Constants for {@link TAG_COLOR_SPACE}
+ */
+ public static interface ColorSpace {
+ public static final short SRGB = 1;
+ public static final short UNCALIBRATED = (short) 0xFFFF;
+ }
+
+ /**
+ * Constants for {@link TAG_EXPOSURE_MODE}
+ */
+ public static interface ExposureMode {
+ public static final short AUTO_EXPOSURE = 0;
+ public static final short MANUAL_EXPOSURE = 1;
+ public static final short AUTO_BRACKET = 2;
+ }
+
+ /**
+ * Constants for {@link TAG_WHITE_BALANCE}
+ */
+ public static interface WhiteBalance {
+ public static final short AUTO = 0;
+ public static final short MANUAL = 1;
+ }
+
+ /**
+ * Constants for {@link TAG_SCENE_CAPTURE_TYPE}
+ */
+ public static interface SceneCapture {
+ public static final short STANDARD = 0;
+ public static final short LANDSCAPE = 1;
+ public static final short PROTRAIT = 2;
+ public static final short NIGHT_SCENE = 3;
+ }
+
+ /**
+ * Constants for {@link TAG_COMPONENTS_CONFIGURATION}
+ */
+ public static interface ComponentsConfiguration {
+ public static final short NOT_EXIST = 0;
+ public static final short Y = 1;
+ public static final short CB = 2;
+ public static final short CR = 3;
+ public static final short R = 4;
+ public static final short G = 5;
+ public static final short B = 6;
+ }
+
+ /**
+ * Constants for {@link TAG_LIGHT_SOURCE}
+ */
+ public static interface LightSource {
+ public static final short UNKNOWN = 0;
+ public static final short DAYLIGHT = 1;
+ public static final short FLUORESCENT = 2;
+ public static final short TUNGSTEN = 3;
+ public static final short FLASH = 4;
+ public static final short FINE_WEATHER = 9;
+ public static final short CLOUDY_WEATHER = 10;
+ public static final short SHADE = 11;
+ public static final short DAYLIGHT_FLUORESCENT = 12;
+ public static final short DAY_WHITE_FLUORESCENT = 13;
+ public static final short COOL_WHITE_FLUORESCENT = 14;
+ public static final short WHITE_FLUORESCENT = 15;
+ public static final short STANDARD_LIGHT_A = 17;
+ public static final short STANDARD_LIGHT_B = 18;
+ public static final short STANDARD_LIGHT_C = 19;
+ public static final short D55 = 20;
+ public static final short D65 = 21;
+ public static final short D75 = 22;
+ public static final short D50 = 23;
+ public static final short ISO_STUDIO_TUNGSTEN = 24;
+ public static final short OTHER = 255;
+ }
+
+ /**
+ * Constants for {@link TAG_SENSING_METHOD}
+ */
+ public static interface SensingMethod {
+ public static final short NOT_DEFINED = 1;
+ public static final short ONE_CHIP_COLOR = 2;
+ public static final short TWO_CHIP_COLOR = 3;
+ public static final short THREE_CHIP_COLOR = 4;
+ public static final short COLOR_SEQUENTIAL_AREA = 5;
+ public static final short TRILINEAR = 7;
+ public static final short COLOR_SEQUENTIAL_LINEAR = 8;
+ }
+
+ /**
+ * Constants for {@link TAG_FILE_SOURCE}
+ */
+ public static interface FileSource {
+ public static final short DSC = 3;
+ }
+
+ /**
+ * Constants for {@link TAG_SCENE_TYPE}
+ */
+ public static interface SceneType {
+ public static final short DIRECT_PHOTOGRAPHED = 1;
+ }
+
+ /**
+ * Constants for {@link TAG_GAIN_CONTROL}
+ */
+ public static interface GainControl {
+ public static final short NONE = 0;
+ public static final short LOW_UP = 1;
+ public static final short HIGH_UP = 2;
+ public static final short LOW_DOWN = 3;
+ public static final short HIGH_DOWN = 4;
+ }
+
+ /**
+ * Constants for {@link TAG_CONTRAST}
+ */
+ public static interface Contrast {
+ public static final short NORMAL = 0;
+ public static final short SOFT = 1;
+ public static final short HARD = 2;
+ }
+
+ /**
+ * Constants for {@link TAG_SATURATION}
+ */
+ public static interface Saturation {
+ public static final short NORMAL = 0;
+ public static final short LOW = 1;
+ public static final short HIGH = 2;
+ }
+
+ /**
+ * Constants for {@link TAG_SHARPNESS}
+ */
+ public static interface Sharpness {
+ public static final short NORMAL = 0;
+ public static final short SOFT = 1;
+ public static final short HARD = 2;
+ }
+
+ /**
+ * Constants for {@link TAG_SUBJECT_DISTANCE}
+ */
+ public static interface SubjectDistance {
+ public static final short UNKNOWN = 0;
+ public static final short MACRO = 1;
+ public static final short CLOSE_VIEW = 2;
+ public static final short DISTANT_VIEW = 3;
+ }
+
+ /**
+ * Constants for {@link TAG_GPS_LATITUDE_REF},
+ * {@link TAG_GPS_DEST_LATITUDE_REF}
+ */
+ public static interface GpsLatitudeRef {
+ public static final String NORTH = "N";
+ public static final String SOUTH = "S";
+ }
+
+ /**
+ * Constants for {@link TAG_GPS_LONGITUDE_REF},
+ * {@link TAG_GPS_DEST_LONGITUDE_REF}
+ */
+ public static interface GpsLongitudeRef {
+ public static final String EAST = "E";
+ public static final String WEST = "W";
+ }
+
+ /**
+ * Constants for {@link TAG_GPS_ALTITUDE_REF}
+ */
+ public static interface GpsAltitudeRef {
+ public static final short SEA_LEVEL = 0;
+ public static final short SEA_LEVEL_NEGATIVE = 1;
+ }
+
+ /**
+ * Constants for {@link TAG_GPS_STATUS}
+ */
+ public static interface GpsStatus {
+ public static final String IN_PROGRESS = "A";
+ public static final String INTEROPERABILITY = "V";
+ }
+
+ /**
+ * Constants for {@link TAG_GPS_MEASURE_MODE}
+ */
+ public static interface GpsMeasureMode {
+ public static final String MODE_2_DIMENSIONAL = "2";
+ public static final String MODE_3_DIMENSIONAL = "3";
+ }
+
+ /**
+ * Constants for {@link TAG_GPS_SPEED_REF},
+ * {@link TAG_GPS_DEST_DISTANCE_REF}
+ */
+ public static interface GpsSpeedRef {
+ public static final String KILOMETERS = "K";
+ public static final String MILES = "M";
+ public static final String KNOTS = "N";
+ }
+
+ /**
+ * Constants for {@link TAG_GPS_TRACK_REF},
+ * {@link TAG_GPS_IMG_DIRECTION_REF}, {@link TAG_GPS_DEST_BEARING_REF}
+ */
+ public static interface GpsTrackRef {
+ public static final String TRUE_DIRECTION = "T";
+ public static final String MAGNETIC_DIRECTION = "M";
+ }
+
+ /**
+ * Constants for {@link TAG_GPS_DIFFERENTIAL}
+ */
+ public static interface GpsDifferential {
+ public static final short WITHOUT_DIFFERENTIAL_CORRECTION = 0;
+ public static final short DIFFERENTIAL_CORRECTION_APPLIED = 1;
+ }
+
+ private static final String NULL_ARGUMENT_STRING = "Argument is null";
+ private ExifData mData = new ExifData(DEFAULT_BYTE_ORDER);
+ public static final ByteOrder DEFAULT_BYTE_ORDER = ByteOrder.BIG_ENDIAN;
+
+ public ExifInterface() {
+ mGPSDateStampFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ /**
+ * Reads the exif tags from a byte array, clearing this ExifInterface
+ * object's existing exif tags.
+ *
+ * @param jpeg a byte array containing a jpeg compressed image.
+ * @throws IOException
+ */
+ public void readExif(byte[] jpeg) throws IOException {
+ readExif(new ByteArrayInputStream(jpeg));
+ }
+
+ /**
+ * Reads the exif tags from an InputStream, clearing this ExifInterface
+ * object's existing exif tags.
+ *
+ * @param inStream an InputStream containing a jpeg compressed image.
+ * @throws IOException
+ */
+ public void readExif(InputStream inStream) throws IOException {
+ if (inStream == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ ExifData d = null;
+ try {
+ d = new ExifReader(this).read(inStream);
+ } catch (ExifInvalidFormatException e) {
+ throw new IOException("Invalid exif format : " + e);
+ }
+ mData = d;
+ }
+
+ /**
+ * Reads the exif tags from a file, clearing this ExifInterface object's
+ * existing exif tags.
+ *
+ * @param inFileName a string representing the filepath to jpeg file.
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ public void readExif(String inFileName) throws FileNotFoundException, IOException {
+ if (inFileName == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ InputStream is = null;
+ try {
+ is = (InputStream) new BufferedInputStream(new FileInputStream(inFileName));
+ readExif(is);
+ } catch (IOException e) {
+ closeSilently(is);
+ throw e;
+ }
+ is.close();
+ }
+
+ /**
+ * Sets the exif tags, clearing this ExifInterface object's existing exif
+ * tags.
+ *
+ * @param tags a collection of exif tags to set.
+ */
+ public void setExif(Collection<ExifTag> tags) {
+ clearExif();
+ setTags(tags);
+ }
+
+ /**
+ * Clears this ExifInterface object's existing exif tags.
+ */
+ public void clearExif() {
+ mData = new ExifData(DEFAULT_BYTE_ORDER);
+ }
+
+ /**
+ * Writes the tags from this ExifInterface object into a jpeg image,
+ * removing prior exif tags.
+ *
+ * @param jpeg a byte array containing a jpeg compressed image.
+ * @param exifOutStream an OutputStream to which the jpeg image with added
+ * exif tags will be written.
+ * @throws IOException
+ */
+ public void writeExif(byte[] jpeg, OutputStream exifOutStream) throws IOException {
+ if (jpeg == null || exifOutStream == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ OutputStream s = getExifWriterStream(exifOutStream);
+ s.write(jpeg, 0, jpeg.length);
+ s.flush();
+ }
+
+ /**
+ * Writes the tags from this ExifInterface object into a jpeg compressed
+ * bitmap, removing prior exif tags.
+ *
+ * @param bmap a bitmap to compress and write exif into.
+ * @param exifOutStream the OutputStream to which the jpeg image with added
+ * exif tags will be written.
+ * @throws IOException
+ */
+ public void writeExif(Bitmap bmap, OutputStream exifOutStream) throws IOException {
+ if (bmap == null || exifOutStream == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ OutputStream s = getExifWriterStream(exifOutStream);
+ bmap.compress(Bitmap.CompressFormat.JPEG, 90, s);
+ s.flush();
+ }
+
+ /**
+ * Writes the tags from this ExifInterface object into a jpeg stream,
+ * removing prior exif tags.
+ *
+ * @param jpegStream an InputStream containing a jpeg compressed image.
+ * @param exifOutStream an OutputStream to which the jpeg image with added
+ * exif tags will be written.
+ * @throws IOException
+ */
+ public void writeExif(InputStream jpegStream, OutputStream exifOutStream) throws IOException {
+ if (jpegStream == null || exifOutStream == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ OutputStream s = getExifWriterStream(exifOutStream);
+ doExifStreamIO(jpegStream, s);
+ s.flush();
+ }
+
+ /**
+ * Writes the tags from this ExifInterface object into a jpeg image,
+ * removing prior exif tags.
+ *
+ * @param jpeg a byte array containing a jpeg compressed image.
+ * @param exifOutFileName a String containing the filepath to which the jpeg
+ * image with added exif tags will be written.
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ public void writeExif(byte[] jpeg, String exifOutFileName) throws FileNotFoundException,
+ IOException {
+ if (jpeg == null || exifOutFileName == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ OutputStream s = null;
+ try {
+ s = getExifWriterStream(exifOutFileName);
+ s.write(jpeg, 0, jpeg.length);
+ s.flush();
+ } catch (IOException e) {
+ closeSilently(s);
+ throw e;
+ }
+ s.close();
+ }
+
+ /**
+ * Writes the tags from this ExifInterface object into a jpeg compressed
+ * bitmap, removing prior exif tags.
+ *
+ * @param bmap a bitmap to compress and write exif into.
+ * @param exifOutFileName a String containing the filepath to which the jpeg
+ * image with added exif tags will be written.
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ public void writeExif(Bitmap bmap, String exifOutFileName) throws FileNotFoundException,
+ IOException {
+ if (bmap == null || exifOutFileName == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ OutputStream s = null;
+ try {
+ s = getExifWriterStream(exifOutFileName);
+ bmap.compress(Bitmap.CompressFormat.JPEG, 90, s);
+ s.flush();
+ } catch (IOException e) {
+ closeSilently(s);
+ throw e;
+ }
+ s.close();
+ }
+
+ /**
+ * Writes the tags from this ExifInterface object into a jpeg stream,
+ * removing prior exif tags.
+ *
+ * @param jpegStream an InputStream containing a jpeg compressed image.
+ * @param exifOutFileName a String containing the filepath to which the jpeg
+ * image with added exif tags will be written.
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ public void writeExif(InputStream jpegStream, String exifOutFileName)
+ throws FileNotFoundException, IOException {
+ if (jpegStream == null || exifOutFileName == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ OutputStream s = null;
+ try {
+ s = getExifWriterStream(exifOutFileName);
+ doExifStreamIO(jpegStream, s);
+ s.flush();
+ } catch (IOException e) {
+ closeSilently(s);
+ throw e;
+ }
+ s.close();
+ }
+
+ /**
+ * Writes the tags from this ExifInterface object into a jpeg file, removing
+ * prior exif tags.
+ *
+ * @param jpegFileName a String containing the filepath for a jpeg file.
+ * @param exifOutFileName a String containing the filepath to which the jpeg
+ * image with added exif tags will be written.
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ public void writeExif(String jpegFileName, String exifOutFileName)
+ throws FileNotFoundException, IOException {
+ if (jpegFileName == null || exifOutFileName == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ InputStream is = null;
+ try {
+ is = new FileInputStream(jpegFileName);
+ writeExif(is, exifOutFileName);
+ } catch (IOException e) {
+ closeSilently(is);
+ throw e;
+ }
+ is.close();
+ }
+
+ /**
+ * Wraps an OutputStream object with an ExifOutputStream. Exif tags in this
+ * ExifInterface object will be added to a jpeg image written to this
+ * stream, removing prior exif tags. Other methods of this ExifInterface
+ * object should not be called until the returned OutputStream has been
+ * closed.
+ *
+ * @param outStream an OutputStream to wrap.
+ * @return an OutputStream that wraps the outStream parameter, and adds exif
+ * metadata. A jpeg image should be written to this stream.
+ */
+ public OutputStream getExifWriterStream(OutputStream outStream) {
+ if (outStream == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ ExifOutputStream eos = new ExifOutputStream(outStream, this);
+ eos.setExifData(mData);
+ return eos;
+ }
+
+ /**
+ * Returns an OutputStream object that writes to a file. Exif tags in this
+ * ExifInterface object will be added to a jpeg image written to this
+ * stream, removing prior exif tags. Other methods of this ExifInterface
+ * object should not be called until the returned OutputStream has been
+ * closed.
+ *
+ * @param exifOutFileName an String containing a filepath for a jpeg file.
+ * @return an OutputStream that writes to the exifOutFileName file, and adds
+ * exif metadata. A jpeg image should be written to this stream.
+ * @throws FileNotFoundException
+ */
+ public OutputStream getExifWriterStream(String exifOutFileName) throws FileNotFoundException {
+ if (exifOutFileName == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ OutputStream out = null;
+ try {
+ out = (OutputStream) new FileOutputStream(exifOutFileName);
+ } catch (FileNotFoundException e) {
+ closeSilently(out);
+ throw e;
+ }
+ return getExifWriterStream(out);
+ }
+
+ /**
+ * Attempts to do an in-place rewrite the exif metadata in a file for the
+ * given tags. If tags do not exist or do not have the same size as the
+ * existing exif tags, this method will fail.
+ *
+ * @param filename a String containing a filepath for a jpeg file with exif
+ * tags to rewrite.
+ * @param tags tags that will be written into the jpeg file over existing
+ * tags if possible.
+ * @return true if success, false if could not overwrite. If false, no
+ * changes are made to the file.
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ public boolean rewriteExif(String filename, Collection<ExifTag> tags)
+ throws FileNotFoundException, IOException {
+ RandomAccessFile file = null;
+ InputStream is = null;
+ boolean ret;
+ try {
+ File temp = new File(filename);
+ is = new BufferedInputStream(new FileInputStream(temp));
+
+ // Parse beginning of APP1 in exif to find size of exif header.
+ ExifParser parser = null;
+ try {
+ parser = ExifParser.parse(is, this);
+ } catch (ExifInvalidFormatException e) {
+ throw new IOException("Invalid exif format : ", e);
+ }
+ long exifSize = parser.getOffsetToExifEndFromSOF();
+
+ // Free up resources
+ is.close();
+ is = null;
+
+ // Open file for memory mapping.
+ file = new RandomAccessFile(temp, "rw");
+ long fileLength = file.length();
+ if (fileLength < exifSize) {
+ throw new IOException("Filesize changed during operation");
+ }
+
+ // Map only exif header into memory.
+ ByteBuffer buf = file.getChannel().map(MapMode.READ_WRITE, 0, exifSize);
+
+ // Attempt to overwrite tag values without changing lengths (avoids
+ // file copy).
+ ret = rewriteExif(buf, tags);
+ } catch (IOException e) {
+ closeSilently(file);
+ throw e;
+ } finally {
+ closeSilently(is);
+ }
+ file.close();
+ return ret;
+ }
+
+ /**
+ * Attempts to do an in-place rewrite the exif metadata in a ByteBuffer for
+ * the given tags. If tags do not exist or do not have the same size as the
+ * existing exif tags, this method will fail.
+ *
+ * @param buf a ByteBuffer containing a jpeg file with existing exif tags to
+ * rewrite.
+ * @param tags tags that will be written into the jpeg ByteBuffer over
+ * existing tags if possible.
+ * @return true if success, false if could not overwrite. If false, no
+ * changes are made to the ByteBuffer.
+ * @throws IOException
+ */
+ public boolean rewriteExif(ByteBuffer buf, Collection<ExifTag> tags) throws IOException {
+ ExifModifier mod = null;
+ try {
+ mod = new ExifModifier(buf, this);
+ for (ExifTag t : tags) {
+ mod.modifyTag(t);
+ }
+ return mod.commit();
+ } catch (ExifInvalidFormatException e) {
+ throw new IOException("Invalid exif format : " + e);
+ }
+ }
+
+ /**
+ * Attempts to do an in-place rewrite of the exif metadata. If this fails,
+ * fall back to overwriting file. This preserves tags that are not being
+ * rewritten.
+ *
+ * @param filename a String containing a filepath for a jpeg file.
+ * @param tags tags that will be written into the jpeg file over existing
+ * tags if possible.
+ * @throws FileNotFoundException
+ * @throws IOException
+ * @see #rewriteExif
+ */
+ public void forceRewriteExif(String filename, Collection<ExifTag> tags)
+ throws FileNotFoundException,
+ IOException {
+ // Attempt in-place write
+ if (!rewriteExif(filename, tags)) {
+ // Fall back to doing a copy
+ ExifData tempData = mData;
+ mData = new ExifData(DEFAULT_BYTE_ORDER);
+ FileInputStream is = null;
+ ByteArrayOutputStream bytes = null;
+ try {
+ is = new FileInputStream(filename);
+ bytes = new ByteArrayOutputStream();
+ doExifStreamIO(is, bytes);
+ byte[] imageBytes = bytes.toByteArray();
+ readExif(imageBytes);
+ setTags(tags);
+ writeExif(imageBytes, filename);
+ } catch (IOException e) {
+ closeSilently(is);
+ throw e;
+ } finally {
+ is.close();
+ // Prevent clobbering of mData
+ mData = tempData;
+ }
+ }
+ }
+
+ /**
+ * Attempts to do an in-place rewrite of the exif metadata using the tags in
+ * this ExifInterface object. If this fails, fall back to overwriting file.
+ * This preserves tags that are not being rewritten.
+ *
+ * @param filename a String containing a filepath for a jpeg file.
+ * @throws FileNotFoundException
+ * @throws IOException
+ * @see #rewriteExif
+ */
+ public void forceRewriteExif(String filename) throws FileNotFoundException, IOException {
+ forceRewriteExif(filename, getAllTags());
+ }
+
+ /**
+ * Get the exif tags in this ExifInterface object or null if none exist.
+ *
+ * @return a List of {@link ExifTag}s.
+ */
+ public List<ExifTag> getAllTags() {
+ return mData.getAllTags();
+ }
+
+ /**
+ * Returns a list of ExifTags that share a TID (which can be obtained by
+ * calling {@link #getTrueTagKey} on a defined tag constant) or null if none
+ * exist.
+ *
+ * @param tagId a TID as defined in the exif standard (or with
+ * {@link #defineTag}).
+ * @return a List of {@link ExifTag}s.
+ */
+ public List<ExifTag> getTagsForTagId(short tagId) {
+ return mData.getAllTagsForTagId(tagId);
+ }
+
+ /**
+ * Returns a list of ExifTags that share an IFD (which can be obtained by
+ * calling {@link #getTrueIFD} on a defined tag constant) or null if none
+ * exist.
+ *
+ * @param ifdId an IFD as defined in the exif standard (or with
+ * {@link #defineTag}).
+ * @return a List of {@link ExifTag}s.
+ */
+ public List<ExifTag> getTagsForIfdId(int ifdId) {
+ return mData.getAllTagsForIfd(ifdId);
+ }
+
+ /**
+ * Gets an ExifTag for an IFD other than the tag's default.
+ *
+ * @see #getTag
+ */
+ public ExifTag getTag(int tagId, int ifdId) {
+ if (!ExifTag.isValidIfd(ifdId)) {
+ return null;
+ }
+ return mData.getTag(getTrueTagKey(tagId), ifdId);
+ }
+
+ /**
+ * Returns the ExifTag in that tag's default IFD for a defined tag constant
+ * or null if none exists.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ * @return an {@link ExifTag} or null if none exists.
+ */
+ public ExifTag getTag(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTag(tagId, ifdId);
+ }
+
+ /**
+ * Gets a tag value for an IFD other than the tag's default.
+ *
+ * @see #getTagValue
+ */
+ public Object getTagValue(int tagId, int ifdId) {
+ ExifTag t = getTag(tagId, ifdId);
+ return (t == null) ? null : t.getValue();
+ }
+
+ /**
+ * Returns the value of the ExifTag in that tag's default IFD for a defined
+ * tag constant or null if none exists or the value could not be cast into
+ * the return type.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ * @return the value of the ExifTag or null if none exists.
+ */
+ public Object getTagValue(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagValue(tagId, ifdId);
+ }
+
+ /*
+ * Getter methods that are similar to getTagValue. Null is returned if the
+ * tag value cannot be cast into the return type.
+ */
+
+ /**
+ * @see #getTagValue
+ */
+ public String getTagStringValue(int tagId, int ifdId) {
+ ExifTag t = getTag(tagId, ifdId);
+ if (t == null) {
+ return null;
+ }
+ return t.getValueAsString();
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public String getTagStringValue(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagStringValue(tagId, ifdId);
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public Long getTagLongValue(int tagId, int ifdId) {
+ long[] l = getTagLongValues(tagId, ifdId);
+ if (l == null || l.length <= 0) {
+ return null;
+ }
+ return Long.valueOf(l[0]);
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public Long getTagLongValue(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagLongValue(tagId, ifdId);
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public Integer getTagIntValue(int tagId, int ifdId) {
+ int[] l = getTagIntValues(tagId, ifdId);
+ if (l == null || l.length <= 0) {
+ return null;
+ }
+ return Integer.valueOf(l[0]);
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public Integer getTagIntValue(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagIntValue(tagId, ifdId);
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public Byte getTagByteValue(int tagId, int ifdId) {
+ byte[] l = getTagByteValues(tagId, ifdId);
+ if (l == null || l.length <= 0) {
+ return null;
+ }
+ return Byte.valueOf(l[0]);
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public Byte getTagByteValue(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagByteValue(tagId, ifdId);
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public Rational getTagRationalValue(int tagId, int ifdId) {
+ Rational[] l = getTagRationalValues(tagId, ifdId);
+ if (l == null || l.length == 0) {
+ return null;
+ }
+ return new Rational(l[0]);
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public Rational getTagRationalValue(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagRationalValue(tagId, ifdId);
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public long[] getTagLongValues(int tagId, int ifdId) {
+ ExifTag t = getTag(tagId, ifdId);
+ if (t == null) {
+ return null;
+ }
+ return t.getValueAsLongs();
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public long[] getTagLongValues(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagLongValues(tagId, ifdId);
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public int[] getTagIntValues(int tagId, int ifdId) {
+ ExifTag t = getTag(tagId, ifdId);
+ if (t == null) {
+ return null;
+ }
+ return t.getValueAsInts();
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public int[] getTagIntValues(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagIntValues(tagId, ifdId);
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public byte[] getTagByteValues(int tagId, int ifdId) {
+ ExifTag t = getTag(tagId, ifdId);
+ if (t == null) {
+ return null;
+ }
+ return t.getValueAsBytes();
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public byte[] getTagByteValues(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagByteValues(tagId, ifdId);
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public Rational[] getTagRationalValues(int tagId, int ifdId) {
+ ExifTag t = getTag(tagId, ifdId);
+ if (t == null) {
+ return null;
+ }
+ return t.getValueAsRationals();
+ }
+
+ /**
+ * @see #getTagValue
+ */
+ public Rational[] getTagRationalValues(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagRationalValues(tagId, ifdId);
+ }
+
+ /**
+ * Checks whether a tag has a defined number of elements.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ * @return true if the tag has a defined number of elements.
+ */
+ public boolean isTagCountDefined(int tagId) {
+ int info = getTagInfo().get(tagId);
+ // No value in info can be zero, as all tags have a non-zero type
+ if (info == 0) {
+ return false;
+ }
+ return getComponentCountFromInfo(info) != ExifTag.SIZE_UNDEFINED;
+ }
+
+ /**
+ * Gets the defined number of elements for a tag.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ * @return the number of elements or {@link ExifTag#SIZE_UNDEFINED} if the
+ * tag or the number of elements is not defined.
+ */
+ public int getDefinedTagCount(int tagId) {
+ int info = getTagInfo().get(tagId);
+ if (info == 0) {
+ return ExifTag.SIZE_UNDEFINED;
+ }
+ return getComponentCountFromInfo(info);
+ }
+
+ /**
+ * Gets the number of elements for an ExifTag in a given IFD.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ * @param ifdId the IFD containing the ExifTag to check.
+ * @return the number of elements in the ExifTag, if the tag's size is
+ * undefined this will return the actual number of elements that is
+ * in the ExifTag's value.
+ */
+ public int getActualTagCount(int tagId, int ifdId) {
+ ExifTag t = getTag(tagId, ifdId);
+ if (t == null) {
+ return 0;
+ }
+ return t.getComponentCount();
+ }
+
+ /**
+ * Gets the default IFD for a tag.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ * @return the default IFD for a tag definition or {@link #IFD_NULL} if no
+ * definition exists.
+ */
+ public int getDefinedTagDefaultIfd(int tagId) {
+ int info = getTagInfo().get(tagId);
+ if (info == DEFINITION_NULL) {
+ return IFD_NULL;
+ }
+ return getTrueIfd(tagId);
+ }
+
+ /**
+ * Gets the defined type for a tag.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ * @return the type.
+ * @see ExifTag#getDataType()
+ */
+ public short getDefinedTagType(int tagId) {
+ int info = getTagInfo().get(tagId);
+ if (info == 0) {
+ return -1;
+ }
+ return getTypeFromInfo(info);
+ }
+
+ /**
+ * Returns true if tag TID is one of the following: {@link TAG_EXIF_IFD},
+ * {@link TAG_GPS_IFD}, {@link TAG_JPEG_INTERCHANGE_FORMAT},
+ * {@link TAG_STRIP_OFFSETS}, {@link TAG_INTEROPERABILITY_IFD}
+ * <p>
+ * Note: defining tags with these TID's is disallowed.
+ *
+ * @param tag a tag's TID (can be obtained from a defined tag constant with
+ * {@link #getTrueTagKey}).
+ * @return true if the TID is that of an offset tag.
+ */
+ protected static boolean isOffsetTag(short tag) {
+ return sOffsetTags.contains(tag);
+ }
+
+ /**
+ * Creates a tag for a defined tag constant in a given IFD if that IFD is
+ * allowed for the tag. This method will fail anytime the appropriate
+ * {@link ExifTag#setValue} for this tag's datatype would fail.
+ *
+ * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ * @param ifdId the IFD that the tag should be in.
+ * @param val the value of the tag to set.
+ * @return an ExifTag object or null if one could not be constructed.
+ * @see #buildTag
+ */
+ public ExifTag buildTag(int tagId, int ifdId, Object val) {
+ int info = getTagInfo().get(tagId);
+ if (info == 0 || val == null) {
+ return null;
+ }
+ short type = getTypeFromInfo(info);
+ int definedCount = getComponentCountFromInfo(info);
+ boolean hasDefinedCount = (definedCount != ExifTag.SIZE_UNDEFINED);
+ if (!ExifInterface.isIfdAllowed(info, ifdId)) {
+ return null;
+ }
+ ExifTag t = new ExifTag(getTrueTagKey(tagId), type, definedCount, ifdId, hasDefinedCount);
+ if (!t.setValue(val)) {
+ return null;
+ }
+ return t;
+ }
+
+ /**
+ * Creates a tag for a defined tag constant in the tag's default IFD.
+ *
+ * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ * @param val the tag's value.
+ * @return an ExifTag object.
+ */
+ public ExifTag buildTag(int tagId, Object val) {
+ int ifdId = getTrueIfd(tagId);
+ return buildTag(tagId, ifdId, val);
+ }
+
+ protected ExifTag buildUninitializedTag(int tagId) {
+ int info = getTagInfo().get(tagId);
+ if (info == 0) {
+ return null;
+ }
+ short type = getTypeFromInfo(info);
+ int definedCount = getComponentCountFromInfo(info);
+ boolean hasDefinedCount = (definedCount != ExifTag.SIZE_UNDEFINED);
+ int ifdId = getTrueIfd(tagId);
+ ExifTag t = new ExifTag(getTrueTagKey(tagId), type, definedCount, ifdId, hasDefinedCount);
+ return t;
+ }
+
+ /**
+ * Sets the value of an ExifTag if it exists in the given IFD. The value
+ * must be the correct type and length for that ExifTag.
+ *
+ * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ * @param ifdId the IFD that the ExifTag is in.
+ * @param val the value to set.
+ * @return true if success, false if the ExifTag doesn't exist or the value
+ * is the wrong type/length.
+ * @see #setTagValue
+ */
+ public boolean setTagValue(int tagId, int ifdId, Object val) {
+ ExifTag t = getTag(tagId, ifdId);
+ if (t == null) {
+ return false;
+ }
+ return t.setValue(val);
+ }
+
+ /**
+ * Sets the value of an ExifTag if it exists it's default IFD. The value
+ * must be the correct type and length for that ExifTag.
+ *
+ * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ * @param val the value to set.
+ * @return true if success, false if the ExifTag doesn't exist or the value
+ * is the wrong type/length.
+ */
+ public boolean setTagValue(int tagId, Object val) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return setTagValue(tagId, ifdId, val);
+ }
+
+ /**
+ * Puts an ExifTag into this ExifInterface object's tags, removing a
+ * previous ExifTag with the same TID and IFD. The IFD it is put into will
+ * be the one the tag was created with in {@link #buildTag}.
+ *
+ * @param tag an ExifTag to put into this ExifInterface's tags.
+ * @return the previous ExifTag with the same TID and IFD or null if none
+ * exists.
+ */
+ public ExifTag setTag(ExifTag tag) {
+ return mData.addTag(tag);
+ }
+
+ /**
+ * Puts a collection of ExifTags into this ExifInterface objects's tags. Any
+ * previous ExifTags with the same TID and IFDs will be removed.
+ *
+ * @param tags a Collection of ExifTags.
+ * @see #setTag
+ */
+ public void setTags(Collection<ExifTag> tags) {
+ for (ExifTag t : tags) {
+ setTag(t);
+ }
+ }
+
+ /**
+ * Removes the ExifTag for a tag constant from the given IFD.
+ *
+ * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ * @param ifdId the IFD of the ExifTag to remove.
+ */
+ public void deleteTag(int tagId, int ifdId) {
+ mData.removeTag(getTrueTagKey(tagId), ifdId);
+ }
+
+ /**
+ * Removes the ExifTag for a tag constant from that tag's default IFD.
+ *
+ * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ */
+ public void deleteTag(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ deleteTag(tagId, ifdId);
+ }
+
+ /**
+ * Creates a new tag definition in this ExifInterface object for a given TID
+ * and default IFD. Creating a definition with the same TID and default IFD
+ * as a previous definition will override it.
+ *
+ * @param tagId the TID for the tag.
+ * @param defaultIfd the default IFD for the tag.
+ * @param tagType the type of the tag (see {@link ExifTag#getDataType()}).
+ * @param defaultComponentCount the number of elements of this tag's type in
+ * the tags value.
+ * @param allowedIfds the IFD's this tag is allowed to be put in.
+ * @return the defined tag constant (e.g. {@link #TAG_IMAGE_WIDTH}) or
+ * {@link #TAG_NULL} if the definition could not be made.
+ */
+ public int setTagDefinition(short tagId, int defaultIfd, short tagType,
+ short defaultComponentCount, int[] allowedIfds) {
+ if (sBannedDefines.contains(tagId)) {
+ return TAG_NULL;
+ }
+ if (ExifTag.isValidType(tagType) && ExifTag.isValidIfd(defaultIfd)) {
+ int tagDef = defineTag(defaultIfd, tagId);
+ if (tagDef == TAG_NULL) {
+ return TAG_NULL;
+ }
+ int[] otherDefs = getTagDefinitionsForTagId(tagId);
+ SparseIntArray infos = getTagInfo();
+ // Make sure defaultIfd is in allowedIfds
+ boolean defaultCheck = false;
+ for (int i : allowedIfds) {
+ if (defaultIfd == i) {
+ defaultCheck = true;
+ }
+ if (!ExifTag.isValidIfd(i)) {
+ return TAG_NULL;
+ }
+ }
+ if (!defaultCheck) {
+ return TAG_NULL;
+ }
+
+ int ifdFlags = getFlagsFromAllowedIfds(allowedIfds);
+ // Make sure no identical tags can exist in allowedIfds
+ if (otherDefs != null) {
+ for (int def : otherDefs) {
+ int tagInfo = infos.get(def);
+ int allowedFlags = getAllowedIfdFlagsFromInfo(tagInfo);
+ if ((ifdFlags & allowedFlags) != 0) {
+ return TAG_NULL;
+ }
+ }
+ }
+ getTagInfo().put(tagDef, ifdFlags << 24 | (tagType << 16) | defaultComponentCount);
+ return tagDef;
+ }
+ return TAG_NULL;
+ }
+
+ protected int getTagDefinition(short tagId, int defaultIfd) {
+ return getTagInfo().get(defineTag(defaultIfd, tagId));
+ }
+
+ protected int[] getTagDefinitionsForTagId(short tagId) {
+ int[] ifds = IfdData.getIfds();
+ int[] defs = new int[ifds.length];
+ int counter = 0;
+ SparseIntArray infos = getTagInfo();
+ for (int i : ifds) {
+ int def = defineTag(i, tagId);
+ if (infos.get(def) != DEFINITION_NULL) {
+ defs[counter++] = def;
+ }
+ }
+ if (counter == 0) {
+ return null;
+ }
+
+ return Arrays.copyOfRange(defs, 0, counter);
+ }
+
+ protected int getTagDefinitionForTag(ExifTag tag) {
+ short type = tag.getDataType();
+ int count = tag.getComponentCount();
+ int ifd = tag.getIfd();
+ return getTagDefinitionForTag(tag.getTagId(), type, count, ifd);
+ }
+
+ protected int getTagDefinitionForTag(short tagId, short type, int count, int ifd) {
+ int[] defs = getTagDefinitionsForTagId(tagId);
+ if (defs == null) {
+ return TAG_NULL;
+ }
+ SparseIntArray infos = getTagInfo();
+ int ret = TAG_NULL;
+ for (int i : defs) {
+ int info = infos.get(i);
+ short def_type = getTypeFromInfo(info);
+ int def_count = getComponentCountFromInfo(info);
+ int[] def_ifds = getAllowedIfdsFromInfo(info);
+ boolean valid_ifd = false;
+ for (int j : def_ifds) {
+ if (j == ifd) {
+ valid_ifd = true;
+ break;
+ }
+ }
+ if (valid_ifd && type == def_type
+ && (count == def_count || def_count == ExifTag.SIZE_UNDEFINED)) {
+ ret = i;
+ break;
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Removes a tag definition for given defined tag constant.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}.
+ */
+ public void removeTagDefinition(int tagId) {
+ getTagInfo().delete(tagId);
+ }
+
+ /**
+ * Resets tag definitions to the default ones.
+ */
+ public void resetTagDefinitions() {
+ mTagInfo = null;
+ }
+
+ /**
+ * Returns the thumbnail from IFD1 as a bitmap, or null if none exists.
+ *
+ * @return the thumbnail as a bitmap.
+ */
+ public Bitmap getThumbnailBitmap() {
+ if (mData.hasCompressedThumbnail()) {
+ byte[] thumb = mData.getCompressedThumbnail();
+ return BitmapFactory.decodeByteArray(thumb, 0, thumb.length);
+ } else if (mData.hasUncompressedStrip()) {
+ // TODO: implement uncompressed
+ }
+ return null;
+ }
+
+ /**
+ * Returns the thumbnail from IFD1 as a byte array, or null if none exists.
+ * The bytes may either be an uncompressed strip as specified in the exif
+ * standard or a jpeg compressed image.
+ *
+ * @return the thumbnail as a byte array.
+ */
+ public byte[] getThumbnailBytes() {
+ if (mData.hasCompressedThumbnail()) {
+ return mData.getCompressedThumbnail();
+ } else if (mData.hasUncompressedStrip()) {
+ // TODO: implement this
+ }
+ return null;
+ }
+
+ /**
+ * Returns the thumbnail if it is jpeg compressed, or null if none exists.
+ *
+ * @return the thumbnail as a byte array.
+ */
+ public byte[] getThumbnail() {
+ return mData.getCompressedThumbnail();
+ }
+
+ /**
+ * Check if thumbnail is compressed.
+ *
+ * @return true if the thumbnail is compressed.
+ */
+ public boolean isThumbnailCompressed() {
+ return mData.hasCompressedThumbnail();
+ }
+
+ /**
+ * Check if thumbnail exists.
+ *
+ * @return true if a compressed thumbnail exists.
+ */
+ public boolean hasThumbnail() {
+ // TODO: add back in uncompressed strip
+ return mData.hasCompressedThumbnail();
+ }
+
+ // TODO: uncompressed thumbnail setters
+
+ /**
+ * Sets the thumbnail to be a jpeg compressed image. Clears any prior
+ * thumbnail.
+ *
+ * @param thumb a byte array containing a jpeg compressed image.
+ * @return true if the thumbnail was set.
+ */
+ public boolean setCompressedThumbnail(byte[] thumb) {
+ mData.clearThumbnailAndStrips();
+ mData.setCompressedThumbnail(thumb);
+ return true;
+ }
+
+ /**
+ * Sets the thumbnail to be a jpeg compressed bitmap. Clears any prior
+ * thumbnail.
+ *
+ * @param thumb a bitmap to compress to a jpeg thumbnail.
+ * @return true if the thumbnail was set.
+ */
+ public boolean setCompressedThumbnail(Bitmap thumb) {
+ ByteArrayOutputStream thumbnail = new ByteArrayOutputStream();
+ if (!thumb.compress(Bitmap.CompressFormat.JPEG, 90, thumbnail)) {
+ return false;
+ }
+ return setCompressedThumbnail(thumbnail.toByteArray());
+ }
+
+ /**
+ * Clears the compressed thumbnail if it exists.
+ */
+ public void removeCompressedThumbnail() {
+ mData.setCompressedThumbnail(null);
+ }
+
+ // Convenience methods:
+
+ /**
+ * Decodes the user comment tag into string as specified in the EXIF
+ * standard. Returns null if decoding failed.
+ */
+ public String getUserComment() {
+ return mData.getUserComment();
+ }
+
+ /**
+ * Returns the Orientation ExifTag value for a given number of degrees.
+ *
+ * @param degrees the amount an image is rotated in degrees.
+ */
+ public static short getOrientationValueForRotation(int degrees) {
+ degrees %= 360;
+ if (degrees < 0) {
+ degrees += 360;
+ }
+ if (degrees < 90) {
+ return Orientation.TOP_LEFT; // 0 degrees
+ } else if (degrees < 180) {
+ return Orientation.RIGHT_TOP; // 90 degrees cw
+ } else if (degrees < 270) {
+ return Orientation.BOTTOM_LEFT; // 180 degrees
+ } else {
+ return Orientation.RIGHT_BOTTOM; // 270 degrees cw
+ }
+ }
+
+ /**
+ * Returns the rotation degrees corresponding to an ExifTag Orientation
+ * value.
+ *
+ * @param orientation the ExifTag Orientation value.
+ */
+ public static int getRotationForOrientationValue(short orientation) {
+ switch (orientation) {
+ case Orientation.TOP_LEFT:
+ return 0;
+ case Orientation.RIGHT_TOP:
+ return 90;
+ case Orientation.BOTTOM_LEFT:
+ return 180;
+ case Orientation.RIGHT_BOTTOM:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Gets the double representation of the GPS latitude or longitude
+ * coordinate.
+ *
+ * @param coordinate an array of 3 Rationals representing the degrees,
+ * minutes, and seconds of the GPS location as defined in the
+ * exif specification.
+ * @param reference a GPS reference reperesented by a String containing "N",
+ * "S", "E", or "W".
+ * @return the GPS coordinate represented as degrees + minutes/60 +
+ * seconds/3600
+ */
+ public static double convertLatOrLongToDouble(Rational[] coordinate, String reference) {
+ try {
+ double degrees = coordinate[0].toDouble();
+ double minutes = coordinate[1].toDouble();
+ double seconds = coordinate[2].toDouble();
+ double result = degrees + minutes / 60.0 + seconds / 3600.0;
+ if ((reference.equals("S") || reference.equals("W"))) {
+ return -result;
+ }
+ return result;
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Gets the GPS latitude and longitude as a pair of doubles from this
+ * ExifInterface object's tags, or null if the necessary tags do not exist.
+ *
+ * @return an array of 2 doubles containing the latitude, and longitude
+ * respectively.
+ * @see #convertLatOrLongToDouble
+ */
+ public double[] getLatLongAsDoubles() {
+ Rational[] latitude = getTagRationalValues(TAG_GPS_LATITUDE);
+ String latitudeRef = getTagStringValue(TAG_GPS_LATITUDE_REF);
+ Rational[] longitude = getTagRationalValues(TAG_GPS_LONGITUDE);
+ String longitudeRef = getTagStringValue(TAG_GPS_LONGITUDE_REF);
+ if (latitude == null || longitude == null || latitudeRef == null || longitudeRef == null
+ || latitude.length < 3 || longitude.length < 3) {
+ return null;
+ }
+ double[] latLon = new double[2];
+ latLon[0] = convertLatOrLongToDouble(latitude, latitudeRef);
+ latLon[1] = convertLatOrLongToDouble(longitude, longitudeRef);
+ return latLon;
+ }
+
+ private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd";
+ private static final String DATETIME_FORMAT_STR = "yyyy:MM:dd kk:mm:ss";
+ private final DateFormat mDateTimeStampFormat = new SimpleDateFormat(DATETIME_FORMAT_STR);
+ private final DateFormat mGPSDateStampFormat = new SimpleDateFormat(GPS_DATE_FORMAT_STR);
+ private final Calendar mGPSTimeStampCalendar = Calendar
+ .getInstance(TimeZone.getTimeZone("UTC"));
+
+ /**
+ * Creates, formats, and sets the DateTimeStamp tag for one of:
+ * {@link #TAG_DATE_TIME}, {@link #TAG_DATE_TIME_DIGITIZED},
+ * {@link #TAG_DATE_TIME_ORIGINAL}.
+ *
+ * @param tagId one of the DateTimeStamp tags.
+ * @param timestamp a timestamp to format.
+ * @param timezone a TimeZone object.
+ * @return true if success, false if the tag could not be set.
+ */
+ public boolean addDateTimeStampTag(int tagId, long timestamp, TimeZone timezone) {
+ if (tagId == TAG_DATE_TIME || tagId == TAG_DATE_TIME_DIGITIZED
+ || tagId == TAG_DATE_TIME_ORIGINAL) {
+ mDateTimeStampFormat.setTimeZone(timezone);
+ ExifTag t = buildTag(tagId, mDateTimeStampFormat.format(timestamp));
+ if (t == null) {
+ return false;
+ }
+ setTag(t);
+ } else {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Creates and sets all to the GPS tags for a give latitude and longitude.
+ *
+ * @param latitude a GPS latitude coordinate.
+ * @param longitude a GPS longitude coordinate.
+ * @return true if success, false if they could not be created or set.
+ */
+ public boolean addGpsTags(double latitude, double longitude) {
+ ExifTag latTag = buildTag(TAG_GPS_LATITUDE, toExifLatLong(latitude));
+ ExifTag longTag = buildTag(TAG_GPS_LONGITUDE, toExifLatLong(longitude));
+ ExifTag latRefTag = buildTag(TAG_GPS_LATITUDE_REF,
+ latitude >= 0 ? ExifInterface.GpsLatitudeRef.NORTH
+ : ExifInterface.GpsLatitudeRef.SOUTH);
+ ExifTag longRefTag = buildTag(TAG_GPS_LONGITUDE_REF,
+ longitude >= 0 ? ExifInterface.GpsLongitudeRef.EAST
+ : ExifInterface.GpsLongitudeRef.WEST);
+ if (latTag == null || longTag == null || latRefTag == null || longRefTag == null) {
+ return false;
+ }
+ setTag(latTag);
+ setTag(longTag);
+ setTag(latRefTag);
+ setTag(longRefTag);
+ return true;
+ }
+
+ /**
+ * Creates and sets the GPS timestamp tag.
+ *
+ * @param timestamp a GPS timestamp.
+ * @return true if success, false if could not be created or set.
+ */
+ public boolean addGpsDateTimeStampTag(long timestamp) {
+ ExifTag t = buildTag(TAG_GPS_DATE_STAMP, mGPSDateStampFormat.format(timestamp));
+ if (t == null) {
+ return false;
+ }
+ setTag(t);
+ mGPSTimeStampCalendar.setTimeInMillis(timestamp);
+ t = buildTag(TAG_GPS_TIME_STAMP, new Rational[] {
+ new Rational(mGPSTimeStampCalendar.get(Calendar.HOUR_OF_DAY), 1),
+ new Rational(mGPSTimeStampCalendar.get(Calendar.MINUTE), 1),
+ new Rational(mGPSTimeStampCalendar.get(Calendar.SECOND), 1)
+ });
+ if (t == null) {
+ return false;
+ }
+ setTag(t);
+ return true;
+ }
+
+ private static Rational[] toExifLatLong(double value) {
+ // convert to the format dd/1 mm/1 ssss/100
+ value = Math.abs(value);
+ int degrees = (int) value;
+ value = (value - degrees) * 60;
+ int minutes = (int) value;
+ value = (value - minutes) * 6000;
+ int seconds = (int) value;
+ return new Rational[] {
+ new Rational(degrees, 1), new Rational(minutes, 1), new Rational(seconds, 100)
+ };
+ }
+
+ private void doExifStreamIO(InputStream is, OutputStream os) throws IOException {
+ byte[] buf = new byte[1024];
+ int ret = is.read(buf, 0, 1024);
+ while (ret != -1) {
+ os.write(buf, 0, ret);
+ ret = is.read(buf, 0, 1024);
+ }
+ }
+
+ protected static void closeSilently(Closeable c) {
+ if (c != null) {
+ try {
+ c.close();
+ } catch (Throwable e) {
+ // ignored
+ }
+ }
+ }
+
+ private SparseIntArray mTagInfo = null;
+
+ protected SparseIntArray getTagInfo() {
+ if (mTagInfo == null) {
+ mTagInfo = new SparseIntArray();
+ initTagInfo();
+ }
+ return mTagInfo;
+ }
+
+ private void initTagInfo() {
+ /**
+ * We put tag information in a 4-bytes integer. The first byte a bitmask
+ * representing the allowed IFDs of the tag, the second byte is the data
+ * type, and the last two byte are a short value indicating the default
+ * component count of this tag.
+ */
+ // IFD0 tags
+ int[] ifdAllowedIfds = {
+ IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1
+ };
+ int ifdFlags = getFlagsFromAllowedIfds(ifdAllowedIfds) << 24;
+ mTagInfo.put(ExifInterface.TAG_MAKE,
+ ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_IMAGE_WIDTH,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_IMAGE_LENGTH,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_BITS_PER_SAMPLE,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 3);
+ mTagInfo.put(ExifInterface.TAG_COMPRESSION,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_ORIENTATION, ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16
+ | 1);
+ mTagInfo.put(ExifInterface.TAG_SAMPLES_PER_PIXEL,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_PLANAR_CONFIGURATION,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_Y_CB_CR_POSITIONING,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_X_RESOLUTION,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_Y_RESOLUTION,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_RESOLUTION_UNIT,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_STRIP_OFFSETS,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_ROWS_PER_STRIP,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_STRIP_BYTE_COUNTS,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_TRANSFER_FUNCTION,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 3 * 256);
+ mTagInfo.put(ExifInterface.TAG_WHITE_POINT,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_PRIMARY_CHROMATICITIES,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 6);
+ mTagInfo.put(ExifInterface.TAG_Y_CB_CR_COEFFICIENTS,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 3);
+ mTagInfo.put(ExifInterface.TAG_REFERENCE_BLACK_WHITE,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 6);
+ mTagInfo.put(ExifInterface.TAG_DATE_TIME,
+ ifdFlags | ExifTag.TYPE_ASCII << 16 | 20);
+ mTagInfo.put(ExifInterface.TAG_IMAGE_DESCRIPTION,
+ ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_MAKE,
+ ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_MODEL,
+ ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_SOFTWARE,
+ ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_ARTIST,
+ ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_COPYRIGHT,
+ ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_EXIF_IFD,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_IFD,
+ ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ // IFD1 tags
+ int[] ifd1AllowedIfds = {
+ IfdId.TYPE_IFD_1
+ };
+ int ifdFlags1 = getFlagsFromAllowedIfds(ifd1AllowedIfds) << 24;
+ mTagInfo.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
+ ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+ ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ // Exif tags
+ int[] exifAllowedIfds = {
+ IfdId.TYPE_IFD_EXIF
+ };
+ int exifFlags = getFlagsFromAllowedIfds(exifAllowedIfds) << 24;
+ mTagInfo.put(ExifInterface.TAG_EXIF_VERSION,
+ exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4);
+ mTagInfo.put(ExifInterface.TAG_FLASHPIX_VERSION,
+ exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4);
+ mTagInfo.put(ExifInterface.TAG_COLOR_SPACE,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_COMPONENTS_CONFIGURATION,
+ exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4);
+ mTagInfo.put(ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_PIXEL_X_DIMENSION,
+ exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_PIXEL_Y_DIMENSION,
+ exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_MAKER_NOTE,
+ exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_USER_COMMENT,
+ exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_RELATED_SOUND_FILE,
+ exifFlags | ExifTag.TYPE_ASCII << 16 | 13);
+ mTagInfo.put(ExifInterface.TAG_DATE_TIME_ORIGINAL,
+ exifFlags | ExifTag.TYPE_ASCII << 16 | 20);
+ mTagInfo.put(ExifInterface.TAG_DATE_TIME_DIGITIZED,
+ exifFlags | ExifTag.TYPE_ASCII << 16 | 20);
+ mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME,
+ exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME_ORIGINAL,
+ exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME_DIGITIZED,
+ exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_IMAGE_UNIQUE_ID,
+ exifFlags | ExifTag.TYPE_ASCII << 16 | 33);
+ mTagInfo.put(ExifInterface.TAG_EXPOSURE_TIME,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_F_NUMBER,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_EXPOSURE_PROGRAM,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_SPECTRAL_SENSITIVITY,
+ exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_ISO_SPEED_RATINGS,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_OECF,
+ exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE,
+ exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_APERTURE_VALUE,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_BRIGHTNESS_VALUE,
+ exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
+ exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_MAX_APERTURE_VALUE,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_SUBJECT_DISTANCE,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_METERING_MODE,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_LIGHT_SOURCE,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_FLASH,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_FOCAL_LENGTH,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_SUBJECT_AREA,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_FLASH_ENERGY,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
+ exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_SUBJECT_LOCATION,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_EXPOSURE_INDEX,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_SENSING_METHOD,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_FILE_SOURCE,
+ exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_SCENE_TYPE,
+ exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_CFA_PATTERN,
+ exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_CUSTOM_RENDERED,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_EXPOSURE_MODE,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_WHITE_BALANCE,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_FOCAL_LENGTH_IN_35_MM_FILE,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_SCENE_CAPTURE_TYPE,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GAIN_CONTROL,
+ exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_CONTRAST,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_SATURATION,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_SHARPNESS,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
+ exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
+ exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_INTEROPERABILITY_IFD, exifFlags
+ | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ // GPS tag
+ int[] gpsAllowedIfds = {
+ IfdId.TYPE_IFD_GPS
+ };
+ int gpsFlags = getFlagsFromAllowedIfds(gpsAllowedIfds) << 24;
+ mTagInfo.put(ExifInterface.TAG_GPS_VERSION_ID,
+ gpsFlags | ExifTag.TYPE_UNSIGNED_BYTE << 16 | 4);
+ mTagInfo.put(ExifInterface.TAG_GPS_LATITUDE_REF,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_GPS_LONGITUDE_REF,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_GPS_LATITUDE,
+ gpsFlags | ExifTag.TYPE_RATIONAL << 16 | 3);
+ mTagInfo.put(ExifInterface.TAG_GPS_LONGITUDE,
+ gpsFlags | ExifTag.TYPE_RATIONAL << 16 | 3);
+ mTagInfo.put(ExifInterface.TAG_GPS_ALTITUDE_REF,
+ gpsFlags | ExifTag.TYPE_UNSIGNED_BYTE << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_ALTITUDE,
+ gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_TIME_STAMP,
+ gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 3);
+ mTagInfo.put(ExifInterface.TAG_GPS_SATTELLITES,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_GPS_STATUS,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_GPS_MEASURE_MODE,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_GPS_DOP,
+ gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_SPEED_REF,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_GPS_SPEED,
+ gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_TRACK_REF,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_GPS_TRACK,
+ gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_GPS_IMG_DIRECTION,
+ gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_MAP_DATUM,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_GPS_DEST_LATITUDE,
+ gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_DEST_BEARING_REF,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_GPS_DEST_BEARING,
+ gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | 2);
+ mTagInfo.put(ExifInterface.TAG_GPS_DEST_DISTANCE,
+ gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_PROCESSING_METHOD,
+ gpsFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_GPS_AREA_INFORMATION,
+ gpsFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED);
+ mTagInfo.put(ExifInterface.TAG_GPS_DATE_STAMP,
+ gpsFlags | ExifTag.TYPE_ASCII << 16 | 11);
+ mTagInfo.put(ExifInterface.TAG_GPS_DIFFERENTIAL,
+ gpsFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 11);
+ // Interoperability tag
+ int[] interopAllowedIfds = {
+ IfdId.TYPE_IFD_INTEROPERABILITY
+ };
+ int interopFlags = getFlagsFromAllowedIfds(interopAllowedIfds) << 24;
+ mTagInfo.put(TAG_INTEROPERABILITY_INDEX, interopFlags | ExifTag.TYPE_ASCII << 16
+ | ExifTag.SIZE_UNDEFINED);
+ }
+
+ protected static int getAllowedIfdFlagsFromInfo(int info) {
+ return info >>> 24;
+ }
+
+ protected static int[] getAllowedIfdsFromInfo(int info) {
+ int ifdFlags = getAllowedIfdFlagsFromInfo(info);
+ int[] ifds = IfdData.getIfds();
+ ArrayList<Integer> l = new ArrayList<Integer>();
+ for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+ int flag = (ifdFlags >> i) & 1;
+ if (flag == 1) {
+ l.add(ifds[i]);
+ }
+ }
+ if (l.size() <= 0) {
+ return null;
+ }
+ int[] ret = new int[l.size()];
+ int j = 0;
+ for (int i : l) {
+ ret[j++] = i;
+ }
+ return ret;
+ }
+
+ protected static boolean isIfdAllowed(int info, int ifd) {
+ int[] ifds = IfdData.getIfds();
+ int ifdFlags = getAllowedIfdFlagsFromInfo(info);
+ for (int i = 0; i < ifds.length; i++) {
+ if (ifd == ifds[i] && ((ifdFlags >> i) & 1) == 1) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected static int getFlagsFromAllowedIfds(int[] allowedIfds) {
+ if (allowedIfds == null || allowedIfds.length == 0) {
+ return 0;
+ }
+ int flags = 0;
+ int[] ifds = IfdData.getIfds();
+ for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+ for (int j : allowedIfds) {
+ if (ifds[i] == j) {
+ flags |= 1 << i;
+ break;
+ }
+ }
+ }
+ return flags;
+ }
+
+ protected static short getTypeFromInfo(int info) {
+ return (short) ((info >> 16) & 0x0ff);
+ }
+
+ protected static int getComponentCountFromInfo(int info) {
+ return info & 0x0ffff;
+ }
+
+}
diff --git a/src/com/android/gallery3d/exif/ExifInvalidFormatException.java b/src/com/android/gallery3d/exif/ExifInvalidFormatException.java
new file mode 100644
index 0000000..bf923ec
--- /dev/null
+++ b/src/com/android/gallery3d/exif/ExifInvalidFormatException.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.gallery3d.exif;
+
+public class ExifInvalidFormatException extends Exception {
+ public ExifInvalidFormatException(String meg) {
+ super(meg);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/gallery3d/exif/ExifModifier.java b/src/com/android/gallery3d/exif/ExifModifier.java
new file mode 100644
index 0000000..0531cba
--- /dev/null
+++ b/src/com/android/gallery3d/exif/ExifModifier.java
@@ -0,0 +1,195 @@
+/*
+ * 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.exif;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+
+class ExifModifier {
+ public static final String TAG = "ExifModifier";
+ public static final boolean DEBUG = false;
+ private final ByteBuffer mByteBuffer;
+ private final ExifData mTagToModified;
+ private final List<TagOffset> mTagOffsets = new ArrayList<TagOffset>();
+ private final ExifInterface mInterface;
+ private int mOffsetBase;
+
+ private static class TagOffset {
+ final int mOffset;
+ final ExifTag mTag;
+
+ TagOffset(ExifTag tag, int offset) {
+ mTag = tag;
+ mOffset = offset;
+ }
+ }
+
+ protected ExifModifier(ByteBuffer byteBuffer, ExifInterface iRef) throws IOException,
+ ExifInvalidFormatException {
+ mByteBuffer = byteBuffer;
+ mOffsetBase = byteBuffer.position();
+ mInterface = iRef;
+ InputStream is = null;
+ try {
+ is = new ByteBufferInputStream(byteBuffer);
+ // Do not require any IFD;
+ ExifParser parser = ExifParser.parse(is, mInterface);
+ mTagToModified = new ExifData(parser.getByteOrder());
+ mOffsetBase += parser.getTiffStartPosition();
+ mByteBuffer.position(0);
+ } finally {
+ ExifInterface.closeSilently(is);
+ }
+ }
+
+ protected ByteOrder getByteOrder() {
+ return mTagToModified.getByteOrder();
+ }
+
+ protected boolean commit() throws IOException, ExifInvalidFormatException {
+ InputStream is = null;
+ try {
+ is = new ByteBufferInputStream(mByteBuffer);
+ int flag = 0;
+ IfdData[] ifdDatas = new IfdData[] {
+ mTagToModified.getIfdData(IfdId.TYPE_IFD_0),
+ mTagToModified.getIfdData(IfdId.TYPE_IFD_1),
+ mTagToModified.getIfdData(IfdId.TYPE_IFD_EXIF),
+ mTagToModified.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY),
+ mTagToModified.getIfdData(IfdId.TYPE_IFD_GPS)
+ };
+
+ if (ifdDatas[IfdId.TYPE_IFD_0] != null) {
+ flag |= ExifParser.OPTION_IFD_0;
+ }
+ if (ifdDatas[IfdId.TYPE_IFD_1] != null) {
+ flag |= ExifParser.OPTION_IFD_1;
+ }
+ if (ifdDatas[IfdId.TYPE_IFD_EXIF] != null) {
+ flag |= ExifParser.OPTION_IFD_EXIF;
+ }
+ if (ifdDatas[IfdId.TYPE_IFD_GPS] != null) {
+ flag |= ExifParser.OPTION_IFD_GPS;
+ }
+ if (ifdDatas[IfdId.TYPE_IFD_INTEROPERABILITY] != null) {
+ flag |= ExifParser.OPTION_IFD_INTEROPERABILITY;
+ }
+
+ ExifParser parser = ExifParser.parse(is, flag, mInterface);
+ int event = parser.next();
+ IfdData currIfd = null;
+ while (event != ExifParser.EVENT_END) {
+ switch (event) {
+ case ExifParser.EVENT_START_OF_IFD:
+ currIfd = ifdDatas[parser.getCurrentIfd()];
+ if (currIfd == null) {
+ parser.skipRemainingTagsInCurrentIfd();
+ }
+ break;
+ case ExifParser.EVENT_NEW_TAG:
+ ExifTag oldTag = parser.getTag();
+ ExifTag newTag = currIfd.getTag(oldTag.getTagId());
+ if (newTag != null) {
+ if (newTag.getComponentCount() != oldTag.getComponentCount()
+ || newTag.getDataType() != oldTag.getDataType()) {
+ return false;
+ } else {
+ mTagOffsets.add(new TagOffset(newTag, oldTag.getOffset()));
+ currIfd.removeTag(oldTag.getTagId());
+ if (currIfd.getTagCount() == 0) {
+ parser.skipRemainingTagsInCurrentIfd();
+ }
+ }
+ }
+ break;
+ }
+ event = parser.next();
+ }
+ for (IfdData ifd : ifdDatas) {
+ if (ifd != null && ifd.getTagCount() > 0) {
+ return false;
+ }
+ }
+ modify();
+ } finally {
+ ExifInterface.closeSilently(is);
+ }
+ return true;
+ }
+
+ private void modify() {
+ mByteBuffer.order(getByteOrder());
+ for (TagOffset tagOffset : mTagOffsets) {
+ writeTagValue(tagOffset.mTag, tagOffset.mOffset);
+ }
+ }
+
+ private void writeTagValue(ExifTag tag, int offset) {
+ if (DEBUG) {
+ Log.v(TAG, "modifying tag to: \n" + tag.toString());
+ Log.v(TAG, "at offset: " + offset);
+ }
+ mByteBuffer.position(offset + mOffsetBase);
+ switch (tag.getDataType()) {
+ case ExifTag.TYPE_ASCII:
+ byte buf[] = tag.getStringByte();
+ if (buf.length == tag.getComponentCount()) {
+ buf[buf.length - 1] = 0;
+ mByteBuffer.put(buf);
+ } else {
+ mByteBuffer.put(buf);
+ mByteBuffer.put((byte) 0);
+ }
+ break;
+ case ExifTag.TYPE_LONG:
+ case ExifTag.TYPE_UNSIGNED_LONG:
+ for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+ mByteBuffer.putInt((int) tag.getValueAt(i));
+ }
+ break;
+ case ExifTag.TYPE_RATIONAL:
+ case ExifTag.TYPE_UNSIGNED_RATIONAL:
+ for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+ Rational v = tag.getRational(i);
+ mByteBuffer.putInt((int) v.getNumerator());
+ mByteBuffer.putInt((int) v.getDenominator());
+ }
+ break;
+ case ExifTag.TYPE_UNDEFINED:
+ case ExifTag.TYPE_UNSIGNED_BYTE:
+ buf = new byte[tag.getComponentCount()];
+ tag.getBytes(buf);
+ mByteBuffer.put(buf);
+ break;
+ case ExifTag.TYPE_UNSIGNED_SHORT:
+ for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+ mByteBuffer.putShort((short) tag.getValueAt(i));
+ }
+ break;
+ }
+ }
+
+ public void modifyTag(ExifTag tag) {
+ mTagToModified.addTag(tag);
+ }
+}
diff --git a/src/com/android/gallery3d/exif/ExifOutputStream.java b/src/com/android/gallery3d/exif/ExifOutputStream.java
new file mode 100644
index 0000000..7ca05f2
--- /dev/null
+++ b/src/com/android/gallery3d/exif/ExifOutputStream.java
@@ -0,0 +1,518 @@
+/*
+ * 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.exif;
+
+import android.util.Log;
+
+import java.io.BufferedOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+
+/**
+ * This class provides a way to replace the Exif header of a JPEG image.
+ * <p>
+ * Below is an example of writing EXIF data into a file
+ *
+ * <pre>
+ * public static void writeExif(byte[] jpeg, ExifData exif, String path) {
+ * OutputStream os = null;
+ * try {
+ * os = new FileOutputStream(path);
+ * ExifOutputStream eos = new ExifOutputStream(os);
+ * // Set the exif header
+ * eos.setExifData(exif);
+ * // Write the original jpeg out, the header will be add into the file.
+ * eos.write(jpeg);
+ * } catch (FileNotFoundException e) {
+ * e.printStackTrace();
+ * } catch (IOException e) {
+ * e.printStackTrace();
+ * } finally {
+ * if (os != null) {
+ * try {
+ * os.close();
+ * } catch (IOException e) {
+ * e.printStackTrace();
+ * }
+ * }
+ * }
+ * }
+ * </pre>
+ */
+class ExifOutputStream extends FilterOutputStream {
+ private static final String TAG = "ExifOutputStream";
+ private static final boolean DEBUG = false;
+ private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb
+
+ private static final int STATE_SOI = 0;
+ private static final int STATE_FRAME_HEADER = 1;
+ private static final int STATE_JPEG_DATA = 2;
+
+ private static final int EXIF_HEADER = 0x45786966;
+ private static final short TIFF_HEADER = 0x002A;
+ private static final short TIFF_BIG_ENDIAN = 0x4d4d;
+ private static final short TIFF_LITTLE_ENDIAN = 0x4949;
+ private static final short TAG_SIZE = 12;
+ private static final short TIFF_HEADER_SIZE = 8;
+ private static final int MAX_EXIF_SIZE = 65535;
+
+ private ExifData mExifData;
+ private int mState = STATE_SOI;
+ private int mByteToSkip;
+ private int mByteToCopy;
+ private byte[] mSingleByteArray = new byte[1];
+ private ByteBuffer mBuffer = ByteBuffer.allocate(4);
+ private final ExifInterface mInterface;
+
+ protected ExifOutputStream(OutputStream ou, ExifInterface iRef) {
+ super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE));
+ mInterface = iRef;
+ }
+
+ /**
+ * Sets the ExifData to be written into the JPEG file. Should be called
+ * before writing image data.
+ */
+ protected void setExifData(ExifData exifData) {
+ mExifData = exifData;
+ }
+
+ /**
+ * Gets the Exif header to be written into the JPEF file.
+ */
+ protected ExifData getExifData() {
+ return mExifData;
+ }
+
+ private int requestByteToBuffer(int requestByteCount, byte[] buffer
+ , int offset, int length) {
+ int byteNeeded = requestByteCount - mBuffer.position();
+ int byteToRead = length > byteNeeded ? byteNeeded : length;
+ mBuffer.put(buffer, offset, byteToRead);
+ return byteToRead;
+ }
+
+ /**
+ * Writes the image out. The input data should be a valid JPEG format. After
+ * writing, it's Exif header will be replaced by the given header.
+ */
+ @Override
+ public void write(byte[] buffer, int offset, int length) throws IOException {
+ while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
+ && length > 0) {
+ if (mByteToSkip > 0) {
+ int byteToProcess = length > mByteToSkip ? mByteToSkip : length;
+ length -= byteToProcess;
+ mByteToSkip -= byteToProcess;
+ offset += byteToProcess;
+ }
+ if (mByteToCopy > 0) {
+ int byteToProcess = length > mByteToCopy ? mByteToCopy : length;
+ out.write(buffer, offset, byteToProcess);
+ length -= byteToProcess;
+ mByteToCopy -= byteToProcess;
+ offset += byteToProcess;
+ }
+ if (length == 0) {
+ return;
+ }
+ switch (mState) {
+ case STATE_SOI:
+ int byteRead = requestByteToBuffer(2, buffer, offset, length);
+ offset += byteRead;
+ length -= byteRead;
+ if (mBuffer.position() < 2) {
+ return;
+ }
+ mBuffer.rewind();
+ if (mBuffer.getShort() != JpegHeader.SOI) {
+ throw new IOException("Not a valid jpeg image, cannot write exif");
+ }
+ out.write(mBuffer.array(), 0, 2);
+ mState = STATE_FRAME_HEADER;
+ mBuffer.rewind();
+ writeExifData();
+ break;
+ case STATE_FRAME_HEADER:
+ // We ignore the APP1 segment and copy all other segments
+ // until SOF tag.
+ byteRead = requestByteToBuffer(4, buffer, offset, length);
+ offset += byteRead;
+ length -= byteRead;
+ // Check if this image data doesn't contain SOF.
+ if (mBuffer.position() == 2) {
+ short tag = mBuffer.getShort();
+ if (tag == JpegHeader.EOI) {
+ out.write(mBuffer.array(), 0, 2);
+ mBuffer.rewind();
+ }
+ }
+ if (mBuffer.position() < 4) {
+ return;
+ }
+ mBuffer.rewind();
+ short marker = mBuffer.getShort();
+ if (marker == JpegHeader.APP1) {
+ mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2;
+ mState = STATE_JPEG_DATA;
+ } else if (!JpegHeader.isSofMarker(marker)) {
+ out.write(mBuffer.array(), 0, 4);
+ mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
+ } else {
+ out.write(mBuffer.array(), 0, 4);
+ mState = STATE_JPEG_DATA;
+ }
+ mBuffer.rewind();
+ }
+ }
+ if (length > 0) {
+ out.write(buffer, offset, length);
+ }
+ }
+
+ /**
+ * Writes the one bytes out. The input data should be a valid JPEG format.
+ * After writing, it's Exif header will be replaced by the given header.
+ */
+ @Override
+ public void write(int oneByte) throws IOException {
+ mSingleByteArray[0] = (byte) (0xff & oneByte);
+ write(mSingleByteArray);
+ }
+
+ /**
+ * Equivalent to calling write(buffer, 0, buffer.length).
+ */
+ @Override
+ public void write(byte[] buffer) throws IOException {
+ write(buffer, 0, buffer.length);
+ }
+
+ private void writeExifData() throws IOException {
+ if (mExifData == null) {
+ return;
+ }
+ if (DEBUG) {
+ Log.v(TAG, "Writing exif data...");
+ }
+ ArrayList<ExifTag> nullTags = stripNullValueTags(mExifData);
+ createRequiredIfdAndTag();
+ int exifSize = calculateAllOffset();
+ if (exifSize + 8 > MAX_EXIF_SIZE) {
+ throw new IOException("Exif header is too large (>64Kb)");
+ }
+ OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out);
+ dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+ dataOutputStream.writeShort(JpegHeader.APP1);
+ dataOutputStream.writeShort((short) (exifSize + 8));
+ dataOutputStream.writeInt(EXIF_HEADER);
+ dataOutputStream.writeShort((short) 0x0000);
+ if (mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN) {
+ dataOutputStream.writeShort(TIFF_BIG_ENDIAN);
+ } else {
+ dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN);
+ }
+ dataOutputStream.setByteOrder(mExifData.getByteOrder());
+ dataOutputStream.writeShort(TIFF_HEADER);
+ dataOutputStream.writeInt(8);
+ writeAllTags(dataOutputStream);
+ writeThumbnail(dataOutputStream);
+ for (ExifTag t : nullTags) {
+ mExifData.addTag(t);
+ }
+ }
+
+ private ArrayList<ExifTag> stripNullValueTags(ExifData data) {
+ ArrayList<ExifTag> nullTags = new ArrayList<ExifTag>();
+ for(ExifTag t : data.getAllTags()) {
+ if (t.getValue() == null && !ExifInterface.isOffsetTag(t.getTagId())) {
+ data.removeTag(t.getTagId(), t.getIfd());
+ nullTags.add(t);
+ }
+ }
+ return nullTags;
+ }
+
+ private void writeThumbnail(OrderedDataOutputStream dataOutputStream) throws IOException {
+ if (mExifData.hasCompressedThumbnail()) {
+ dataOutputStream.write(mExifData.getCompressedThumbnail());
+ } else if (mExifData.hasUncompressedStrip()) {
+ for (int i = 0; i < mExifData.getStripCount(); i++) {
+ dataOutputStream.write(mExifData.getStrip(i));
+ }
+ }
+ }
+
+ private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException {
+ writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_0), dataOutputStream);
+ writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_EXIF), dataOutputStream);
+ IfdData interoperabilityIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
+ if (interoperabilityIfd != null) {
+ writeIfd(interoperabilityIfd, dataOutputStream);
+ }
+ IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
+ if (gpsIfd != null) {
+ writeIfd(gpsIfd, dataOutputStream);
+ }
+ IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
+ if (ifd1 != null) {
+ writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream);
+ }
+ }
+
+ private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream)
+ throws IOException {
+ ExifTag[] tags = ifd.getAllTags();
+ dataOutputStream.writeShort((short) tags.length);
+ for (ExifTag tag : tags) {
+ dataOutputStream.writeShort(tag.getTagId());
+ dataOutputStream.writeShort(tag.getDataType());
+ dataOutputStream.writeInt(tag.getComponentCount());
+ if (DEBUG) {
+ Log.v(TAG, "\n" + tag.toString());
+ }
+ if (tag.getDataSize() > 4) {
+ dataOutputStream.writeInt(tag.getOffset());
+ } else {
+ ExifOutputStream.writeTagValue(tag, dataOutputStream);
+ for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) {
+ dataOutputStream.write(0);
+ }
+ }
+ }
+ dataOutputStream.writeInt(ifd.getOffsetToNextIfd());
+ for (ExifTag tag : tags) {
+ if (tag.getDataSize() > 4) {
+ ExifOutputStream.writeTagValue(tag, dataOutputStream);
+ }
+ }
+ }
+
+ private int calculateOffsetOfIfd(IfdData ifd, int offset) {
+ offset += 2 + ifd.getTagCount() * TAG_SIZE + 4;
+ ExifTag[] tags = ifd.getAllTags();
+ for (ExifTag tag : tags) {
+ if (tag.getDataSize() > 4) {
+ tag.setOffset(offset);
+ offset += tag.getDataSize();
+ }
+ }
+ return offset;
+ }
+
+ private void createRequiredIfdAndTag() throws IOException {
+ // IFD0 is required for all file
+ IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
+ if (ifd0 == null) {
+ ifd0 = new IfdData(IfdId.TYPE_IFD_0);
+ mExifData.addIfdData(ifd0);
+ }
+ ExifTag exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD);
+ if (exifOffsetTag == null) {
+ throw new IOException("No definition for crucial exif tag: "
+ + ExifInterface.TAG_EXIF_IFD);
+ }
+ ifd0.setTag(exifOffsetTag);
+
+ // Exif IFD is required for all files.
+ IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
+ if (exifIfd == null) {
+ exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF);
+ mExifData.addIfdData(exifIfd);
+ }
+
+ // GPS IFD
+ IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
+ if (gpsIfd != null) {
+ ExifTag gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD);
+ if (gpsOffsetTag == null) {
+ throw new IOException("No definition for crucial exif tag: "
+ + ExifInterface.TAG_GPS_IFD);
+ }
+ ifd0.setTag(gpsOffsetTag);
+ }
+
+ // Interoperability IFD
+ IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
+ if (interIfd != null) {
+ ExifTag interOffsetTag = mInterface
+ .buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD);
+ if (interOffsetTag == null) {
+ throw new IOException("No definition for crucial exif tag: "
+ + ExifInterface.TAG_INTEROPERABILITY_IFD);
+ }
+ exifIfd.setTag(interOffsetTag);
+ }
+
+ IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
+
+ // thumbnail
+ if (mExifData.hasCompressedThumbnail()) {
+
+ if (ifd1 == null) {
+ ifd1 = new IfdData(IfdId.TYPE_IFD_1);
+ mExifData.addIfdData(ifd1);
+ }
+
+ ExifTag offsetTag = mInterface
+ .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
+ if (offsetTag == null) {
+ throw new IOException("No definition for crucial exif tag: "
+ + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
+ }
+
+ ifd1.setTag(offsetTag);
+ ExifTag lengthTag = mInterface
+ .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+ if (lengthTag == null) {
+ throw new IOException("No definition for crucial exif tag: "
+ + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+ }
+
+ lengthTag.setValue(mExifData.getCompressedThumbnail().length);
+ ifd1.setTag(lengthTag);
+
+ // Get rid of tags for uncompressed if they exist.
+ ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
+ ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
+ } else if (mExifData.hasUncompressedStrip()) {
+ if (ifd1 == null) {
+ ifd1 = new IfdData(IfdId.TYPE_IFD_1);
+ mExifData.addIfdData(ifd1);
+ }
+ int stripCount = mExifData.getStripCount();
+ ExifTag offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS);
+ if (offsetTag == null) {
+ throw new IOException("No definition for crucial exif tag: "
+ + ExifInterface.TAG_STRIP_OFFSETS);
+ }
+ ExifTag lengthTag = mInterface
+ .buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS);
+ if (lengthTag == null) {
+ throw new IOException("No definition for crucial exif tag: "
+ + ExifInterface.TAG_STRIP_BYTE_COUNTS);
+ }
+ long[] lengths = new long[stripCount];
+ for (int i = 0; i < mExifData.getStripCount(); i++) {
+ lengths[i] = mExifData.getStrip(i).length;
+ }
+ lengthTag.setValue(lengths);
+ ifd1.setTag(offsetTag);
+ ifd1.setTag(lengthTag);
+ // Get rid of tags for compressed if they exist.
+ ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
+ ifd1.removeTag(ExifInterface
+ .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
+ } else if (ifd1 != null) {
+ // Get rid of offset and length tags if there is no thumbnail.
+ ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
+ ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
+ ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
+ ifd1.removeTag(ExifInterface
+ .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
+ }
+ }
+
+ private int calculateAllOffset() {
+ int offset = TIFF_HEADER_SIZE;
+ IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
+ offset = calculateOffsetOfIfd(ifd0, offset);
+ ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD)).setValue(offset);
+
+ IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
+ offset = calculateOffsetOfIfd(exifIfd, offset);
+
+ IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
+ if (interIfd != null) {
+ exifIfd.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD))
+ .setValue(offset);
+ offset = calculateOffsetOfIfd(interIfd, offset);
+ }
+
+ IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
+ if (gpsIfd != null) {
+ ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)).setValue(offset);
+ offset = calculateOffsetOfIfd(gpsIfd, offset);
+ }
+
+ IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
+ if (ifd1 != null) {
+ ifd0.setOffsetToNextIfd(offset);
+ offset = calculateOffsetOfIfd(ifd1, offset);
+ }
+
+ // thumbnail
+ if (mExifData.hasCompressedThumbnail()) {
+ ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
+ .setValue(offset);
+ offset += mExifData.getCompressedThumbnail().length;
+ } else if (mExifData.hasUncompressedStrip()) {
+ int stripCount = mExifData.getStripCount();
+ long[] offsets = new long[stripCount];
+ for (int i = 0; i < mExifData.getStripCount(); i++) {
+ offsets[i] = offset;
+ offset += mExifData.getStrip(i).length;
+ }
+ ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)).setValue(
+ offsets);
+ }
+ return offset;
+ }
+
+ static void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)
+ throws IOException {
+ switch (tag.getDataType()) {
+ case ExifTag.TYPE_ASCII:
+ byte buf[] = tag.getStringByte();
+ if (buf.length == tag.getComponentCount()) {
+ buf[buf.length - 1] = 0;
+ dataOutputStream.write(buf);
+ } else {
+ dataOutputStream.write(buf);
+ dataOutputStream.write(0);
+ }
+ break;
+ case ExifTag.TYPE_LONG:
+ case ExifTag.TYPE_UNSIGNED_LONG:
+ for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+ dataOutputStream.writeInt((int) tag.getValueAt(i));
+ }
+ break;
+ case ExifTag.TYPE_RATIONAL:
+ case ExifTag.TYPE_UNSIGNED_RATIONAL:
+ for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+ dataOutputStream.writeRational(tag.getRational(i));
+ }
+ break;
+ case ExifTag.TYPE_UNDEFINED:
+ case ExifTag.TYPE_UNSIGNED_BYTE:
+ buf = new byte[tag.getComponentCount()];
+ tag.getBytes(buf);
+ dataOutputStream.write(buf);
+ break;
+ case ExifTag.TYPE_UNSIGNED_SHORT:
+ for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
+ dataOutputStream.writeShort((short) tag.getValueAt(i));
+ }
+ break;
+ }
+ }
+}
diff --git a/src/com/android/gallery3d/exif/ExifParser.java b/src/com/android/gallery3d/exif/ExifParser.java
new file mode 100644
index 0000000..5467d42
--- /dev/null
+++ b/src/com/android/gallery3d/exif/ExifParser.java
@@ -0,0 +1,916 @@
+/*
+ * 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.exif;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * This class provides a low-level EXIF parsing API. Given a JPEG format
+ * InputStream, the caller can request which IFD's to read via
+ * {@link #parse(InputStream, int)} with given options.
+ * <p>
+ * Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the
+ * parser.
+ *
+ * <pre>
+ * void parse() {
+ * ExifParser parser = ExifParser.parse(mImageInputStream,
+ * ExifParser.OPTION_IFD_0 | ExifParser.OPTIONS_IFD_EXIF);
+ * int event = parser.next();
+ * while (event != ExifParser.EVENT_END) {
+ * switch (event) {
+ * case ExifParser.EVENT_START_OF_IFD:
+ * break;
+ * case ExifParser.EVENT_NEW_TAG:
+ * ExifTag tag = parser.getTag();
+ * if (!tag.hasValue()) {
+ * parser.registerForTagValue(tag);
+ * } else {
+ * processTag(tag);
+ * }
+ * break;
+ * case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+ * tag = parser.getTag();
+ * if (tag.getDataType() != ExifTag.TYPE_UNDEFINED) {
+ * processTag(tag);
+ * }
+ * break;
+ * }
+ * event = parser.next();
+ * }
+ * }
+ *
+ * void processTag(ExifTag tag) {
+ * // process the tag as you like.
+ * }
+ * </pre>
+ */
+class ExifParser {
+ private static final boolean LOGV = false;
+ private static final String TAG = "ExifParser";
+ /**
+ * When the parser reaches a new IFD area. Call {@link #getCurrentIfd()} to
+ * know which IFD we are in.
+ */
+ public static final int EVENT_START_OF_IFD = 0;
+ /**
+ * When the parser reaches a new tag. Call {@link #getTag()}to get the
+ * corresponding tag.
+ */
+ public static final int EVENT_NEW_TAG = 1;
+ /**
+ * When the parser reaches the value area of tag that is registered by
+ * {@link #registerForTagValue(ExifTag)} previously. Call {@link #getTag()}
+ * to get the corresponding tag.
+ */
+ public static final int EVENT_VALUE_OF_REGISTERED_TAG = 2;
+
+ /**
+ * When the parser reaches the compressed image area.
+ */
+ public static final int EVENT_COMPRESSED_IMAGE = 3;
+ /**
+ * When the parser reaches the uncompressed image strip. Call
+ * {@link #getStripIndex()} to get the index of the strip.
+ *
+ * @see #getStripIndex()
+ * @see #getStripCount()
+ */
+ public static final int EVENT_UNCOMPRESSED_STRIP = 4;
+ /**
+ * When there is nothing more to parse.
+ */
+ public static final int EVENT_END = 5;
+
+ /**
+ * Option bit to request to parse IFD0.
+ */
+ public static final int OPTION_IFD_0 = 1 << 0;
+ /**
+ * Option bit to request to parse IFD1.
+ */
+ public static final int OPTION_IFD_1 = 1 << 1;
+ /**
+ * Option bit to request to parse Exif-IFD.
+ */
+ public static final int OPTION_IFD_EXIF = 1 << 2;
+ /**
+ * Option bit to request to parse GPS-IFD.
+ */
+ public static final int OPTION_IFD_GPS = 1 << 3;
+ /**
+ * Option bit to request to parse Interoperability-IFD.
+ */
+ public static final int OPTION_IFD_INTEROPERABILITY = 1 << 4;
+ /**
+ * Option bit to request to parse thumbnail.
+ */
+ public static final int OPTION_THUMBNAIL = 1 << 5;
+
+ protected static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif"
+ protected static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in APP1
+
+ // TIFF header
+ protected static final short LITTLE_ENDIAN_TAG = (short) 0x4949; // "II"
+ protected static final short BIG_ENDIAN_TAG = (short) 0x4d4d; // "MM"
+ protected static final short TIFF_HEADER_TAIL = 0x002A;
+
+ protected static final int TAG_SIZE = 12;
+ protected static final int OFFSET_SIZE = 2;
+
+ private static final Charset US_ASCII = Charset.forName("US-ASCII");
+
+ protected static final int DEFAULT_IFD0_OFFSET = 8;
+
+ private final CountedDataInputStream mTiffStream;
+ private final int mOptions;
+ private int mIfdStartOffset = 0;
+ private int mNumOfTagInIfd = 0;
+ private int mIfdType;
+ private ExifTag mTag;
+ private ImageEvent mImageEvent;
+ private int mStripCount;
+ private ExifTag mStripSizeTag;
+ private ExifTag mJpegSizeTag;
+ private boolean mNeedToParseOffsetsInCurrentIfd;
+ private boolean mContainExifData = false;
+ private int mApp1End;
+ private int mOffsetToApp1EndFromSOF = 0;
+ private byte[] mDataAboveIfd0;
+ private int mIfd0Position;
+ private int mTiffStartPosition;
+ private final ExifInterface mInterface;
+
+ private static final short TAG_EXIF_IFD = ExifInterface
+ .getTrueTagKey(ExifInterface.TAG_EXIF_IFD);
+ private static final short TAG_GPS_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD);
+ private static final short TAG_INTEROPERABILITY_IFD = ExifInterface
+ .getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD);
+ private static final short TAG_JPEG_INTERCHANGE_FORMAT = ExifInterface
+ .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
+ private static final short TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = ExifInterface
+ .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+ private static final short TAG_STRIP_OFFSETS = ExifInterface
+ .getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS);
+ private static final short TAG_STRIP_BYTE_COUNTS = ExifInterface
+ .getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS);
+
+ private final TreeMap<Integer, Object> mCorrespondingEvent = new TreeMap<Integer, Object>();
+
+ private boolean isIfdRequested(int ifdType) {
+ switch (ifdType) {
+ case IfdId.TYPE_IFD_0:
+ return (mOptions & OPTION_IFD_0) != 0;
+ case IfdId.TYPE_IFD_1:
+ return (mOptions & OPTION_IFD_1) != 0;
+ case IfdId.TYPE_IFD_EXIF:
+ return (mOptions & OPTION_IFD_EXIF) != 0;
+ case IfdId.TYPE_IFD_GPS:
+ return (mOptions & OPTION_IFD_GPS) != 0;
+ case IfdId.TYPE_IFD_INTEROPERABILITY:
+ return (mOptions & OPTION_IFD_INTEROPERABILITY) != 0;
+ }
+ return false;
+ }
+
+ private boolean isThumbnailRequested() {
+ return (mOptions & OPTION_THUMBNAIL) != 0;
+ }
+
+ private ExifParser(InputStream inputStream, int options, ExifInterface iRef)
+ throws IOException, ExifInvalidFormatException {
+ if (inputStream == null) {
+ throw new IOException("Null argument inputStream to ExifParser");
+ }
+ if (LOGV) {
+ Log.v(TAG, "Reading exif...");
+ }
+ mInterface = iRef;
+ mContainExifData = seekTiffData(inputStream);
+ mTiffStream = new CountedDataInputStream(inputStream);
+ mOptions = options;
+ if (!mContainExifData) {
+ return;
+ }
+
+ parseTiffHeader();
+ long offset = mTiffStream.readUnsignedInt();
+ if (offset > Integer.MAX_VALUE) {
+ throw new ExifInvalidFormatException("Invalid offset " + offset);
+ }
+ mIfd0Position = (int) offset;
+ mIfdType = IfdId.TYPE_IFD_0;
+ if (isIfdRequested(IfdId.TYPE_IFD_0) || needToParseOffsetsInCurrentIfd()) {
+ registerIfd(IfdId.TYPE_IFD_0, offset);
+ if (offset != DEFAULT_IFD0_OFFSET) {
+ mDataAboveIfd0 = new byte[(int) offset - DEFAULT_IFD0_OFFSET];
+ read(mDataAboveIfd0);
+ }
+ }
+ }
+
+ /**
+ * Parses the the given InputStream with the given options
+ *
+ * @exception IOException
+ * @exception ExifInvalidFormatException
+ */
+ protected static ExifParser parse(InputStream inputStream, int options, ExifInterface iRef)
+ throws IOException, ExifInvalidFormatException {
+ return new ExifParser(inputStream, options, iRef);
+ }
+
+ /**
+ * Parses the the given InputStream with default options; that is, every IFD
+ * and thumbnaill will be parsed.
+ *
+ * @exception IOException
+ * @exception ExifInvalidFormatException
+ * @see #parse(InputStream, int)
+ */
+ protected static ExifParser parse(InputStream inputStream, ExifInterface iRef)
+ throws IOException, ExifInvalidFormatException {
+ return new ExifParser(inputStream, OPTION_IFD_0 | OPTION_IFD_1
+ | OPTION_IFD_EXIF | OPTION_IFD_GPS | OPTION_IFD_INTEROPERABILITY
+ | OPTION_THUMBNAIL, iRef);
+ }
+
+ /**
+ * Moves the parser forward and returns the next parsing event
+ *
+ * @exception IOException
+ * @exception ExifInvalidFormatException
+ * @see #EVENT_START_OF_IFD
+ * @see #EVENT_NEW_TAG
+ * @see #EVENT_VALUE_OF_REGISTERED_TAG
+ * @see #EVENT_COMPRESSED_IMAGE
+ * @see #EVENT_UNCOMPRESSED_STRIP
+ * @see #EVENT_END
+ */
+ protected int next() throws IOException, ExifInvalidFormatException {
+ if (!mContainExifData) {
+ return EVENT_END;
+ }
+ int offset = mTiffStream.getReadByteCount();
+ int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
+ if (offset < endOfTags) {
+ mTag = readTag();
+ if (mTag == null) {
+ return next();
+ }
+ if (mNeedToParseOffsetsInCurrentIfd) {
+ checkOffsetOrImageTag(mTag);
+ }
+ return EVENT_NEW_TAG;
+ } else if (offset == endOfTags) {
+ // There is a link to ifd1 at the end of ifd0
+ if (mIfdType == IfdId.TYPE_IFD_0) {
+ long ifdOffset = readUnsignedLong();
+ if (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested()) {
+ if (ifdOffset != 0) {
+ registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
+ }
+ }
+ } else {
+ int offsetSize = 4;
+ // Some camera models use invalid length of the offset
+ if (mCorrespondingEvent.size() > 0) {
+ offsetSize = mCorrespondingEvent.firstEntry().getKey() -
+ mTiffStream.getReadByteCount();
+ }
+ if (offsetSize < 4) {
+ Log.w(TAG, "Invalid size of link to next IFD: " + offsetSize);
+ } else {
+ long ifdOffset = readUnsignedLong();
+ if (ifdOffset != 0) {
+ Log.w(TAG, "Invalid link to next IFD: " + ifdOffset);
+ }
+ }
+ }
+ }
+ while (mCorrespondingEvent.size() != 0) {
+ Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
+ Object event = entry.getValue();
+ try {
+ skipTo(entry.getKey());
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to skip to data at: " + entry.getKey() +
+ " for " + event.getClass().getName() + ", the file may be broken.");
+ continue;
+ }
+ if (event instanceof IfdEvent) {
+ mIfdType = ((IfdEvent) event).ifd;
+ mNumOfTagInIfd = mTiffStream.readUnsignedShort();
+ mIfdStartOffset = entry.getKey();
+
+ if (mNumOfTagInIfd * TAG_SIZE + mIfdStartOffset + OFFSET_SIZE > mApp1End) {
+ Log.w(TAG, "Invalid size of IFD " + mIfdType);
+ return EVENT_END;
+ }
+
+ mNeedToParseOffsetsInCurrentIfd = needToParseOffsetsInCurrentIfd();
+ if (((IfdEvent) event).isRequested) {
+ return EVENT_START_OF_IFD;
+ } else {
+ skipRemainingTagsInCurrentIfd();
+ }
+ } else if (event instanceof ImageEvent) {
+ mImageEvent = (ImageEvent) event;
+ return mImageEvent.type;
+ } else {
+ ExifTagEvent tagEvent = (ExifTagEvent) event;
+ mTag = tagEvent.tag;
+ if (mTag.getDataType() != ExifTag.TYPE_UNDEFINED) {
+ readFullTagValue(mTag);
+ checkOffsetOrImageTag(mTag);
+ }
+ if (tagEvent.isRequested) {
+ return EVENT_VALUE_OF_REGISTERED_TAG;
+ }
+ }
+ }
+ return EVENT_END;
+ }
+
+ /**
+ * Skips the tags area of current IFD, if the parser is not in the tag area,
+ * nothing will happen.
+ *
+ * @throws IOException
+ * @throws ExifInvalidFormatException
+ */
+ protected void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException {
+ int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
+ int offset = mTiffStream.getReadByteCount();
+ if (offset > endOfTags) {
+ return;
+ }
+ if (mNeedToParseOffsetsInCurrentIfd) {
+ while (offset < endOfTags) {
+ mTag = readTag();
+ offset += TAG_SIZE;
+ if (mTag == null) {
+ continue;
+ }
+ checkOffsetOrImageTag(mTag);
+ }
+ } else {
+ skipTo(endOfTags);
+ }
+ long ifdOffset = readUnsignedLong();
+ // For ifd0, there is a link to ifd1 in the end of all tags
+ if (mIfdType == IfdId.TYPE_IFD_0
+ && (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested())) {
+ if (ifdOffset > 0) {
+ registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
+ }
+ }
+ }
+
+ private boolean needToParseOffsetsInCurrentIfd() {
+ switch (mIfdType) {
+ case IfdId.TYPE_IFD_0:
+ return isIfdRequested(IfdId.TYPE_IFD_EXIF) || isIfdRequested(IfdId.TYPE_IFD_GPS)
+ || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)
+ || isIfdRequested(IfdId.TYPE_IFD_1);
+ case IfdId.TYPE_IFD_1:
+ return isThumbnailRequested();
+ case IfdId.TYPE_IFD_EXIF:
+ // The offset to interoperability IFD is located in Exif IFD
+ return isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY);
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * If {@link #next()} return {@link #EVENT_NEW_TAG} or
+ * {@link #EVENT_VALUE_OF_REGISTERED_TAG}, call this function to get the
+ * corresponding tag.
+ * <p>
+ * For {@link #EVENT_NEW_TAG}, the tag may not contain the value if the size
+ * of the value is greater than 4 bytes. One should call
+ * {@link ExifTag#hasValue()} to check if the tag contains value. If there
+ * is no value,call {@link #registerForTagValue(ExifTag)} to have the parser
+ * emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area
+ * pointed by the offset.
+ * <p>
+ * When {@link #EVENT_VALUE_OF_REGISTERED_TAG} is emitted, the value of the
+ * tag will have already been read except for tags of undefined type. For
+ * tags of undefined type, call one of the read methods to get the value.
+ *
+ * @see #registerForTagValue(ExifTag)
+ * @see #read(byte[])
+ * @see #read(byte[], int, int)
+ * @see #readLong()
+ * @see #readRational()
+ * @see #readString(int)
+ * @see #readString(int, Charset)
+ */
+ protected ExifTag getTag() {
+ return mTag;
+ }
+
+ /**
+ * Gets number of tags in the current IFD area.
+ */
+ protected int getTagCountInCurrentIfd() {
+ return mNumOfTagInIfd;
+ }
+
+ /**
+ * Gets the ID of current IFD.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ * @see IfdId#TYPE_IFD_EXIF
+ */
+ protected int getCurrentIfd() {
+ return mIfdType;
+ }
+
+ /**
+ * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
+ * get the index of this strip.
+ *
+ * @see #getStripCount()
+ */
+ protected int getStripIndex() {
+ return mImageEvent.stripIndex;
+ }
+
+ /**
+ * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
+ * get the number of strip data.
+ *
+ * @see #getStripIndex()
+ */
+ protected int getStripCount() {
+ return mStripCount;
+ }
+
+ /**
+ * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to
+ * get the strip size.
+ */
+ protected int getStripSize() {
+ if (mStripSizeTag == null)
+ return 0;
+ return (int) mStripSizeTag.getValueAt(0);
+ }
+
+ /**
+ * When receiving {@link #EVENT_COMPRESSED_IMAGE}, call this function to get
+ * the image data size.
+ */
+ protected int getCompressedImageSize() {
+ if (mJpegSizeTag == null) {
+ return 0;
+ }
+ return (int) mJpegSizeTag.getValueAt(0);
+ }
+
+ private void skipTo(int offset) throws IOException {
+ mTiffStream.skipTo(offset);
+ while (!mCorrespondingEvent.isEmpty() && mCorrespondingEvent.firstKey() < offset) {
+ mCorrespondingEvent.pollFirstEntry();
+ }
+ }
+
+ /**
+ * When getting {@link #EVENT_NEW_TAG} in the tag area of IFD, the tag may
+ * not contain the value if the size of the value is greater than 4 bytes.
+ * When the value is not available here, call this method so that the parser
+ * will emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area
+ * where the value is located.
+ *
+ * @see #EVENT_VALUE_OF_REGISTERED_TAG
+ */
+ protected void registerForTagValue(ExifTag tag) {
+ if (tag.getOffset() >= mTiffStream.getReadByteCount()) {
+ mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, true));
+ }
+ }
+
+ private void registerIfd(int ifdType, long offset) {
+ // Cast unsigned int to int since the offset is always smaller
+ // than the size of APP1 (65536)
+ mCorrespondingEvent.put((int) offset, new IfdEvent(ifdType, isIfdRequested(ifdType)));
+ }
+
+ private void registerCompressedImage(long offset) {
+ mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_COMPRESSED_IMAGE));
+ }
+
+ private void registerUncompressedStrip(int stripIndex, long offset) {
+ mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_UNCOMPRESSED_STRIP
+ , stripIndex));
+ }
+
+ private ExifTag readTag() throws IOException, ExifInvalidFormatException {
+ short tagId = mTiffStream.readShort();
+ short dataFormat = mTiffStream.readShort();
+ long numOfComp = mTiffStream.readUnsignedInt();
+ if (numOfComp > Integer.MAX_VALUE) {
+ throw new ExifInvalidFormatException(
+ "Number of component is larger then Integer.MAX_VALUE");
+ }
+ // Some invalid image file contains invalid data type. Ignore those tags
+ if (!ExifTag.isValidType(dataFormat)) {
+ Log.w(TAG, String.format("Tag %04x: Invalid data type %d", tagId, dataFormat));
+ mTiffStream.skip(4);
+ return null;
+ }
+ // TODO: handle numOfComp overflow
+ ExifTag tag = new ExifTag(tagId, dataFormat, (int) numOfComp, mIfdType,
+ ((int) numOfComp) != ExifTag.SIZE_UNDEFINED);
+ int dataSize = tag.getDataSize();
+ if (dataSize > 4) {
+ long offset = mTiffStream.readUnsignedInt();
+ if (offset > Integer.MAX_VALUE) {
+ throw new ExifInvalidFormatException(
+ "offset is larger then Integer.MAX_VALUE");
+ }
+ // Some invalid images put some undefined data before IFD0.
+ // Read the data here.
+ if ((offset < mIfd0Position) && (dataFormat == ExifTag.TYPE_UNDEFINED)) {
+ byte[] buf = new byte[(int) numOfComp];
+ System.arraycopy(mDataAboveIfd0, (int) offset - DEFAULT_IFD0_OFFSET,
+ buf, 0, (int) numOfComp);
+ tag.setValue(buf);
+ } else {
+ tag.setOffset((int) offset);
+ }
+ } else {
+ boolean defCount = tag.hasDefinedCount();
+ // Set defined count to 0 so we can add \0 to non-terminated strings
+ tag.setHasDefinedCount(false);
+ // Read value
+ readFullTagValue(tag);
+ tag.setHasDefinedCount(defCount);
+ mTiffStream.skip(4 - dataSize);
+ // Set the offset to the position of value.
+ tag.setOffset(mTiffStream.getReadByteCount() - 4);
+ }
+ return tag;
+ }
+
+ /**
+ * Check the tag, if the tag is one of the offset tag that points to the IFD
+ * or image the caller is interested in, register the IFD or image.
+ */
+ private void checkOffsetOrImageTag(ExifTag tag) {
+ // Some invalid formattd image contains tag with 0 size.
+ if (tag.getComponentCount() == 0) {
+ return;
+ }
+ short tid = tag.getTagId();
+ int ifd = tag.getIfd();
+ if (tid == TAG_EXIF_IFD && checkAllowed(ifd, ExifInterface.TAG_EXIF_IFD)) {
+ if (isIfdRequested(IfdId.TYPE_IFD_EXIF)
+ || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
+ registerIfd(IfdId.TYPE_IFD_EXIF, tag.getValueAt(0));
+ }
+ } else if (tid == TAG_GPS_IFD && checkAllowed(ifd, ExifInterface.TAG_GPS_IFD)) {
+ if (isIfdRequested(IfdId.TYPE_IFD_GPS)) {
+ registerIfd(IfdId.TYPE_IFD_GPS, tag.getValueAt(0));
+ }
+ } else if (tid == TAG_INTEROPERABILITY_IFD
+ && checkAllowed(ifd, ExifInterface.TAG_INTEROPERABILITY_IFD)) {
+ if (isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
+ registerIfd(IfdId.TYPE_IFD_INTEROPERABILITY, tag.getValueAt(0));
+ }
+ } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT
+ && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) {
+ if (isThumbnailRequested()) {
+ registerCompressedImage(tag.getValueAt(0));
+ }
+ } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT_LENGTH
+ && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)) {
+ if (isThumbnailRequested()) {
+ mJpegSizeTag = tag;
+ }
+ } else if (tid == TAG_STRIP_OFFSETS && checkAllowed(ifd, ExifInterface.TAG_STRIP_OFFSETS)) {
+ if (isThumbnailRequested()) {
+ if (tag.hasValue()) {
+ for (int i = 0; i < tag.getComponentCount(); i++) {
+ if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) {
+ registerUncompressedStrip(i, tag.getValueAt(i));
+ } else {
+ registerUncompressedStrip(i, tag.getValueAt(i));
+ }
+ }
+ } else {
+ mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, false));
+ }
+ }
+ } else if (tid == TAG_STRIP_BYTE_COUNTS
+ && checkAllowed(ifd, ExifInterface.TAG_STRIP_BYTE_COUNTS)
+ &&isThumbnailRequested() && tag.hasValue()) {
+ mStripSizeTag = tag;
+ }
+ }
+
+ private boolean checkAllowed(int ifd, int tagId) {
+ int info = mInterface.getTagInfo().get(tagId);
+ if (info == ExifInterface.DEFINITION_NULL) {
+ return false;
+ }
+ return ExifInterface.isIfdAllowed(info, ifd);
+ }
+
+ protected void readFullTagValue(ExifTag tag) throws IOException {
+ // Some invalid images contains tags with wrong size, check it here
+ short type = tag.getDataType();
+ if (type == ExifTag.TYPE_ASCII || type == ExifTag.TYPE_UNDEFINED ||
+ type == ExifTag.TYPE_UNSIGNED_BYTE) {
+ int size = tag.getComponentCount();
+ if (mCorrespondingEvent.size() > 0) {
+ if (mCorrespondingEvent.firstEntry().getKey() < mTiffStream.getReadByteCount()
+ + size) {
+ Object event = mCorrespondingEvent.firstEntry().getValue();
+ if (event instanceof ImageEvent) {
+ // Tag value overlaps thumbnail, ignore thumbnail.
+ Log.w(TAG, "Thumbnail overlaps value for tag: \n" + tag.toString());
+ Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
+ Log.w(TAG, "Invalid thumbnail offset: " + entry.getKey());
+ } else {
+ // Tag value overlaps another tag, shorten count
+ if (event instanceof IfdEvent) {
+ Log.w(TAG, "Ifd " + ((IfdEvent) event).ifd
+ + " overlaps value for tag: \n" + tag.toString());
+ } else if (event instanceof ExifTagEvent) {
+ Log.w(TAG, "Tag value for tag: \n"
+ + ((ExifTagEvent) event).tag.toString()
+ + " overlaps value for tag: \n" + tag.toString());
+ }
+ size = mCorrespondingEvent.firstEntry().getKey()
+ - mTiffStream.getReadByteCount();
+ Log.w(TAG, "Invalid size of tag: \n" + tag.toString()
+ + " setting count to: " + size);
+ tag.forceSetComponentCount(size);
+ }
+ }
+ }
+ }
+ switch (tag.getDataType()) {
+ case ExifTag.TYPE_UNSIGNED_BYTE:
+ case ExifTag.TYPE_UNDEFINED: {
+ byte buf[] = new byte[tag.getComponentCount()];
+ read(buf);
+ tag.setValue(buf);
+ }
+ break;
+ case ExifTag.TYPE_ASCII:
+ tag.setValue(readString(tag.getComponentCount()));
+ break;
+ case ExifTag.TYPE_UNSIGNED_LONG: {
+ long value[] = new long[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readUnsignedLong();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_UNSIGNED_RATIONAL: {
+ Rational value[] = new Rational[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readUnsignedRational();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_UNSIGNED_SHORT: {
+ int value[] = new int[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readUnsignedShort();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_LONG: {
+ int value[] = new int[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readLong();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_RATIONAL: {
+ Rational value[] = new Rational[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readRational();
+ }
+ tag.setValue(value);
+ }
+ break;
+ }
+ if (LOGV) {
+ Log.v(TAG, "\n" + tag.toString());
+ }
+ }
+
+ private void parseTiffHeader() throws IOException,
+ ExifInvalidFormatException {
+ short byteOrder = mTiffStream.readShort();
+ if (LITTLE_ENDIAN_TAG == byteOrder) {
+ mTiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ } else if (BIG_ENDIAN_TAG == byteOrder) {
+ mTiffStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+ } else {
+ throw new ExifInvalidFormatException("Invalid TIFF header");
+ }
+
+ if (mTiffStream.readShort() != TIFF_HEADER_TAIL) {
+ throw new ExifInvalidFormatException("Invalid TIFF header");
+ }
+ }
+
+ private boolean seekTiffData(InputStream inputStream) throws IOException,
+ ExifInvalidFormatException {
+ CountedDataInputStream dataStream = new CountedDataInputStream(inputStream);
+ if (dataStream.readShort() != JpegHeader.SOI) {
+ throw new ExifInvalidFormatException("Invalid JPEG format");
+ }
+
+ short marker = dataStream.readShort();
+ while (marker != JpegHeader.EOI
+ && !JpegHeader.isSofMarker(marker)) {
+ int length = dataStream.readUnsignedShort();
+ // Some invalid formatted image contains multiple APP1,
+ // try to find the one with Exif data.
+ if (marker == JpegHeader.APP1) {
+ int header = 0;
+ short headerTail = 0;
+ if (length >= 8) {
+ header = dataStream.readInt();
+ headerTail = dataStream.readShort();
+ length -= 6;
+ if (header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL) {
+ mTiffStartPosition = dataStream.getReadByteCount();
+ mApp1End = length;
+ mOffsetToApp1EndFromSOF = mTiffStartPosition + mApp1End;
+ return true;
+ }
+ }
+ }
+ if (length < 2 || (length - 2) != dataStream.skip(length - 2)) {
+ Log.w(TAG, "Invalid JPEG format.");
+ return false;
+ }
+ marker = dataStream.readShort();
+ }
+ return false;
+ }
+
+ protected int getOffsetToExifEndFromSOF() {
+ return mOffsetToApp1EndFromSOF;
+ }
+
+ protected int getTiffStartPosition() {
+ return mTiffStartPosition;
+ }
+
+ /**
+ * Reads bytes from the InputStream.
+ */
+ protected int read(byte[] buffer, int offset, int length) throws IOException {
+ return mTiffStream.read(buffer, offset, length);
+ }
+
+ /**
+ * Equivalent to read(buffer, 0, buffer.length).
+ */
+ protected int read(byte[] buffer) throws IOException {
+ return mTiffStream.read(buffer);
+ }
+
+ /**
+ * Reads a String from the InputStream with US-ASCII charset. The parser
+ * will read n bytes and convert it to ascii string. This is used for
+ * reading values of type {@link ExifTag#TYPE_ASCII}.
+ */
+ protected String readString(int n) throws IOException {
+ return readString(n, US_ASCII);
+ }
+
+ /**
+ * Reads a String from the InputStream with the given charset. The parser
+ * will read n bytes and convert it to string. This is used for reading
+ * values of type {@link ExifTag#TYPE_ASCII}.
+ */
+ protected String readString(int n, Charset charset) throws IOException {
+ if (n > 0) {
+ return mTiffStream.readString(n, charset);
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the
+ * InputStream.
+ */
+ protected int readUnsignedShort() throws IOException {
+ return mTiffStream.readShort() & 0xffff;
+ }
+
+ /**
+ * Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the
+ * InputStream.
+ */
+ protected long readUnsignedLong() throws IOException {
+ return readLong() & 0xffffffffL;
+ }
+
+ /**
+ * Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the
+ * InputStream.
+ */
+ protected Rational readUnsignedRational() throws IOException {
+ long nomi = readUnsignedLong();
+ long denomi = readUnsignedLong();
+ return new Rational(nomi, denomi);
+ }
+
+ /**
+ * Reads value of type {@link ExifTag#TYPE_LONG} from the InputStream.
+ */
+ protected int readLong() throws IOException {
+ return mTiffStream.readInt();
+ }
+
+ /**
+ * Reads value of type {@link ExifTag#TYPE_RATIONAL} from the InputStream.
+ */
+ protected Rational readRational() throws IOException {
+ int nomi = readLong();
+ int denomi = readLong();
+ return new Rational(nomi, denomi);
+ }
+
+ private static class ImageEvent {
+ int stripIndex;
+ int type;
+
+ ImageEvent(int type) {
+ this.stripIndex = 0;
+ this.type = type;
+ }
+
+ ImageEvent(int type, int stripIndex) {
+ this.type = type;
+ this.stripIndex = stripIndex;
+ }
+ }
+
+ private static class IfdEvent {
+ int ifd;
+ boolean isRequested;
+
+ IfdEvent(int ifd, boolean isInterestedIfd) {
+ this.ifd = ifd;
+ this.isRequested = isInterestedIfd;
+ }
+ }
+
+ private static class ExifTagEvent {
+ ExifTag tag;
+ boolean isRequested;
+
+ ExifTagEvent(ExifTag tag, boolean isRequireByUser) {
+ this.tag = tag;
+ this.isRequested = isRequireByUser;
+ }
+ }
+
+ /**
+ * Gets the byte order of the current InputStream.
+ */
+ protected ByteOrder getByteOrder() {
+ return mTiffStream.getByteOrder();
+ }
+}
diff --git a/src/com/android/gallery3d/exif/ExifReader.java b/src/com/android/gallery3d/exif/ExifReader.java
new file mode 100644
index 0000000..68e972f
--- /dev/null
+++ b/src/com/android/gallery3d/exif/ExifReader.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.exif;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * This class reads the EXIF header of a JPEG file and stores it in
+ * {@link ExifData}.
+ */
+class ExifReader {
+ private static final String TAG = "ExifReader";
+
+ private final ExifInterface mInterface;
+
+ ExifReader(ExifInterface iRef) {
+ mInterface = iRef;
+ }
+
+ /**
+ * Parses the inputStream and and returns the EXIF data in an
+ * {@link ExifData}.
+ *
+ * @throws ExifInvalidFormatException
+ * @throws IOException
+ */
+ protected ExifData read(InputStream inputStream) throws ExifInvalidFormatException,
+ IOException {
+ ExifParser parser = ExifParser.parse(inputStream, mInterface);
+ ExifData exifData = new ExifData(parser.getByteOrder());
+ ExifTag tag = null;
+
+ int event = parser.next();
+ while (event != ExifParser.EVENT_END) {
+ switch (event) {
+ case ExifParser.EVENT_START_OF_IFD:
+ exifData.addIfdData(new IfdData(parser.getCurrentIfd()));
+ break;
+ case ExifParser.EVENT_NEW_TAG:
+ tag = parser.getTag();
+ if (!tag.hasValue()) {
+ parser.registerForTagValue(tag);
+ } else {
+ exifData.getIfdData(tag.getIfd()).setTag(tag);
+ }
+ break;
+ case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+ tag = parser.getTag();
+ if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) {
+ parser.readFullTagValue(tag);
+ }
+ exifData.getIfdData(tag.getIfd()).setTag(tag);
+ break;
+ case ExifParser.EVENT_COMPRESSED_IMAGE:
+ byte buf[] = new byte[parser.getCompressedImageSize()];
+ if (buf.length == parser.read(buf)) {
+ exifData.setCompressedThumbnail(buf);
+ } else {
+ Log.w(TAG, "Failed to read the compressed thumbnail");
+ }
+ break;
+ case ExifParser.EVENT_UNCOMPRESSED_STRIP:
+ buf = new byte[parser.getStripSize()];
+ if (buf.length == parser.read(buf)) {
+ exifData.setStripBytes(parser.getStripIndex(), buf);
+ } else {
+ Log.w(TAG, "Failed to read the strip bytes");
+ }
+ break;
+ }
+ event = parser.next();
+ }
+ return exifData;
+ }
+}
diff --git a/src/com/android/gallery3d/exif/ExifTag.java b/src/com/android/gallery3d/exif/ExifTag.java
new file mode 100644
index 0000000..b8b3872
--- /dev/null
+++ b/src/com/android/gallery3d/exif/ExifTag.java
@@ -0,0 +1,1008 @@
+/*
+ * 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.exif;
+
+import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+
+/**
+ * This class stores information of an EXIF tag. For more information about
+ * defined EXIF tags, please read the Jeita EXIF 2.2 standard. Tags should be
+ * instantiated using {@link ExifInterface#buildTag}.
+ *
+ * @see ExifInterface
+ */
+public class ExifTag {
+ /**
+ * The BYTE type in the EXIF standard. An 8-bit unsigned integer.
+ */
+ public static final short TYPE_UNSIGNED_BYTE = 1;
+ /**
+ * The ASCII type in the EXIF standard. An 8-bit byte containing one 7-bit
+ * ASCII code. The final byte is terminated with NULL.
+ */
+ public static final short TYPE_ASCII = 2;
+ /**
+ * The SHORT type in the EXIF standard. A 16-bit (2-byte) unsigned integer
+ */
+ public static final short TYPE_UNSIGNED_SHORT = 3;
+ /**
+ * The LONG type in the EXIF standard. A 32-bit (4-byte) unsigned integer
+ */
+ public static final short TYPE_UNSIGNED_LONG = 4;
+ /**
+ * The RATIONAL type of EXIF standard. It consists of two LONGs. The first
+ * one is the numerator and the second one expresses the denominator.
+ */
+ public static final short TYPE_UNSIGNED_RATIONAL = 5;
+ /**
+ * The UNDEFINED type in the EXIF standard. An 8-bit byte that can take any
+ * value depending on the field definition.
+ */
+ public static final short TYPE_UNDEFINED = 7;
+ /**
+ * The SLONG type in the EXIF standard. A 32-bit (4-byte) signed integer
+ * (2's complement notation).
+ */
+ public static final short TYPE_LONG = 9;
+ /**
+ * The SRATIONAL type of EXIF standard. It consists of two SLONGs. The first
+ * one is the numerator and the second one is the denominator.
+ */
+ public static final short TYPE_RATIONAL = 10;
+
+ private static Charset US_ASCII = Charset.forName("US-ASCII");
+ private static final int TYPE_TO_SIZE_MAP[] = new int[11];
+ private static final int UNSIGNED_SHORT_MAX = 65535;
+ private static final long UNSIGNED_LONG_MAX = 4294967295L;
+ private static final long LONG_MAX = Integer.MAX_VALUE;
+ private static final long LONG_MIN = Integer.MIN_VALUE;
+
+ static {
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_BYTE] = 1;
+ TYPE_TO_SIZE_MAP[TYPE_ASCII] = 1;
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_SHORT] = 2;
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_LONG] = 4;
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_RATIONAL] = 8;
+ TYPE_TO_SIZE_MAP[TYPE_UNDEFINED] = 1;
+ TYPE_TO_SIZE_MAP[TYPE_LONG] = 4;
+ TYPE_TO_SIZE_MAP[TYPE_RATIONAL] = 8;
+ }
+
+ static final int SIZE_UNDEFINED = 0;
+
+ // Exif TagId
+ private final short mTagId;
+ // Exif Tag Type
+ private final short mDataType;
+ // If tag has defined count
+ private boolean mHasDefinedDefaultComponentCount;
+ // Actual data count in tag (should be number of elements in value array)
+ private int mComponentCountActual;
+ // The ifd that this tag should be put in
+ private int mIfd;
+ // The value (array of elements of type Tag Type)
+ private Object mValue;
+ // Value offset in exif header.
+ private int mOffset;
+
+ private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("yyyy:MM:dd kk:mm:ss");
+
+ /**
+ * Returns true if the given IFD is a valid IFD.
+ */
+ public static boolean isValidIfd(int ifdId) {
+ return ifdId == IfdId.TYPE_IFD_0 || ifdId == IfdId.TYPE_IFD_1
+ || ifdId == IfdId.TYPE_IFD_EXIF || ifdId == IfdId.TYPE_IFD_INTEROPERABILITY
+ || ifdId == IfdId.TYPE_IFD_GPS;
+ }
+
+ /**
+ * Returns true if a given type is a valid tag type.
+ */
+ public static boolean isValidType(short type) {
+ return type == TYPE_UNSIGNED_BYTE || type == TYPE_ASCII ||
+ type == TYPE_UNSIGNED_SHORT || type == TYPE_UNSIGNED_LONG ||
+ type == TYPE_UNSIGNED_RATIONAL || type == TYPE_UNDEFINED ||
+ type == TYPE_LONG || type == TYPE_RATIONAL;
+ }
+
+ // Use builtTag in ExifInterface instead of constructor.
+ ExifTag(short tagId, short type, int componentCount, int ifd,
+ boolean hasDefinedComponentCount) {
+ mTagId = tagId;
+ mDataType = type;
+ mComponentCountActual = componentCount;
+ mHasDefinedDefaultComponentCount = hasDefinedComponentCount;
+ mIfd = ifd;
+ mValue = null;
+ }
+
+ /**
+ * Gets the element size of the given data type in bytes.
+ *
+ * @see #TYPE_ASCII
+ * @see #TYPE_LONG
+ * @see #TYPE_RATIONAL
+ * @see #TYPE_UNDEFINED
+ * @see #TYPE_UNSIGNED_BYTE
+ * @see #TYPE_UNSIGNED_LONG
+ * @see #TYPE_UNSIGNED_RATIONAL
+ * @see #TYPE_UNSIGNED_SHORT
+ */
+ public static int getElementSize(short type) {
+ return TYPE_TO_SIZE_MAP[type];
+ }
+
+ /**
+ * Returns the ID of the IFD this tag belongs to.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_EXIF
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ */
+ public int getIfd() {
+ return mIfd;
+ }
+
+ protected void setIfd(int ifdId) {
+ mIfd = ifdId;
+ }
+
+ /**
+ * Gets the TID of this tag.
+ */
+ public short getTagId() {
+ return mTagId;
+ }
+
+ /**
+ * Gets the data type of this tag
+ *
+ * @see #TYPE_ASCII
+ * @see #TYPE_LONG
+ * @see #TYPE_RATIONAL
+ * @see #TYPE_UNDEFINED
+ * @see #TYPE_UNSIGNED_BYTE
+ * @see #TYPE_UNSIGNED_LONG
+ * @see #TYPE_UNSIGNED_RATIONAL
+ * @see #TYPE_UNSIGNED_SHORT
+ */
+ public short getDataType() {
+ return mDataType;
+ }
+
+ /**
+ * Gets the total data size in bytes of the value of this tag.
+ */
+ public int getDataSize() {
+ return getComponentCount() * getElementSize(getDataType());
+ }
+
+ /**
+ * Gets the component count of this tag.
+ */
+
+ // TODO: fix integer overflows with this
+ public int getComponentCount() {
+ return mComponentCountActual;
+ }
+
+ /**
+ * Sets the component count of this tag. Call this function before
+ * setValue() if the length of value does not match the component count.
+ */
+ protected void forceSetComponentCount(int count) {
+ mComponentCountActual = count;
+ }
+
+ /**
+ * Returns true if this ExifTag contains value; otherwise, this tag will
+ * contain an offset value that is determined when the tag is written.
+ */
+ public boolean hasValue() {
+ return mValue != null;
+ }
+
+ /**
+ * Sets integer values into this tag. This method should be used for tags of
+ * type {@link #TYPE_UNSIGNED_SHORT}. This method will fail if:
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT},
+ * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.</li>
+ * <li>The value overflows.</li>
+ * <li>The value.length does NOT match the component count in the definition
+ * for this tag.</li>
+ * </ul>
+ */
+ public boolean setValue(int[] value) {
+ if (checkBadComponentCount(value.length)) {
+ return false;
+ }
+ if (mDataType != TYPE_UNSIGNED_SHORT && mDataType != TYPE_LONG &&
+ mDataType != TYPE_UNSIGNED_LONG) {
+ return false;
+ }
+ if (mDataType == TYPE_UNSIGNED_SHORT && checkOverflowForUnsignedShort(value)) {
+ return false;
+ } else if (mDataType == TYPE_UNSIGNED_LONG && checkOverflowForUnsignedLong(value)) {
+ return false;
+ }
+
+ long[] data = new long[value.length];
+ for (int i = 0; i < value.length; i++) {
+ data[i] = value[i];
+ }
+ mValue = data;
+ mComponentCountActual = value.length;
+ return true;
+ }
+
+ /**
+ * Sets integer value into this tag. This method should be used for tags of
+ * type {@link #TYPE_UNSIGNED_SHORT}, or {@link #TYPE_LONG}. This method
+ * will fail if:
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT},
+ * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.</li>
+ * <li>The value overflows.</li>
+ * <li>The component count in the definition of this tag is not 1.</li>
+ * </ul>
+ */
+ public boolean setValue(int value) {
+ return setValue(new int[] {
+ value
+ });
+ }
+
+ /**
+ * Sets long values into this tag. This method should be used for tags of
+ * type {@link #TYPE_UNSIGNED_LONG}. This method will fail if:
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.</li>
+ * <li>The value overflows.</li>
+ * <li>The value.length does NOT match the component count in the definition
+ * for this tag.</li>
+ * </ul>
+ */
+ public boolean setValue(long[] value) {
+ if (checkBadComponentCount(value.length) || mDataType != TYPE_UNSIGNED_LONG) {
+ return false;
+ }
+ if (checkOverflowForUnsignedLong(value)) {
+ return false;
+ }
+ mValue = value;
+ mComponentCountActual = value.length;
+ return true;
+ }
+
+ /**
+ * Sets long values into this tag. This method should be used for tags of
+ * type {@link #TYPE_UNSIGNED_LONG}. This method will fail if:
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.</li>
+ * <li>The value overflows.</li>
+ * <li>The component count in the definition for this tag is not 1.</li>
+ * </ul>
+ */
+ public boolean setValue(long value) {
+ return setValue(new long[] {
+ value
+ });
+ }
+
+ /**
+ * Sets a string value into this tag. This method should be used for tags of
+ * type {@link #TYPE_ASCII}. The string is converted to an ASCII string.
+ * Characters that cannot be converted are replaced with '?'. The length of
+ * the string must be equal to either (component count -1) or (component
+ * count). The final byte will be set to the string null terminator '\0',
+ * overwriting the last character in the string if the value.length is equal
+ * to the component count. This method will fail if:
+ * <ul>
+ * <li>The data type is not {@link #TYPE_ASCII} or {@link #TYPE_UNDEFINED}.</li>
+ * <li>The length of the string is not equal to (component count -1) or
+ * (component count) in the definition for this tag.</li>
+ * </ul>
+ */
+ public boolean setValue(String value) {
+ if (mDataType != TYPE_ASCII && mDataType != TYPE_UNDEFINED) {
+ return false;
+ }
+
+ byte[] buf = value.getBytes(US_ASCII);
+ byte[] finalBuf = buf;
+ if (buf.length > 0) {
+ finalBuf = (buf[buf.length - 1] == 0 || mDataType == TYPE_UNDEFINED) ? buf : Arrays
+ .copyOf(buf, buf.length + 1);
+ } else if (mDataType == TYPE_ASCII && mComponentCountActual == 1) {
+ finalBuf = new byte[] { 0 };
+ }
+ int count = finalBuf.length;
+ if (checkBadComponentCount(count)) {
+ return false;
+ }
+ mComponentCountActual = count;
+ mValue = finalBuf;
+ return true;
+ }
+
+ /**
+ * Sets Rational values into this tag. This method should be used for tags
+ * of type {@link #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This
+ * method will fail if:
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL}
+ * or {@link #TYPE_RATIONAL}.</li>
+ * <li>The value overflows.</li>
+ * <li>The value.length does NOT match the component count in the definition
+ * for this tag.</li>
+ * </ul>
+ *
+ * @see Rational
+ */
+ public boolean setValue(Rational[] value) {
+ if (checkBadComponentCount(value.length)) {
+ return false;
+ }
+ if (mDataType != TYPE_UNSIGNED_RATIONAL && mDataType != TYPE_RATIONAL) {
+ return false;
+ }
+ if (mDataType == TYPE_UNSIGNED_RATIONAL && checkOverflowForUnsignedRational(value)) {
+ return false;
+ } else if (mDataType == TYPE_RATIONAL && checkOverflowForRational(value)) {
+ return false;
+ }
+
+ mValue = value;
+ mComponentCountActual = value.length;
+ return true;
+ }
+
+ /**
+ * Sets a Rational value into this tag. This method should be used for tags
+ * of type {@link #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This
+ * method will fail if:
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL}
+ * or {@link #TYPE_RATIONAL}.</li>
+ * <li>The value overflows.</li>
+ * <li>The component count in the definition for this tag is not 1.</li>
+ * </ul>
+ *
+ * @see Rational
+ */
+ public boolean setValue(Rational value) {
+ return setValue(new Rational[] {
+ value
+ });
+ }
+
+ /**
+ * Sets byte values into this tag. This method should be used for tags of
+ * type {@link #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method
+ * will fail if:
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or
+ * {@link #TYPE_UNDEFINED} .</li>
+ * <li>The length does NOT match the component count in the definition for
+ * this tag.</li>
+ * </ul>
+ */
+ public boolean setValue(byte[] value, int offset, int length) {
+ if (checkBadComponentCount(length)) {
+ return false;
+ }
+ if (mDataType != TYPE_UNSIGNED_BYTE && mDataType != TYPE_UNDEFINED) {
+ return false;
+ }
+ mValue = new byte[length];
+ System.arraycopy(value, offset, mValue, 0, length);
+ mComponentCountActual = length;
+ return true;
+ }
+
+ /**
+ * Equivalent to setValue(value, 0, value.length).
+ */
+ public boolean setValue(byte[] value) {
+ return setValue(value, 0, value.length);
+ }
+
+ /**
+ * Sets byte value into this tag. This method should be used for tags of
+ * type {@link #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method
+ * will fail if:
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or
+ * {@link #TYPE_UNDEFINED} .</li>
+ * <li>The component count in the definition for this tag is not 1.</li>
+ * </ul>
+ */
+ public boolean setValue(byte value) {
+ return setValue(new byte[] {
+ value
+ });
+ }
+
+ /**
+ * Sets the value for this tag using an appropriate setValue method for the
+ * given object. This method will fail if:
+ * <ul>
+ * <li>The corresponding setValue method for the class of the object passed
+ * in would fail.</li>
+ * <li>There is no obvious way to cast the object passed in into an EXIF tag
+ * type.</li>
+ * </ul>
+ */
+ public boolean setValue(Object obj) {
+ if (obj == null) {
+ return false;
+ } else if (obj instanceof Short) {
+ return setValue(((Short) obj).shortValue() & 0x0ffff);
+ } else if (obj instanceof String) {
+ return setValue((String) obj);
+ } else if (obj instanceof int[]) {
+ return setValue((int[]) obj);
+ } else if (obj instanceof long[]) {
+ return setValue((long[]) obj);
+ } else if (obj instanceof Rational) {
+ return setValue((Rational) obj);
+ } else if (obj instanceof Rational[]) {
+ return setValue((Rational[]) obj);
+ } else if (obj instanceof byte[]) {
+ return setValue((byte[]) obj);
+ } else if (obj instanceof Integer) {
+ return setValue(((Integer) obj).intValue());
+ } else if (obj instanceof Long) {
+ return setValue(((Long) obj).longValue());
+ } else if (obj instanceof Byte) {
+ return setValue(((Byte) obj).byteValue());
+ } else if (obj instanceof Short[]) {
+ // Nulls in this array are treated as zeroes.
+ Short[] arr = (Short[]) obj;
+ int[] fin = new int[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ fin[i] = (arr[i] == null) ? 0 : arr[i].shortValue() & 0x0ffff;
+ }
+ return setValue(fin);
+ } else if (obj instanceof Integer[]) {
+ // Nulls in this array are treated as zeroes.
+ Integer[] arr = (Integer[]) obj;
+ int[] fin = new int[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ fin[i] = (arr[i] == null) ? 0 : arr[i].intValue();
+ }
+ return setValue(fin);
+ } else if (obj instanceof Long[]) {
+ // Nulls in this array are treated as zeroes.
+ Long[] arr = (Long[]) obj;
+ long[] fin = new long[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ fin[i] = (arr[i] == null) ? 0 : arr[i].longValue();
+ }
+ return setValue(fin);
+ } else if (obj instanceof Byte[]) {
+ // Nulls in this array are treated as zeroes.
+ Byte[] arr = (Byte[]) obj;
+ byte[] fin = new byte[arr.length];
+ for (int i = 0; i < arr.length; i++) {
+ fin[i] = (arr[i] == null) ? 0 : arr[i].byteValue();
+ }
+ return setValue(fin);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Sets a timestamp to this tag. The method converts the timestamp with the
+ * format of "yyyy:MM:dd kk:mm:ss" and calls {@link #setValue(String)}. This
+ * method will fail if the data type is not {@link #TYPE_ASCII} or the
+ * component count of this tag is not 20 or undefined.
+ *
+ * @param time the number of milliseconds since Jan. 1, 1970 GMT
+ * @return true on success
+ */
+ public boolean setTimeValue(long time) {
+ // synchronized on TIME_FORMAT as SimpleDateFormat is not thread safe
+ synchronized (TIME_FORMAT) {
+ return setValue(TIME_FORMAT.format(new Date(time)));
+ }
+ }
+
+ /**
+ * Gets the value as a String. This method should be used for tags of type
+ * {@link #TYPE_ASCII}.
+ *
+ * @return the value as a String, or null if the tag's value does not exist
+ * or cannot be converted to a String.
+ */
+ public String getValueAsString() {
+ if (mValue == null) {
+ return null;
+ } else if (mValue instanceof String) {
+ return (String) mValue;
+ } else if (mValue instanceof byte[]) {
+ return new String((byte[]) mValue, US_ASCII);
+ }
+ return null;
+ }
+
+ /**
+ * Gets the value as a String. This method should be used for tags of type
+ * {@link #TYPE_ASCII}.
+ *
+ * @param defaultValue the String to return if the tag's value does not
+ * exist or cannot be converted to a String.
+ * @return the tag's value as a String, or the defaultValue.
+ */
+ public String getValueAsString(String defaultValue) {
+ String s = getValueAsString();
+ if (s == null) {
+ return defaultValue;
+ }
+ return s;
+ }
+
+ /**
+ * Gets the value as a byte array. This method should be used for tags of
+ * type {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}.
+ *
+ * @return the value as a byte array, or null if the tag's value does not
+ * exist or cannot be converted to a byte array.
+ */
+ public byte[] getValueAsBytes() {
+ if (mValue instanceof byte[]) {
+ return (byte[]) mValue;
+ }
+ return null;
+ }
+
+ /**
+ * Gets the value as a byte. If there are more than 1 bytes in this value,
+ * gets the first byte. This method should be used for tags of type
+ * {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}.
+ *
+ * @param defaultValue the byte to return if tag's value does not exist or
+ * cannot be converted to a byte.
+ * @return the tag's value as a byte, or the defaultValue.
+ */
+ public byte getValueAsByte(byte defaultValue) {
+ byte[] b = getValueAsBytes();
+ if (b == null || b.length < 1) {
+ return defaultValue;
+ }
+ return b[0];
+ }
+
+ /**
+ * Gets the value as an array of Rationals. This method should be used for
+ * tags of type {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+ *
+ * @return the value as as an array of Rationals, or null if the tag's value
+ * does not exist or cannot be converted to an array of Rationals.
+ */
+ public Rational[] getValueAsRationals() {
+ if (mValue instanceof Rational[]) {
+ return (Rational[]) mValue;
+ }
+ return null;
+ }
+
+ /**
+ * Gets the value as a Rational. If there are more than 1 Rationals in this
+ * value, gets the first one. This method should be used for tags of type
+ * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+ *
+ * @param defaultValue the Rational to return if tag's value does not exist
+ * or cannot be converted to a Rational.
+ * @return the tag's value as a Rational, or the defaultValue.
+ */
+ public Rational getValueAsRational(Rational defaultValue) {
+ Rational[] r = getValueAsRationals();
+ if (r == null || r.length < 1) {
+ return defaultValue;
+ }
+ return r[0];
+ }
+
+ /**
+ * Gets the value as a Rational. If there are more than 1 Rationals in this
+ * value, gets the first one. This method should be used for tags of type
+ * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+ *
+ * @param defaultValue the numerator of the Rational to return if tag's
+ * value does not exist or cannot be converted to a Rational (the
+ * denominator will be 1).
+ * @return the tag's value as a Rational, or the defaultValue.
+ */
+ public Rational getValueAsRational(long defaultValue) {
+ Rational defaultVal = new Rational(defaultValue, 1);
+ return getValueAsRational(defaultVal);
+ }
+
+ /**
+ * Gets the value as an array of ints. This method should be used for tags
+ * of type {@link #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}.
+ *
+ * @return the value as as an array of ints, or null if the tag's value does
+ * not exist or cannot be converted to an array of ints.
+ */
+ public int[] getValueAsInts() {
+ if (mValue == null) {
+ return null;
+ } else if (mValue instanceof long[]) {
+ long[] val = (long[]) mValue;
+ int[] arr = new int[val.length];
+ for (int i = 0; i < val.length; i++) {
+ arr[i] = (int) val[i]; // Truncates
+ }
+ return arr;
+ }
+ return null;
+ }
+
+ /**
+ * Gets the value as an int. If there are more than 1 ints in this value,
+ * gets the first one. This method should be used for tags of type
+ * {@link #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}.
+ *
+ * @param defaultValue the int to return if tag's value does not exist or
+ * cannot be converted to an int.
+ * @return the tag's value as a int, or the defaultValue.
+ */
+ public int getValueAsInt(int defaultValue) {
+ int[] i = getValueAsInts();
+ if (i == null || i.length < 1) {
+ return defaultValue;
+ }
+ return i[0];
+ }
+
+ /**
+ * Gets the value as an array of longs. This method should be used for tags
+ * of type {@link #TYPE_UNSIGNED_LONG}.
+ *
+ * @return the value as as an array of longs, or null if the tag's value
+ * does not exist or cannot be converted to an array of longs.
+ */
+ public long[] getValueAsLongs() {
+ if (mValue instanceof long[]) {
+ return (long[]) mValue;
+ }
+ return null;
+ }
+
+ /**
+ * Gets the value or null if none exists. If there are more than 1 longs in
+ * this value, gets the first one. This method should be used for tags of
+ * type {@link #TYPE_UNSIGNED_LONG}.
+ *
+ * @param defaultValue the long to return if tag's value does not exist or
+ * cannot be converted to a long.
+ * @return the tag's value as a long, or the defaultValue.
+ */
+ public long getValueAsLong(long defaultValue) {
+ long[] l = getValueAsLongs();
+ if (l == null || l.length < 1) {
+ return defaultValue;
+ }
+ return l[0];
+ }
+
+ /**
+ * Gets the tag's value or null if none exists.
+ */
+ public Object getValue() {
+ return mValue;
+ }
+
+ /**
+ * Gets a long representation of the value.
+ *
+ * @param defaultValue value to return if there is no value or value is a
+ * rational with a denominator of 0.
+ * @return the tag's value as a long, or defaultValue if no representation
+ * exists.
+ */
+ public long forceGetValueAsLong(long defaultValue) {
+ long[] l = getValueAsLongs();
+ if (l != null && l.length >= 1) {
+ return l[0];
+ }
+ byte[] b = getValueAsBytes();
+ if (b != null && b.length >= 1) {
+ return b[0];
+ }
+ Rational[] r = getValueAsRationals();
+ if (r != null && r.length >= 1 && r[0].getDenominator() != 0) {
+ return (long) r[0].toDouble();
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets a string representation of the value.
+ */
+ public String forceGetValueAsString() {
+ if (mValue == null) {
+ return "";
+ } else if (mValue instanceof byte[]) {
+ if (mDataType == TYPE_ASCII) {
+ return new String((byte[]) mValue, US_ASCII);
+ } else {
+ return Arrays.toString((byte[]) mValue);
+ }
+ } else if (mValue instanceof long[]) {
+ if (((long[]) mValue).length == 1) {
+ return String.valueOf(((long[]) mValue)[0]);
+ } else {
+ return Arrays.toString((long[]) mValue);
+ }
+ } else if (mValue instanceof Object[]) {
+ if (((Object[]) mValue).length == 1) {
+ Object val = ((Object[]) mValue)[0];
+ if (val == null) {
+ return "";
+ } else {
+ return val.toString();
+ }
+ } else {
+ return Arrays.toString((Object[]) mValue);
+ }
+ } else {
+ return mValue.toString();
+ }
+ }
+
+ /**
+ * Gets the value for type {@link #TYPE_ASCII}, {@link #TYPE_LONG},
+ * {@link #TYPE_UNDEFINED}, {@link #TYPE_UNSIGNED_BYTE},
+ * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_UNSIGNED_SHORT}. For
+ * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}, call
+ * {@link #getRational(int)} instead.
+ *
+ * @exception IllegalArgumentException if the data type is
+ * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+ */
+ protected long getValueAt(int index) {
+ if (mValue instanceof long[]) {
+ return ((long[]) mValue)[index];
+ } else if (mValue instanceof byte[]) {
+ return ((byte[]) mValue)[index];
+ }
+ throw new IllegalArgumentException("Cannot get integer value from "
+ + convertTypeToString(mDataType));
+ }
+
+ /**
+ * Gets the {@link #TYPE_ASCII} data.
+ *
+ * @exception IllegalArgumentException If the type is NOT
+ * {@link #TYPE_ASCII}.
+ */
+ protected String getString() {
+ if (mDataType != TYPE_ASCII) {
+ throw new IllegalArgumentException("Cannot get ASCII value from "
+ + convertTypeToString(mDataType));
+ }
+ return new String((byte[]) mValue, US_ASCII);
+ }
+
+ /*
+ * Get the converted ascii byte. Used by ExifOutputStream.
+ */
+ protected byte[] getStringByte() {
+ return (byte[]) mValue;
+ }
+
+ /**
+ * Gets the {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL} data.
+ *
+ * @exception IllegalArgumentException If the type is NOT
+ * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}.
+ */
+ protected Rational getRational(int index) {
+ if ((mDataType != TYPE_RATIONAL) && (mDataType != TYPE_UNSIGNED_RATIONAL)) {
+ throw new IllegalArgumentException("Cannot get RATIONAL value from "
+ + convertTypeToString(mDataType));
+ }
+ return ((Rational[]) mValue)[index];
+ }
+
+ /**
+ * Equivalent to getBytes(buffer, 0, buffer.length).
+ */
+ protected void getBytes(byte[] buf) {
+ getBytes(buf, 0, buf.length);
+ }
+
+ /**
+ * Gets the {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE} data.
+ *
+ * @param buf the byte array in which to store the bytes read.
+ * @param offset the initial position in buffer to store the bytes.
+ * @param length the maximum number of bytes to store in buffer. If length >
+ * component count, only the valid bytes will be stored.
+ * @exception IllegalArgumentException If the type is NOT
+ * {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}.
+ */
+ protected void getBytes(byte[] buf, int offset, int length) {
+ if ((mDataType != TYPE_UNDEFINED) && (mDataType != TYPE_UNSIGNED_BYTE)) {
+ throw new IllegalArgumentException("Cannot get BYTE value from "
+ + convertTypeToString(mDataType));
+ }
+ System.arraycopy(mValue, 0, buf, offset,
+ (length > mComponentCountActual) ? mComponentCountActual : length);
+ }
+
+ /**
+ * Gets the offset of this tag. This is only valid if this data size > 4 and
+ * contains an offset to the location of the actual value.
+ */
+ protected int getOffset() {
+ return mOffset;
+ }
+
+ /**
+ * Sets the offset of this tag.
+ */
+ protected void setOffset(int offset) {
+ mOffset = offset;
+ }
+
+ protected void setHasDefinedCount(boolean d) {
+ mHasDefinedDefaultComponentCount = d;
+ }
+
+ protected boolean hasDefinedCount() {
+ return mHasDefinedDefaultComponentCount;
+ }
+
+ private boolean checkBadComponentCount(int count) {
+ if (mHasDefinedDefaultComponentCount && (mComponentCountActual != count)) {
+ return true;
+ }
+ return false;
+ }
+
+ private static String convertTypeToString(short type) {
+ switch (type) {
+ case TYPE_UNSIGNED_BYTE:
+ return "UNSIGNED_BYTE";
+ case TYPE_ASCII:
+ return "ASCII";
+ case TYPE_UNSIGNED_SHORT:
+ return "UNSIGNED_SHORT";
+ case TYPE_UNSIGNED_LONG:
+ return "UNSIGNED_LONG";
+ case TYPE_UNSIGNED_RATIONAL:
+ return "UNSIGNED_RATIONAL";
+ case TYPE_UNDEFINED:
+ return "UNDEFINED";
+ case TYPE_LONG:
+ return "LONG";
+ case TYPE_RATIONAL:
+ return "RATIONAL";
+ default:
+ return "";
+ }
+ }
+
+ private boolean checkOverflowForUnsignedShort(int[] value) {
+ for (int v : value) {
+ if (v > UNSIGNED_SHORT_MAX || v < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForUnsignedLong(long[] value) {
+ for (long v : value) {
+ if (v < 0 || v > UNSIGNED_LONG_MAX) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForUnsignedLong(int[] value) {
+ for (int v : value) {
+ if (v < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForUnsignedRational(Rational[] value) {
+ for (Rational v : value) {
+ if (v.getNumerator() < 0 || v.getDenominator() < 0
+ || v.getNumerator() > UNSIGNED_LONG_MAX
+ || v.getDenominator() > UNSIGNED_LONG_MAX) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForRational(Rational[] value) {
+ for (Rational v : value) {
+ if (v.getNumerator() < LONG_MIN || v.getDenominator() < LONG_MIN
+ || v.getNumerator() > LONG_MAX
+ || v.getDenominator() > LONG_MAX) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj instanceof ExifTag) {
+ ExifTag tag = (ExifTag) obj;
+ if (tag.mTagId != this.mTagId
+ || tag.mComponentCountActual != this.mComponentCountActual
+ || tag.mDataType != this.mDataType) {
+ return false;
+ }
+ if (mValue != null) {
+ if (tag.mValue == null) {
+ return false;
+ } else if (mValue instanceof long[]) {
+ if (!(tag.mValue instanceof long[])) {
+ return false;
+ }
+ return Arrays.equals((long[]) mValue, (long[]) tag.mValue);
+ } else if (mValue instanceof Rational[]) {
+ if (!(tag.mValue instanceof Rational[])) {
+ return false;
+ }
+ return Arrays.equals((Rational[]) mValue, (Rational[]) tag.mValue);
+ } else if (mValue instanceof byte[]) {
+ if (!(tag.mValue instanceof byte[])) {
+ return false;
+ }
+ return Arrays.equals((byte[]) mValue, (byte[]) tag.mValue);
+ } else {
+ return mValue.equals(tag.mValue);
+ }
+ } else {
+ return tag.mValue == null;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("tag id: %04X\n", mTagId) + "ifd id: " + mIfd + "\ntype: "
+ + convertTypeToString(mDataType) + "\ncount: " + mComponentCountActual
+ + "\noffset: " + mOffset + "\nvalue: " + forceGetValueAsString() + "\n";
+ }
+
+}
diff --git a/src/com/android/gallery3d/exif/IfdData.java b/src/com/android/gallery3d/exif/IfdData.java
new file mode 100644
index 0000000..093944a
--- /dev/null
+++ b/src/com/android/gallery3d/exif/IfdData.java
@@ -0,0 +1,152 @@
+/*
+ * 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.exif;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class stores all the tags in an IFD.
+ *
+ * @see ExifData
+ * @see ExifTag
+ */
+class IfdData {
+
+ private final int mIfdId;
+ private final Map<Short, ExifTag> mExifTags = new HashMap<Short, ExifTag>();
+ private int mOffsetToNextIfd = 0;
+ private static final int[] sIfds = {
+ IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1, IfdId.TYPE_IFD_EXIF,
+ IfdId.TYPE_IFD_INTEROPERABILITY, IfdId.TYPE_IFD_GPS
+ };
+ /**
+ * Creates an IfdData with given IFD ID.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_EXIF
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ */
+ IfdData(int ifdId) {
+ mIfdId = ifdId;
+ }
+
+ static protected int[] getIfds() {
+ return sIfds;
+ }
+
+ /**
+ * Get a array the contains all {@link ExifTag} in this IFD.
+ */
+ protected ExifTag[] getAllTags() {
+ return mExifTags.values().toArray(new ExifTag[mExifTags.size()]);
+ }
+
+ /**
+ * Gets the ID of this IFD.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_EXIF
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ */
+ protected int getId() {
+ return mIfdId;
+ }
+
+ /**
+ * Gets the {@link ExifTag} with given tag id. Return null if there is no
+ * such tag.
+ */
+ protected ExifTag getTag(short tagId) {
+ return mExifTags.get(tagId);
+ }
+
+ /**
+ * Adds or replaces a {@link ExifTag}.
+ */
+ protected ExifTag setTag(ExifTag tag) {
+ tag.setIfd(mIfdId);
+ return mExifTags.put(tag.getTagId(), tag);
+ }
+
+ protected boolean checkCollision(short tagId) {
+ return mExifTags.get(tagId) != null;
+ }
+
+ /**
+ * Removes the tag of the given ID
+ */
+ protected void removeTag(short tagId) {
+ mExifTags.remove(tagId);
+ }
+
+ /**
+ * Gets the tags count in the IFD.
+ */
+ protected int getTagCount() {
+ return mExifTags.size();
+ }
+
+ /**
+ * Sets the offset of next IFD.
+ */
+ protected void setOffsetToNextIfd(int offset) {
+ mOffsetToNextIfd = offset;
+ }
+
+ /**
+ * Gets the offset of next IFD.
+ */
+ protected int getOffsetToNextIfd() {
+ return mOffsetToNextIfd;
+ }
+
+ /**
+ * Returns true if all tags in this two IFDs are equal. Note that tags of
+ * IFDs offset or thumbnail offset will be ignored.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (obj instanceof IfdData) {
+ IfdData data = (IfdData) obj;
+ if (data.getId() == mIfdId && data.getTagCount() == getTagCount()) {
+ ExifTag[] tags = data.getAllTags();
+ for (ExifTag tag : tags) {
+ if (ExifInterface.isOffsetTag(tag.getTagId())) {
+ continue;
+ }
+ ExifTag tag2 = mExifTags.get(tag.getTagId());
+ if (!tag.equals(tag2)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/gallery3d/exif/IfdId.java b/src/com/android/gallery3d/exif/IfdId.java
new file mode 100644
index 0000000..7842edb
--- /dev/null
+++ b/src/com/android/gallery3d/exif/IfdId.java
@@ -0,0 +1,31 @@
+/*
+ * 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.exif;
+
+/**
+ * The constants of the IFD ID defined in EXIF spec.
+ */
+public interface IfdId {
+ public static final int TYPE_IFD_0 = 0;
+ public static final int TYPE_IFD_1 = 1;
+ public static final int TYPE_IFD_EXIF = 2;
+ public static final int TYPE_IFD_INTEROPERABILITY = 3;
+ public static final int TYPE_IFD_GPS = 4;
+ /* This is used in ExifData to allocate enough IfdData */
+ static final int TYPE_IFD_COUNT = 5;
+
+}
diff --git a/src/com/android/gallery3d/exif/JpegHeader.java b/src/com/android/gallery3d/exif/JpegHeader.java
new file mode 100644
index 0000000..e3e787e
--- /dev/null
+++ b/src/com/android/gallery3d/exif/JpegHeader.java
@@ -0,0 +1,39 @@
+/*
+ * 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.exif;
+
+class JpegHeader {
+ public static final short SOI = (short) 0xFFD8;
+ public static final short APP1 = (short) 0xFFE1;
+ public static final short APP0 = (short) 0xFFE0;
+ public static final short EOI = (short) 0xFFD9;
+
+ /**
+ * SOF (start of frame). All value between SOF0 and SOF15 is SOF marker except for DHT, JPG,
+ * and DAC marker.
+ */
+ public static final short SOF0 = (short) 0xFFC0;
+ public static final short SOF15 = (short) 0xFFCF;
+ public static final short DHT = (short) 0xFFC4;
+ public static final short JPG = (short) 0xFFC8;
+ public static final short DAC = (short) 0xFFCC;
+
+ public static final boolean isSofMarker(short marker) {
+ return marker >= SOF0 && marker <= SOF15 && marker != DHT && marker != JPG
+ && marker != DAC;
+ }
+}
diff --git a/src/com/android/gallery3d/exif/OrderedDataOutputStream.java b/src/com/android/gallery3d/exif/OrderedDataOutputStream.java
new file mode 100644
index 0000000..428e6b9
--- /dev/null
+++ b/src/com/android/gallery3d/exif/OrderedDataOutputStream.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.exif;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+class OrderedDataOutputStream extends FilterOutputStream {
+ private final ByteBuffer mByteBuffer = ByteBuffer.allocate(4);
+
+ public OrderedDataOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ public OrderedDataOutputStream setByteOrder(ByteOrder order) {
+ mByteBuffer.order(order);
+ return this;
+ }
+
+ public OrderedDataOutputStream writeShort(short value) throws IOException {
+ mByteBuffer.rewind();
+ mByteBuffer.putShort(value);
+ out.write(mByteBuffer.array(), 0, 2);
+ return this;
+ }
+
+ public OrderedDataOutputStream writeInt(int value) throws IOException {
+ mByteBuffer.rewind();
+ mByteBuffer.putInt(value);
+ out.write(mByteBuffer.array());
+ return this;
+ }
+
+ public OrderedDataOutputStream writeRational(Rational rational) throws IOException {
+ writeInt((int) rational.getNumerator());
+ writeInt((int) rational.getDenominator());
+ return this;
+ }
+}
diff --git a/src/com/android/gallery3d/exif/Rational.java b/src/com/android/gallery3d/exif/Rational.java
new file mode 100644
index 0000000..591d63f
--- /dev/null
+++ b/src/com/android/gallery3d/exif/Rational.java
@@ -0,0 +1,88 @@
+/*
+ * 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.exif;
+
+/**
+ * The rational data type of EXIF tag. Contains a pair of longs representing the
+ * numerator and denominator of a Rational number.
+ */
+public class Rational {
+
+ private final long mNumerator;
+ private final long mDenominator;
+
+ /**
+ * Create a Rational with a given numerator and denominator.
+ *
+ * @param nominator
+ * @param denominator
+ */
+ public Rational(long nominator, long denominator) {
+ mNumerator = nominator;
+ mDenominator = denominator;
+ }
+
+ /**
+ * Create a copy of a Rational.
+ */
+ public Rational(Rational r) {
+ mNumerator = r.mNumerator;
+ mDenominator = r.mDenominator;
+ }
+
+ /**
+ * Gets the numerator of the rational.
+ */
+ public long getNumerator() {
+ return mNumerator;
+ }
+
+ /**
+ * Gets the denominator of the rational
+ */
+ public long getDenominator() {
+ return mDenominator;
+ }
+
+ /**
+ * Gets the rational value as type double. Will cause a divide-by-zero error
+ * if the denominator is 0.
+ */
+ public double toDouble() {
+ return mNumerator / (double) mDenominator;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof Rational) {
+ Rational data = (Rational) obj;
+ return mNumerator == data.mNumerator && mDenominator == data.mDenominator;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return mNumerator + "/" + mDenominator;
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/BasicTexture.java b/src/com/android/gallery3d/glrenderer/BasicTexture.java
new file mode 100644
index 0000000..0f3efb7
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/BasicTexture.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.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 {
+
+ 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 0000000..f8b01cb
--- /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 com.android.gallery3d.common.Utils;
+
+// 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);
+ Utils.assertTrue(bitmap != null && !bitmap.isRecycled());
+ mContentBitmap = bitmap;
+ }
+
+ @Override
+ protected void onFreeBitmap(Bitmap bitmap) {
+ // Do nothing.
+ }
+
+ @Override
+ protected Bitmap onGetBitmap() {
+ return mContentBitmap;
+ }
+
+ public Bitmap getBitmap() {
+ return mContentBitmap;
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/GLCanvas.java b/src/com/android/gallery3d/glrenderer/GLCanvas.java
new file mode 100644
index 0000000..5b07477
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLCanvas.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+//
+// 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/GLES20Canvas.java b/src/com/android/gallery3d/glrenderer/GLES20Canvas.java
new file mode 100644
index 0000000..933260b
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/GLES20Canvas.java
@@ -0,0 +1,1008 @@
+/*
+ * 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 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) {
+ deleteRecycledResources();
+ 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 0000000..6cd7149
--- /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 0000000..3cec558
--- /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 0000000..b26e9ab
--- /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 com.android.gallery3d.common.Utils;
+
+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) {
+ Utils.assertTrue(width >= 0);
+ mLineWidth = width;
+ }
+
+ public float getLineWidth() {
+ return mLineWidth;
+ }
+}
diff --git a/src/com/android/gallery3d/glrenderer/IntArray.java b/src/com/android/gallery3d/glrenderer/IntArray.java
new file mode 100644
index 0000000..f123624
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/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.glrenderer;
+
+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/glrenderer/RawTexture.java b/src/com/android/gallery3d/glrenderer/RawTexture.java
new file mode 100644
index 0000000..93f0fdf
--- /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/Texture.java b/src/com/android/gallery3d/glrenderer/Texture.java
new file mode 100644
index 0000000..3dcae4a
--- /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/UploadedTexture.java b/src/com/android/gallery3d/glrenderer/UploadedTexture.java
new file mode 100644
index 0000000..d171bd9
--- /dev/null
+++ b/src/com/android/gallery3d/glrenderer/UploadedTexture.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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 com.android.gallery3d.common.Utils;
+import org.cyanogenmod.wallpaperpicker.util.Thunk;
+
+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;
+ }
+
+ @Thunk
+ 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() {
+ Utils.assertTrue(mBitmap != null);
+ onFreeBitmap(mBitmap);
+ mBitmap = null;
+ }
+
+ @Override
+ public int getWidth() {
+ if (mWidth == UNSPECIFIED) getBitmap();
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ if (mWidth == UNSPECIFIED) getBitmap();
+ return mHeight;
+ }
+
+ protected abstract Bitmap onGetBitmap();
+
+ protected abstract void onFreeBitmap(Bitmap bitmap);
+
+ protected void invalidateContent() {
+ if (mBitmap != null) freeBitmap();
+ mContentValid = false;
+ 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();
+
+ Utils.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/photos/BitmapRegionTileSource.java b/src/com/android/photos/BitmapRegionTileSource.java
new file mode 100644
index 0000000..19393af
--- /dev/null
+++ b/src/com/android/photos/BitmapRegionTileSource.java
@@ -0,0 +1,563 @@
+/*
+ * 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.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.opengl.GLUtils;
+import android.os.Build;
+import android.util.Log;
+
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.photos.views.TiledImageRenderer;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+interface SimpleBitmapRegionDecoder {
+ int getWidth();
+ int getHeight();
+ Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options);
+}
+
+class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder {
+ BitmapRegionDecoder mDecoder;
+ private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) {
+ mDecoder = decoder;
+ }
+ public static SimpleBitmapRegionDecoderWrapper newInstance(
+ String pathName, boolean isShareable) {
+ try {
+ BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(pathName, isShareable);
+ if (d != null) {
+ return new SimpleBitmapRegionDecoderWrapper(d);
+ }
+ } catch (IOException e) {
+ Log.w("BitmapRegionTileSource", "getting decoder failed for path " + pathName, e);
+ return null;
+ }
+ return null;
+ }
+ public static SimpleBitmapRegionDecoderWrapper newInstance(
+ InputStream is, boolean isShareable) {
+ try {
+ BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable);
+ if (d != null) {
+ return new SimpleBitmapRegionDecoderWrapper(d);
+ }
+ } catch (IOException e) {
+ Log.w("BitmapRegionTileSource", "getting decoder failed", e);
+ return null;
+ }
+ return null;
+ }
+ public int getWidth() {
+ return mDecoder.getWidth();
+ }
+ public int getHeight() {
+ return mDecoder.getHeight();
+ }
+ public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
+ return mDecoder.decodeRegion(wantRegion, options);
+ }
+}
+
+class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder {
+ Bitmap mBuffer;
+ Canvas mTempCanvas;
+ Paint mTempPaint;
+ private DumbBitmapRegionDecoder(Bitmap b) {
+ mBuffer = b;
+ }
+ public static DumbBitmapRegionDecoder newInstance(String pathName) {
+ Bitmap b = BitmapFactory.decodeFile(pathName);
+ if (b != null) {
+ return new DumbBitmapRegionDecoder(b);
+ }
+ return null;
+ }
+ public static DumbBitmapRegionDecoder newInstance(InputStream is) {
+ Bitmap b = BitmapFactory.decodeStream(is);
+ if (b != null) {
+ return new DumbBitmapRegionDecoder(b);
+ }
+ return null;
+ }
+ public static DumbBitmapRegionDecoder newInstance(Bitmap src) {
+ if (src != null) {
+ return new DumbBitmapRegionDecoder(src);
+ }
+ return null;
+ }
+ public int getWidth() {
+ return mBuffer.getWidth();
+ }
+ public int getHeight() {
+ return mBuffer.getHeight();
+ }
+ public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) {
+ if (mTempCanvas == null) {
+ mTempCanvas = new Canvas();
+ mTempPaint = new Paint();
+ mTempPaint.setFilterBitmap(true);
+ }
+ int sampleSize = Math.max(options.inSampleSize, 1);
+ Bitmap newBitmap = Bitmap.createBitmap(
+ wantRegion.width() / sampleSize,
+ wantRegion.height() / sampleSize,
+ Bitmap.Config.ARGB_8888);
+ mTempCanvas.setBitmap(newBitmap);
+ mTempCanvas.save();
+ mTempCanvas.scale(1f / sampleSize, 1f / sampleSize);
+ mTempCanvas.drawBitmap(mBuffer, -wantRegion.left, -wantRegion.top, mTempPaint);
+ mTempCanvas.restore();
+ mTempCanvas.setBitmap(null);
+ return newBitmap;
+ }
+}
+
+/**
+ * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using
+ * {@link BitmapRegionDecoder} to wrap a local file
+ */
+@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
+public class BitmapRegionTileSource implements TiledImageRenderer.TileSource {
+
+ private static final String TAG = "BitmapRegionTileSource";
+
+ private static final int GL_SIZE_LIMIT = 2048;
+ // This must be no larger than half the size of the GL_SIZE_LIMIT
+ // due to decodePreview being allowed to be up to 2x the size of the target
+ private static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
+
+ public static abstract class BitmapSource {
+ private SimpleBitmapRegionDecoder mDecoder;
+ private Bitmap mPreview;
+ private int mRotation;
+ public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
+ private State mState = State.NOT_LOADED;
+
+ public boolean loadInBackground(InBitmapProvider bitmapProvider) {
+ ExifInterface ei = new ExifInterface();
+ if (readExif(ei)) {
+ Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+ if (ori != null) {
+ mRotation = ExifInterface.getRotationForOrientationValue(ori.shortValue());
+ }
+ }
+ mDecoder = loadBitmapRegionDecoder();
+ if (mDecoder == null) {
+ mState = State.ERROR_LOADING;
+ return false;
+ } else {
+ int width = mDecoder.getWidth();
+ int height = mDecoder.getHeight();
+
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ opts.inPreferQualityOverSpeed = true;
+
+ float scale = (float) MAX_PREVIEW_SIZE / Math.max(width, height);
+ opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
+ opts.inJustDecodeBounds = false;
+ opts.inMutable = true;
+
+ if (bitmapProvider != null) {
+ int expectedPixles = (width / opts.inSampleSize) * (height / opts.inSampleSize);
+ Bitmap reusableBitmap = bitmapProvider.forPixelCount(expectedPixles);
+ if (reusableBitmap != null) {
+ // Try loading with reusable bitmap
+ opts.inBitmap = reusableBitmap;
+ try {
+ mPreview = loadPreviewBitmap(opts);
+ } catch (IllegalArgumentException e) {
+ Log.d(TAG, "Unable to reusage bitmap", e);
+ opts.inBitmap = null;
+ mPreview = null;
+ }
+ }
+ }
+ if (mPreview == null) {
+ mPreview = loadPreviewBitmap(opts);
+ }
+
+ // Verify that the bitmap can be used on GL surface
+ try {
+ GLUtils.getInternalFormat(mPreview);
+ GLUtils.getType(mPreview);
+ mState = State.LOADED;
+ } catch (IllegalArgumentException e) {
+ Log.d(TAG, "Image cannot be rendered on a GL surface", e);
+ mState = State.ERROR_LOADING;
+ }
+ return true;
+ }
+ }
+
+ public State getLoadingState() {
+ return mState;
+ }
+
+ public SimpleBitmapRegionDecoder getBitmapRegionDecoder() {
+ return mDecoder;
+ }
+
+ public Bitmap getPreviewBitmap() {
+ return mPreview;
+ }
+
+ public int getRotation() {
+ return mRotation;
+ }
+
+ public abstract boolean readExif(ExifInterface ei);
+ public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
+ public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
+
+ public interface InBitmapProvider {
+ Bitmap forPixelCount(int count);
+ }
+ }
+
+ public static class FilePathBitmapSource extends BitmapSource {
+ private String mPath;
+ public FilePathBitmapSource(String path) {
+ mPath = path;
+ }
+ @Override
+ public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
+ SimpleBitmapRegionDecoder d;
+ d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true);
+ if (d == null) {
+ d = DumbBitmapRegionDecoder.newInstance(mPath);
+ }
+ return d;
+ }
+ @Override
+ public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
+ return BitmapFactory.decodeFile(mPath, options);
+ }
+ @Override
+ public boolean readExif(ExifInterface ei) {
+ try {
+ ei.readExif(mPath);
+ return true;
+ } catch (NullPointerException e) {
+ Log.w("BitmapRegionTileSource", "reading exif failed", e);
+ return false;
+ } catch (IOException e) {
+ Log.w("BitmapRegionTileSource", "getting decoder failed", e);
+ return false;
+ }
+ }
+ }
+
+ public static class UriBitmapSource extends BitmapSource {
+ private Context mContext;
+ private Uri mUri;
+ public UriBitmapSource(Context context, Uri uri) {
+ mContext = context;
+ mUri = uri;
+ }
+ private InputStream regenerateInputStream() throws FileNotFoundException {
+ InputStream is = mContext.getContentResolver().openInputStream(mUri);
+ return new BufferedInputStream(is);
+ }
+ @Override
+ public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
+ try {
+ InputStream is = regenerateInputStream();
+ SimpleBitmapRegionDecoder regionDecoder =
+ SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
+ Utils.closeSilently(is);
+ if (regionDecoder == null) {
+ is = regenerateInputStream();
+ regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
+ Utils.closeSilently(is);
+ }
+ return regionDecoder;
+ } catch (FileNotFoundException e) {
+ Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
+ return null;
+ }
+ }
+ @Override
+ public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
+ try {
+ InputStream is = regenerateInputStream();
+ Bitmap b = BitmapFactory.decodeStream(is, null, options);
+ Utils.closeSilently(is);
+ return b;
+ } catch (FileNotFoundException e) {
+ Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
+ return null;
+ }
+ }
+ @Override
+ public boolean readExif(ExifInterface ei) {
+ InputStream is = null;
+ try {
+ is = regenerateInputStream();
+ ei.readExif(is);
+ Utils.closeSilently(is);
+ return true;
+ } catch (FileNotFoundException e) {
+ Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
+ return false;
+ } catch (IOException e) {
+ Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
+ return false;
+ } catch (NullPointerException e) {
+ Log.d("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e);
+ return false;
+ } finally {
+ Utils.closeSilently(is);
+ }
+ }
+ }
+
+ public static class ResourceBitmapSource extends BitmapSource {
+ private Resources mRes;
+ private int mResId;
+ public ResourceBitmapSource(Resources res, int resId) {
+ mRes = res;
+ mResId = resId;
+ }
+ private InputStream regenerateInputStream() {
+ InputStream is = mRes.openRawResource(mResId);
+ return new BufferedInputStream(is);
+ }
+ @Override
+ public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
+ InputStream is = regenerateInputStream();
+ SimpleBitmapRegionDecoder regionDecoder =
+ SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
+ Utils.closeSilently(is);
+ if (regionDecoder == null) {
+ is = regenerateInputStream();
+ regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
+ Utils.closeSilently(is);
+ }
+ return regionDecoder;
+ }
+ @Override
+ public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
+ return BitmapFactory.decodeResource(mRes, mResId, options);
+ }
+ @Override
+ public boolean readExif(ExifInterface ei) {
+ try {
+ InputStream is = regenerateInputStream();
+ ei.readExif(is);
+ Utils.closeSilently(is);
+ return true;
+ } catch (IOException e) {
+ Log.e("BitmapRegionTileSource", "Error reading resource", e);
+ return false;
+ }
+ }
+ }
+
+ public static class ThemeBitmapSource extends BitmapSource {
+ private AssetManager mAssets;
+ private String mPath;
+ public ThemeBitmapSource(AssetManager assets, String path) {
+ mAssets = assets;
+ mPath = path;
+ }
+ private InputStream regenerateInputStream() {
+ String[] pathImages = new String[0];
+ try {
+ pathImages = mAssets.list(mPath);
+ if (pathImages == null || pathImages.length == 0) {
+ Log.d(TAG, "did not find any images in path: " + mPath);
+ return null;
+ }
+ InputStream is = mAssets.open(mPath + File.separator + pathImages[0]);
+ return new BufferedInputStream(is);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+ @Override
+ public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
+ InputStream is = regenerateInputStream();
+ SimpleBitmapRegionDecoder regionDecoder =
+ SimpleBitmapRegionDecoderWrapper.newInstance(is, false);
+ Utils.closeSilently(is);
+ if (regionDecoder == null) {
+ is = regenerateInputStream();
+ regionDecoder = DumbBitmapRegionDecoder.newInstance(is);
+ Utils.closeSilently(is);
+ }
+ return regionDecoder;
+ }
+ @Override
+ public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
+ InputStream is = regenerateInputStream();
+ return is != null ? BitmapFactory.decodeStream(is, null, options) : null;
+ }
+ @Override
+ public boolean readExif(ExifInterface ei) {
+ try {
+ InputStream is = regenerateInputStream();
+ ei.readExif(is);
+ Utils.closeSilently(is);
+ return true;
+ } catch (IOException e) {
+ Log.e("BitmapRegionTileSource", "Error reading resource", e);
+ return false;
+ }
+ }
+ }
+
+ public static class DumbBitmapSource extends BitmapSource {
+ Bitmap mSource;
+ public DumbBitmapSource(Bitmap source) {
+ mSource = source;
+ }
+ @Override
+ public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() {
+ return DumbBitmapRegionDecoder.newInstance(mSource);
+ }
+ @Override
+ public Bitmap loadPreviewBitmap(BitmapFactory.Options options) {
+ // We need to honor the options being passed in so that an appropriate bitmap, for use
+ // as a texture, is returned so we need to encode the bitmap using compress and then
+ // decode the compressed image's bytes using the provided options.
+ // JPEG is used instead of PNG as it was encoding much faster.
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ mSource.compress(Bitmap.CompressFormat.JPEG, 100, stream);
+ byte[] bytes = stream.toByteArray();
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+ }
+ @Override
+ public boolean readExif(ExifInterface ei) {
+ return false;
+ }
+ }
+
+ SimpleBitmapRegionDecoder 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 BitmapFactory.Options mOptions;
+
+ public BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage) {
+ mTileSize = TiledImageRenderer.suggestedTileSize(context);
+ mRotation = source.getRotation();
+ mDecoder = source.getBitmapRegionDecoder();
+ if (mDecoder != null) {
+ mWidth = mDecoder.getWidth();
+ mHeight = mDecoder.getHeight();
+ mOptions = new BitmapFactory.Options();
+ mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ mOptions.inPreferQualityOverSpeed = true;
+ mOptions.inTempStorage = tempStorage;
+
+ Bitmap preview = source.getPreviewBitmap();
+ if (preview != null &&
+ preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
+ mPreview = new BitmapTexture(preview);
+ } else {
+ Log.w(TAG, String.format(
+ "Failed to create preview of apropriate size! "
+ + " in: %dx%d, out: %dx%d",
+ mWidth, mHeight,
+ preview.getWidth(), preview.getHeight()));
+ }
+ }
+ }
+
+ public Bitmap getBitmap() {
+ return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null;
+ }
+
+ @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();
+ 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;
+ }
+}
diff --git a/src/com/android/photos/views/TiledImageRenderer.java b/src/com/android/photos/views/TiledImageRenderer.java
new file mode 100644
index 0000000..4ec8dd4
--- /dev/null
+++ b/src/com/android/photos/views/TiledImageRenderer.java
@@ -0,0 +1,828 @@
+/*
+ * 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.Pools.Pool;
+import android.support.v4.util.Pools.SynchronizedPool;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.LongSparseArray;
+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;
+
+import org.cyanogenmod.wallpaperpicker.util.Thunk;
+
+/**
+ * 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;
+
+ @Thunk
+ static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64);
+
+ // TILE_SIZE must be 2^N
+ @Thunk int mTileSize;
+
+ @Thunk 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
+ @Thunk 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
+ @Thunk final Object mQueueLock = new Object();
+ private final TileQueue mRecycledQueue = new TileQueue();
+ private final TileQueue mUploadQueue = new TileQueue();
+ @Thunk 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();
+ }
+ }
+ }
+ }
+
+ @Thunk 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);
+ }
+
+ @Thunk 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);
+ }
+ }
+
+ @Thunk 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;
+ }
+ }
+
+ @Thunk 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 0000000..cbd8401
--- /dev/null
+++ b/src/com/android/photos/views/TiledImageView.java
@@ -0,0 +1,314 @@
+/*
+ * 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.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.util.AttributeSet;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.widget.FrameLayout;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.GLES20Canvas;
+import com.android.photos.views.TiledImageRenderer.TileSource;
+
+import org.cyanogenmod.wallpaperpicker.util.Thunk;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+/**
+ * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView}.
+ */
+public class TiledImageView extends FrameLayout {
+
+ @Thunk
+ GLSurfaceView mGLSurfaceView;
+ @Thunk boolean mInvalPending = false;
+ private FrameCallback mFrameCallback;
+
+ protected static class ImageRendererWrapper {
+ // Guarded by locks
+ public float scale;
+ public int centerX, centerY;
+ public int rotation;
+ public TileSource source;
+ Runnable isReadyCallback;
+
+ // GL thread only
+ TiledImageRenderer image;
+ }
+
+ private float[] mValues = new float[9];
+
+ // -------------------------
+ // Guarded by mLock
+ // -------------------------
+ protected Object mLock = new Object();
+ protected ImageRendererWrapper mRenderer;
+
+ public TiledImageView(Context context) {
+ this(context, null);
+ }
+
+ public TiledImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mRenderer = new ImageRendererWrapper();
+ mRenderer.image = new TiledImageRenderer(this);
+ mGLSurfaceView = new GLSurfaceView(context);
+ mGLSurfaceView.setEGLContextClientVersion(2);
+ mGLSurfaceView.setRenderer(new TileRenderer());
+ mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+ addView(mGLSurfaceView, new LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ //setTileSource(new ColoredTiles());
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ // need to update inner view's visibility because it seems like we're causing it to draw
+ // from {@link #dispatchDraw} or {@link #invalidate} even if we are invisible.
+ mGLSurfaceView.setVisibility(visibility);
+ }
+
+ public void destroy() {
+ mGLSurfaceView.queueEvent(mFreeTextures);
+ }
+
+ private Runnable mFreeTextures = new Runnable() {
+
+ @Override
+ public void run() {
+ mRenderer.image.freeTextures();
+ }
+ };
+
+ public void onPause() {
+ mGLSurfaceView.onPause();
+ }
+
+ public void onResume() {
+ mGLSurfaceView.onResume();
+ }
+
+ public void setTileSource(TileSource source, Runnable isReadyCallback) {
+ 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();
+ }
+
+ public TileSource getTileSource() {
+ return mRenderer.source;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right,
+ int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ 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
+ public void invalidate() {
+ invalOnVsync();
+ }
+
+ 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 (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();
+ }
+ }
+
+ @Thunk 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;
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/AlphaDisableableButton.java b/src/org/cyanogenmod/wallpaperpicker/AlphaDisableableButton.java
new file mode 100644
index 0000000..9a3d6c3
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/AlphaDisableableButton.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package org.cyanogenmod.wallpaperpicker;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Button;
+
+/**
+ * A Button which becomes translucent when it is disabled
+ */
+public class AlphaDisableableButton extends Button {
+ public static float DISABLED_ALPHA_VALUE = 0.4f;
+ public AlphaDisableableButton(Context context) {
+ this(context, null);
+ }
+
+ public AlphaDisableableButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AlphaDisableableButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ setLayerType(LAYER_TYPE_HARDWARE, null);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ if(enabled) {
+ setAlpha(1.0f);
+ } else {
+ setAlpha(DISABLED_ALPHA_VALUE);
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/CheckableFrameLayout.java b/src/org/cyanogenmod/wallpaperpicker/CheckableFrameLayout.java
new file mode 100644
index 0000000..8f1bc3a
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/CheckableFrameLayout.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 org.cyanogenmod.wallpaperpicker;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.FrameLayout;
+
+public class CheckableFrameLayout extends FrameLayout implements Checkable {
+ private static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked };
+ boolean mChecked;
+
+ public CheckableFrameLayout(Context context) {
+ super(context);
+ }
+
+ public CheckableFrameLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public CheckableFrameLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ public void setChecked(boolean checked) {
+ if (checked != mChecked) {
+ mChecked = checked;
+ refreshDrawableState();
+ }
+ }
+
+ public void toggle() {
+ setChecked(!mChecked);
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (isChecked()) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/CropView.java b/src/org/cyanogenmod/wallpaperpicker/CropView.java
new file mode 100644
index 0000000..118efd3
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/CropView.java
@@ -0,0 +1,321 @@
+/*
+ * 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 org.cyanogenmod.wallpaperpicker;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.ScaleGestureDetector.OnScaleGestureListener;
+import android.view.ViewConfiguration;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+
+import com.android.photos.views.TiledImageRenderer.TileSource;
+import com.android.photos.views.TiledImageView;
+
+public class CropView extends TiledImageView implements OnScaleGestureListener {
+
+ private ScaleGestureDetector mScaleGestureDetector;
+ private long mTouchDownTime;
+ private float mFirstX, mFirstY;
+ private float mLastX, mLastY;
+ private float mCenterX, mCenterY;
+ private float mMinScale;
+ private boolean mTouchEnabled = true;
+ private RectF mTempEdges = new RectF();
+ private float[] mTempPoint = new float[] { 0, 0 };
+ private float[] mTempCoef = new float[] { 0, 0 };
+ private float[] mTempAdjustment = new float[] { 0, 0 };
+ private float[] mTempImageDims = new float[] { 0, 0 };
+ private float[] mTempRendererCenter = new float[] { 0, 0 };
+ TouchCallback mTouchCallback;
+ Matrix mRotateMatrix;
+ Matrix mInverseRotateMatrix;
+
+ public interface TouchCallback {
+ void onTouchDown();
+ void onTap();
+ void onTouchUp();
+ }
+
+ public CropView(Context context) {
+ this(context, null);
+ }
+
+ public CropView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mScaleGestureDetector = new ScaleGestureDetector(context, this);
+ mRotateMatrix = new Matrix();
+ mInverseRotateMatrix = new Matrix();
+ }
+
+ private float[] getImageDims() {
+ final float imageWidth = mRenderer.source.getImageWidth();
+ final float imageHeight = mRenderer.source.getImageHeight();
+ float[] imageDims = mTempImageDims;
+ imageDims[0] = imageWidth;
+ imageDims[1] = imageHeight;
+ mRotateMatrix.mapPoints(imageDims);
+ imageDims[0] = Math.abs(imageDims[0]);
+ imageDims[1] = Math.abs(imageDims[1]);
+ return imageDims;
+ }
+
+ private void getEdgesHelper(RectF edgesOut) {
+ final float width = getWidth();
+ final float height = getHeight();
+ final float[] imageDims = getImageDims();
+ final float imageWidth = imageDims[0];
+ final float imageHeight = imageDims[1];
+
+ float initialCenterX = mRenderer.source.getImageWidth() / 2f;
+ float initialCenterY = mRenderer.source.getImageHeight() / 2f;
+
+ float[] rendererCenter = mTempRendererCenter;
+ rendererCenter[0] = mCenterX - initialCenterX;
+ rendererCenter[1] = mCenterY - initialCenterY;
+ mRotateMatrix.mapPoints(rendererCenter);
+ rendererCenter[0] += imageWidth / 2;
+ rendererCenter[1] += imageHeight / 2;
+
+ final float scale = mRenderer.scale;
+ float centerX = (width / 2f - rendererCenter[0] + (imageWidth - width) / 2f)
+ * scale + width / 2f;
+ float centerY = (height / 2f - rendererCenter[1] + (imageHeight - height) / 2f)
+ * scale + height / 2f;
+ float leftEdge = centerX - imageWidth / 2f * scale;
+ float rightEdge = centerX + imageWidth / 2f * scale;
+ float topEdge = centerY - imageHeight / 2f * scale;
+ float bottomEdge = centerY + imageHeight / 2f * scale;
+
+ edgesOut.left = leftEdge;
+ edgesOut.right = rightEdge;
+ edgesOut.top = topEdge;
+ edgesOut.bottom = bottomEdge;
+ }
+
+ public int getImageRotation() {
+ return mRenderer.rotation;
+ }
+
+ public RectF getCrop() {
+ final RectF edges = mTempEdges;
+ getEdgesHelper(edges);
+ final float scale = mRenderer.scale;
+
+ float cropLeft = -edges.left / scale;
+ float cropTop = -edges.top / scale;
+ float cropRight = cropLeft + getWidth() / scale;
+ float cropBottom = cropTop + getHeight() / scale;
+
+ return new RectF(cropLeft, cropTop, cropRight, cropBottom);
+ }
+
+ public Point getSourceDimensions() {
+ return new Point(mRenderer.source.getImageWidth(), mRenderer.source.getImageHeight());
+ }
+
+ public void setTileSource(TileSource source, Runnable isReadyCallback) {
+ super.setTileSource(source, isReadyCallback);
+ mCenterX = mRenderer.centerX;
+ mCenterY = mRenderer.centerY;
+ mRotateMatrix.reset();
+ mRotateMatrix.setRotate(mRenderer.rotation);
+ mInverseRotateMatrix.reset();
+ mInverseRotateMatrix.setRotate(-mRenderer.rotation);
+ updateMinScale(getWidth(), getHeight(), source, true);
+ }
+
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ updateMinScale(w, h, mRenderer.source, false);
+ }
+
+ public void setScale(float scale) {
+ synchronized (mLock) {
+ mRenderer.scale = scale;
+ }
+ }
+
+ private void updateMinScale(int w, int h, TileSource source, boolean resetScale) {
+ synchronized (mLock) {
+ if (resetScale) {
+ mRenderer.scale = 1;
+ }
+ if (source != null) {
+ final float[] imageDims = getImageDims();
+ final float imageWidth = imageDims[0];
+ final float imageHeight = imageDims[1];
+ mMinScale = Math.max(w / imageWidth, h / imageHeight);
+ mRenderer.scale =
+ Math.max(mMinScale, resetScale ? Float.MIN_VALUE : mRenderer.scale);
+ }
+ }
+ }
+
+ @Override
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ return true;
+ }
+
+ @Override
+ public boolean onScale(ScaleGestureDetector detector) {
+ // Don't need the lock because this will only fire inside of
+ // onTouchEvent
+ mRenderer.scale *= detector.getScaleFactor();
+ mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ }
+
+ public void moveToLeft() {
+ if (getWidth() == 0 || getHeight() == 0) {
+ final ViewTreeObserver observer = getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
+ public void onGlobalLayout() {
+ moveToLeft();
+ getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+ });
+ }
+ final RectF edges = mTempEdges;
+ getEdgesHelper(edges);
+ final float scale = mRenderer.scale;
+ mCenterX += Math.ceil(edges.left / scale);
+ updateCenter();
+ }
+
+ private void updateCenter() {
+ mRenderer.centerX = Math.round(mCenterX);
+ mRenderer.centerY = Math.round(mCenterY);
+ }
+
+ public void setTouchEnabled(boolean enabled) {
+ mTouchEnabled = enabled;
+ }
+
+ public void setTouchCallback(TouchCallback cb) {
+ mTouchCallback = cb;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+ final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
+ final int skipIndex = pointerUp ? event.getActionIndex() : -1;
+
+ // Determine focal point
+ float sumX = 0, sumY = 0;
+ final int count = event.getPointerCount();
+ for (int i = 0; i < count; i++) {
+ if (skipIndex == i)
+ continue;
+ sumX += event.getX(i);
+ sumY += event.getY(i);
+ }
+ final int div = pointerUp ? count - 1 : count;
+ float x = sumX / div;
+ float y = sumY / div;
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mFirstX = x;
+ mFirstY = y;
+ mTouchDownTime = System.currentTimeMillis();
+ if (mTouchCallback != null) {
+ mTouchCallback.onTouchDown();
+ }
+ } else if (action == MotionEvent.ACTION_UP) {
+ ViewConfiguration config = ViewConfiguration.get(getContext());
+
+ float squaredDist = (mFirstX - x) * (mFirstX - x) + (mFirstY - y) * (mFirstY - y);
+ float slop = config.getScaledTouchSlop() * config.getScaledTouchSlop();
+ long now = System.currentTimeMillis();
+ if (mTouchCallback != null) {
+ // only do this if it's a small movement
+ if (squaredDist < slop &&
+ now < mTouchDownTime + ViewConfiguration.getTapTimeout()) {
+ mTouchCallback.onTap();
+ }
+ mTouchCallback.onTouchUp();
+ }
+ }
+
+ if (!mTouchEnabled) {
+ return true;
+ }
+
+ synchronized (mLock) {
+ mScaleGestureDetector.onTouchEvent(event);
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ float[] point = mTempPoint;
+ point[0] = (mLastX - x) / mRenderer.scale;
+ point[1] = (mLastY - y) / mRenderer.scale;
+ mInverseRotateMatrix.mapPoints(point);
+ mCenterX += point[0];
+ mCenterY += point[1];
+ updateCenter();
+ invalidate();
+ break;
+ }
+ if (mRenderer.source != null) {
+ // Adjust position so that the wallpaper covers the entire area
+ // of the screen
+ final RectF edges = mTempEdges;
+ getEdgesHelper(edges);
+ final float scale = mRenderer.scale;
+
+ float[] coef = mTempCoef;
+ coef[0] = 1;
+ coef[1] = 1;
+ mRotateMatrix.mapPoints(coef);
+ float[] adjustment = mTempAdjustment;
+ mTempAdjustment[0] = 0;
+ mTempAdjustment[1] = 0;
+ if (edges.left > 0) {
+ adjustment[0] = edges.left / scale;
+ } else if (edges.right < getWidth()) {
+ adjustment[0] = (edges.right - getWidth()) / scale;
+ }
+ if (edges.top > 0) {
+ adjustment[1] = (float) Math.ceil(edges.top / scale);
+ } else if (edges.bottom < getHeight()) {
+ adjustment[1] = (edges.bottom - getHeight()) / scale;
+ }
+ for (int dim = 0; dim <= 1; dim++) {
+ if (coef[dim] > 0) adjustment[dim] = (float) Math.ceil(adjustment[dim]);
+ }
+
+ mInverseRotateMatrix.mapPoints(adjustment);
+ mCenterX += adjustment[0];
+ mCenterY += adjustment[1];
+ updateCenter();
+ }
+ }
+
+ mLastX = x;
+ mLastY = y;
+ return true;
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/DrawableTileSource.java b/src/org/cyanogenmod/wallpaperpicker/DrawableTileSource.java
new file mode 100644
index 0000000..5752047
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/DrawableTileSource.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 org.cyanogenmod.wallpaperpicker;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import com.android.gallery3d.glrenderer.BasicTexture;
+import com.android.gallery3d.glrenderer.BitmapTexture;
+import com.android.photos.views.TiledImageRenderer;
+
+public class DrawableTileSource implements TiledImageRenderer.TileSource {
+ private static final int GL_SIZE_LIMIT = 2048;
+ // This must be no larger than half the size of the GL_SIZE_LIMIT
+ // due to decodePreview being allowed to be up to 2x the size of the target
+ public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2;
+
+ private int mTileSize;
+ private int mPreviewSize;
+ private Drawable mDrawable;
+ private BitmapTexture mPreview;
+
+ public DrawableTileSource(Context context, Drawable d, int previewSize) {
+ mTileSize = TiledImageRenderer.suggestedTileSize(context);
+ mDrawable = d;
+ mPreviewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
+ }
+
+ @Override
+ public int getTileSize() {
+ return mTileSize;
+ }
+
+ @Override
+ public int getImageWidth() {
+ return mDrawable.getIntrinsicWidth();
+ }
+
+ @Override
+ public int getImageHeight() {
+ return mDrawable.getIntrinsicHeight();
+ }
+
+ @Override
+ public int getRotation() {
+ return 0;
+ }
+
+ @Override
+ public BasicTexture getPreview() {
+ if (mPreviewSize == 0) {
+ return null;
+ }
+ if (mPreview == null){
+ float width = getImageWidth();
+ float height = getImageHeight();
+ while (width > MAX_PREVIEW_SIZE || height > MAX_PREVIEW_SIZE) {
+ width /= 2;
+ height /= 2;
+ }
+ Bitmap b = Bitmap.createBitmap((int) width, (int) height, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(b);
+ mDrawable.setBounds(new Rect(0, 0, (int) width, (int) height));
+ mDrawable.draw(c);
+ c.setBitmap(null);
+ mPreview = new BitmapTexture(b);
+ }
+ return mPreview;
+ }
+
+ @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);
+ }
+ Canvas c = new Canvas(bitmap);
+ Rect bounds = new Rect(0, 0, getImageWidth(), getImageHeight());
+ bounds.offset(-x, -y);
+ mDrawable.setBounds(bounds);
+ mDrawable.draw(c);
+ c.setBitmap(null);
+ return bitmap;
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/LiveWallpaperListAdapter.java b/src/org/cyanogenmod/wallpaperpicker/LiveWallpaperListAdapter.java
new file mode 100644
index 0000000..f72564f
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/LiveWallpaperListAdapter.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 org.cyanogenmod.wallpaperpicker;
+
+import android.app.WallpaperInfo;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.service.wallpaper.WallpaperService;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import org.cyanogenmod.wallpaperpicker.util.Thunk;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class LiveWallpaperListAdapter extends BaseAdapter implements ListAdapter {
+ private static final String LOG_TAG = "LiveWallpaperListAdapter";
+
+ private final LayoutInflater mInflater;
+ private final PackageManager mPackageManager;
+
+ @Thunk
+ List<LiveWallpaperTile> mWallpapers;
+
+ @SuppressWarnings("unchecked")
+ public LiveWallpaperListAdapter(Context context) {
+ mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mPackageManager = context.getPackageManager();
+
+ List<ResolveInfo> list = mPackageManager.queryIntentServices(
+ new Intent(WallpaperService.SERVICE_INTERFACE),
+ PackageManager.GET_META_DATA);
+
+ mWallpapers = new ArrayList<LiveWallpaperTile>();
+
+ new LiveWallpaperEnumerator(context).execute(list);
+ }
+
+ public int getCount() {
+ if (mWallpapers == null) {
+ return 0;
+ }
+ return mWallpapers.size();
+ }
+
+ public LiveWallpaperTile getItem(int position) {
+ return mWallpapers.get(position);
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view;
+
+ if (convertView == null) {
+ view = mInflater.inflate(R.layout.wallpaper_picker_live_wallpaper_item, parent, false);
+ } else {
+ view = convertView;
+ }
+
+ LiveWallpaperTile wallpaperInfo = mWallpapers.get(position);
+ wallpaperInfo.setView(view);
+ ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image);
+ ImageView icon = (ImageView) view.findViewById(R.id.wallpaper_icon);
+ if (wallpaperInfo.mThumbnail != null) {
+ image.setImageDrawable(wallpaperInfo.mThumbnail);
+ icon.setVisibility(View.GONE);
+ } else {
+ icon.setImageDrawable(wallpaperInfo.mInfo.loadIcon(mPackageManager));
+ icon.setVisibility(View.VISIBLE);
+ }
+
+ TextView label = (TextView) view.findViewById(R.id.wallpaper_item_label);
+ label.setText(wallpaperInfo.mInfo.loadLabel(mPackageManager));
+
+ return view;
+ }
+
+ public static class LiveWallpaperTile extends WallpaperPickerActivity.WallpaperTileInfo {
+ @Thunk Drawable mThumbnail;
+ @Thunk WallpaperInfo mInfo;
+ public LiveWallpaperTile(Drawable thumbnail, WallpaperInfo info, Intent intent) {
+ mThumbnail = thumbnail;
+ mInfo = info;
+ }
+ @Override
+ public void onClick(WallpaperPickerActivity a) {
+ Intent preview = new Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER);
+ preview.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,
+ mInfo.getComponent());
+ a.startActivityForResultSafely(preview,
+ WallpaperPickerActivity.PICK_WALLPAPER_THIRD_PARTY_ACTIVITY);
+ }
+ }
+
+ private class LiveWallpaperEnumerator extends
+ AsyncTask<List<ResolveInfo>, LiveWallpaperTile, Void> {
+ private Context mContext;
+ private int mWallpaperPosition;
+
+ public LiveWallpaperEnumerator(Context context) {
+ super();
+ mContext = context;
+ mWallpaperPosition = 0;
+ }
+
+ @Override
+ protected Void doInBackground(List<ResolveInfo>... params) {
+ final PackageManager packageManager = mContext.getPackageManager();
+
+ List<ResolveInfo> list = params[0];
+
+ Collections.sort(list, new Comparator<ResolveInfo>() {
+ final Collator mCollator;
+
+ {
+ mCollator = Collator.getInstance();
+ }
+
+ public int compare(ResolveInfo info1, ResolveInfo info2) {
+ return mCollator.compare(info1.loadLabel(packageManager),
+ info2.loadLabel(packageManager));
+ }
+ });
+
+ for (ResolveInfo resolveInfo : list) {
+ WallpaperInfo info = null;
+ try {
+ info = new WallpaperInfo(mContext, resolveInfo);
+ } catch (XmlPullParserException e) {
+ Log.w(LOG_TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e);
+ continue;
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "Skipping wallpaper " + resolveInfo.serviceInfo, e);
+ continue;
+ }
+
+
+ Drawable thumb = info.loadThumbnail(packageManager);
+ Intent launchIntent = new Intent(WallpaperService.SERVICE_INTERFACE);
+ launchIntent.setClassName(info.getPackageName(), info.getServiceName());
+ LiveWallpaperTile wallpaper = new LiveWallpaperTile(thumb, info, launchIntent);
+ publishProgress(wallpaper);
+ }
+ // Send a null object to show loading is finished
+ publishProgress((LiveWallpaperTile) null);
+
+ return null;
+ }
+
+ @Override
+ protected void onProgressUpdate(LiveWallpaperTile...infos) {
+ for (LiveWallpaperTile info : infos) {
+ if (info == null) {
+ LiveWallpaperListAdapter.this.notifyDataSetChanged();
+ break;
+ }
+ if (info.mThumbnail != null) {
+ info.mThumbnail.setDither(true);
+ }
+ if (mWallpaperPosition < mWallpapers.size()) {
+ mWallpapers.set(mWallpaperPosition, info);
+ } else {
+ mWallpapers.add(info);
+ }
+ mWallpaperPosition++;
+ }
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/Partner.java b/src/org/cyanogenmod/wallpaperpicker/Partner.java
new file mode 100644
index 0000000..0e0d7d0
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/Partner.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cyanogenmod.wallpaperpicker;
+
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.util.Pair;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utilities to discover and interact with partner customizations. There can
+ * only be one set of customizations on a device, and it must be bundled with
+ * the system.
+ */
+public class Partner {
+
+ static final String TAG = "Launcher.Partner";
+
+ /** Marker action used to discover partner */
+ private static final String
+ ACTION_PARTNER_CUSTOMIZATION = "com.android.launcher3.action.PARTNER_CUSTOMIZATION";
+
+ public static final String RES_FOLDER = "partner_folder";
+ public static final String RES_WALLPAPERS = "partner_wallpapers";
+
+ public static final String RES_DEFAULT_WALLPAPER_HIDDEN = "default_wallpapper_hidden";
+ public static final String RES_SYSTEM_WALLPAPER_DIR = "system_wallpaper_directory";
+
+ private static boolean sSearched = false;
+ private static List<Partner> sPartners;
+
+ static {
+ sPartners = new ArrayList<Partner>();
+ }
+
+ /**
+ * Find and return first partner details, or {@code null} if none exists.
+ */
+ public static synchronized Partner get(PackageManager pm) {
+ getAllPartners(pm);
+ return sPartners.size() > 0 ? sPartners.get(0) : null;
+ }
+
+ /**
+ * Find and return all partner details, or {@code null} if none exists.
+ */
+ public static synchronized List<Partner> getAllPartners(PackageManager pm) {
+ if (!sSearched) {
+ List<Pair<String, Resources>> apkInfos =
+ Utilities.findSystemApks(ACTION_PARTNER_CUSTOMIZATION, pm);
+ for (Pair<String, Resources> apkInfo : apkInfos) {
+ sPartners.add(new Partner(apkInfo.first, apkInfo.second));
+ }
+ sSearched = true;
+ }
+ return sPartners;
+ }
+
+ private final String mPackageName;
+ private final Resources mResources;
+
+ private Partner(String packageName, Resources res) {
+ mPackageName = packageName;
+ mResources = res;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public Resources getResources() {
+ return mResources;
+ }
+
+ public boolean hasFolder() {
+ int folder = getResources().getIdentifier(Partner.RES_FOLDER,
+ "xml", getPackageName());
+ return folder != 0;
+ }
+
+ public boolean hideDefaultWallpaper() {
+ int resId = getResources().getIdentifier(RES_DEFAULT_WALLPAPER_HIDDEN, "bool",
+ getPackageName());
+ return resId != 0 && getResources().getBoolean(resId);
+ }
+
+ public File getWallpaperDirectory() {
+ int resId = getResources().getIdentifier(RES_SYSTEM_WALLPAPER_DIR, "string",
+ getPackageName());
+ return (resId != 0) ? new File(getResources().getString(resId)) : null;
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/PickerFiles.java b/src/org/cyanogenmod/wallpaperpicker/PickerFiles.java
new file mode 100644
index 0000000..eb3485e
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/PickerFiles.java
@@ -0,0 +1,30 @@
+package org.cyanogenmod.wallpaperpicker;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Central list of files the Launcher writes to the application data directory.
+ *
+ * To add a new Launcher file, create a String constant referring to the filename, and add it to
+ * ALL_FILES, as shown below.
+ */
+public class PickerFiles {
+
+ private static final String XML = ".xml";
+
+ public static final String DEFAULT_WALLPAPER_THUMBNAIL = "default_thumb2.jpg";
+ public static final String DEFAULT_WALLPAPER_THUMBNAIL_OLD = "default_thumb.jpg";
+ public static final String WALLPAPER_CROP_PREFERENCES_KEY =
+ "org.cyanogenmod.wallpaperpicker.WallpaperCropActivity";
+
+ public static final String WALLPAPER_IMAGES_DB = "saved_wallpaper_images.db";
+
+ public static final List<String> ALL_FILES = Collections.unmodifiableList(Arrays.asList(
+ DEFAULT_WALLPAPER_THUMBNAIL,
+ DEFAULT_WALLPAPER_THUMBNAIL_OLD,
+ WALLPAPER_CROP_PREFERENCES_KEY + XML,
+ WALLPAPER_IMAGES_DB));
+
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/SavedWallpaperImages.java b/src/org/cyanogenmod/wallpaperpicker/SavedWallpaperImages.java
new file mode 100644
index 0000000..c3e4bb4
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/SavedWallpaperImages.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 org.cyanogenmod.wallpaperpicker;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+
+
+public class SavedWallpaperImages extends BaseAdapter implements ListAdapter {
+ private static String TAG = "SavedWallpaperImages";
+ private ImageDb mDb;
+ private boolean mIsLockScreenWallpaper;
+ ArrayList<SavedWallpaperTile> mImages;
+ Context mContext;
+ LayoutInflater mLayoutInflater;
+
+ public static class SavedWallpaperTile extends WallpaperPickerActivity.FileWallpaperInfo {
+ private int mDbId;
+ public SavedWallpaperTile(int dbId, File target, Drawable thumb,
+ boolean isLockScreenWallpaper) {
+ super(target, thumb, isLockScreenWallpaper);
+ mDbId = dbId;
+ }
+
+ @Override
+ public void onDelete(WallpaperPickerActivity a) {
+ a.getSavedImages().deleteImage(mDbId);
+ }
+ }
+
+ public SavedWallpaperImages(Context context, boolean isLockScreenWallpaper) {
+ // We used to store the saved images in the cache directory, but that meant they'd get
+ // deleted sometimes-- move them to the data directory
+ ImageDb.moveFromCacheDirectoryIfNecessary(context);
+ mDb = new ImageDb(context);
+ mContext = context;
+ mLayoutInflater = LayoutInflater.from(context);
+ mIsLockScreenWallpaper = isLockScreenWallpaper;
+ }
+
+ public void loadThumbnailsAndImageIdList() {
+ mImages = new ArrayList<SavedWallpaperTile>();
+ SQLiteDatabase db = mDb.getReadableDatabase();
+ Cursor result = db.query(ImageDb.TABLE_NAME,
+ new String[] { ImageDb.COLUMN_ID,
+ ImageDb.COLUMN_IMAGE_THUMBNAIL_FILENAME,
+ ImageDb.COLUMN_IMAGE_FILENAME}, // cols to return
+ null, // select query
+ null, // args to select query
+ null,
+ null,
+ ImageDb.COLUMN_ID + " DESC",
+ null);
+
+ while (result.moveToNext()) {
+ String filename = result.getString(1);
+ File file = new File(mContext.getFilesDir(), filename);
+
+ Bitmap thumb = BitmapFactory.decodeFile(file.getAbsolutePath());
+ if (thumb != null) {
+ mImages.add(new SavedWallpaperTile(result.getInt(0),
+ new File(mContext.getFilesDir(), result.getString(2)),
+ new BitmapDrawable(thumb), mIsLockScreenWallpaper));
+ }
+ }
+ result.close();
+ }
+
+ public int getCount() {
+ return mImages.size();
+ }
+
+ public SavedWallpaperTile getItem(int position) {
+ return mImages.get(position);
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Drawable thumbDrawable = mImages.get(position).mThumb;
+ if (thumbDrawable == null) {
+ Log.e(TAG, "Error decoding thumbnail for wallpaper #" + position);
+ }
+ return WallpaperPickerActivity.createImageTileView(
+ mLayoutInflater, convertView, parent, thumbDrawable);
+ }
+
+ private Pair<String, String> getImageFilenames(int id) {
+ SQLiteDatabase db = mDb.getReadableDatabase();
+ Cursor result = db.query(ImageDb.TABLE_NAME,
+ new String[] { ImageDb.COLUMN_IMAGE_THUMBNAIL_FILENAME,
+ ImageDb.COLUMN_IMAGE_FILENAME }, // cols to return
+ ImageDb.COLUMN_ID + " = ?", // select query
+ new String[] { Integer.toString(id) }, // args to select query
+ null,
+ null,
+ null,
+ null);
+ if (result.getCount() > 0) {
+ result.moveToFirst();
+ String thumbFilename = result.getString(0);
+ String imageFilename = result.getString(1);
+ result.close();
+ return new Pair<String, String>(thumbFilename, imageFilename);
+ } else {
+ return null;
+ }
+ }
+
+ public void deleteImage(int id) {
+ Pair<String, String> filenames = getImageFilenames(id);
+ File imageFile = new File(mContext.getFilesDir(), filenames.first);
+ imageFile.delete();
+ File thumbFile = new File(mContext.getFilesDir(), filenames.second);
+ thumbFile.delete();
+ SQLiteDatabase db = mDb.getWritableDatabase();
+ db.delete(ImageDb.TABLE_NAME,
+ ImageDb.COLUMN_ID + " = ?", // SELECT query
+ new String[] {
+ Integer.toString(id) // args to SELECT query
+ });
+ }
+
+ public void writeImage(Bitmap thumbnail, byte[] imageBytes) {
+ try {
+ File imageFile = File.createTempFile("wallpaper", "", mContext.getFilesDir());
+ FileOutputStream imageFileStream =
+ mContext.openFileOutput(imageFile.getName(), Context.MODE_PRIVATE);
+ imageFileStream.write(imageBytes);
+ imageFileStream.close();
+
+ File thumbFile = File.createTempFile("wallpaperthumb", "", mContext.getFilesDir());
+ FileOutputStream thumbFileStream =
+ mContext.openFileOutput(thumbFile.getName(), Context.MODE_PRIVATE);
+ thumbnail.compress(Bitmap.CompressFormat.JPEG, 95, thumbFileStream);
+ thumbFileStream.close();
+
+ SQLiteDatabase db = mDb.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(ImageDb.COLUMN_IMAGE_THUMBNAIL_FILENAME, thumbFile.getName());
+ values.put(ImageDb.COLUMN_IMAGE_FILENAME, imageFile.getName());
+ db.insert(ImageDb.TABLE_NAME, null, values);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed writing images to storage " + e);
+ }
+ }
+
+ static class ImageDb extends SQLiteOpenHelper {
+ final static int DB_VERSION = 1;
+ final static String TABLE_NAME = "saved_wallpaper_images";
+ final static String COLUMN_ID = "id";
+ final static String COLUMN_IMAGE_THUMBNAIL_FILENAME = "image_thumbnail";
+ final static String COLUMN_IMAGE_FILENAME = "image";
+
+ Context mContext;
+
+ public ImageDb(Context context) {
+ super(context, context.getDatabasePath(PickerFiles.WALLPAPER_IMAGES_DB).getPath(),
+ null, DB_VERSION);
+ // Store the context for later use
+ mContext = context;
+ }
+
+ public static void moveFromCacheDirectoryIfNecessary(Context context) {
+ // We used to store the saved images in the cache directory, but that meant they'd get
+ // deleted sometimes-- move them to the data directory
+ File oldSavedImagesFile = new File(context.getCacheDir(),
+ PickerFiles.WALLPAPER_IMAGES_DB);
+ File savedImagesFile = context.getDatabasePath(PickerFiles.WALLPAPER_IMAGES_DB);
+ if (oldSavedImagesFile.exists()) {
+ oldSavedImagesFile.renameTo(savedImagesFile);
+ }
+ }
+ @Override
+ public void onCreate(SQLiteDatabase database) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
+ COLUMN_ID + " INTEGER NOT NULL, " +
+ COLUMN_IMAGE_THUMBNAIL_FILENAME + " TEXT NOT NULL, " +
+ COLUMN_IMAGE_FILENAME + " TEXT NOT NULL, " +
+ "PRIMARY KEY (" + COLUMN_ID + " ASC) " +
+ ");");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion != newVersion) {
+ // Delete all the records; they'll be repopulated as this is a cache
+ db.execSQL("DELETE FROM " + TABLE_NAME);
+ }
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/StylusEventHelper.java b/src/org/cyanogenmod/wallpaperpicker/StylusEventHelper.java
new file mode 100644
index 0000000..d6d9649
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/StylusEventHelper.java
@@ -0,0 +1,82 @@
+package org.cyanogenmod.wallpaperpicker;
+
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+/**
+ * Helper for identifying when a stylus touches a view while the primary stylus button is pressed.
+ * This can occur in {@value MotionEvent#ACTION_DOWN} or {@value MotionEvent#ACTION_MOVE}. On a
+ * stylus button press this performs the view's {@link View#performLongClick()} method, if the view
+ * is long clickable.
+ */
+public class StylusEventHelper {
+ private boolean mIsButtonPressed;
+ private View mView;
+
+ public StylusEventHelper(View view) {
+ mView = view;
+ }
+
+ /**
+ * Call this in onTouchEvent method of a view to identify a stylus button press and perform a
+ * long click (if the view is long clickable).
+ *
+ * @param event The event to check for a stylus button press.
+ * @return Whether a stylus event occurred and was handled.
+ */
+ public boolean checkAndPerformStylusEvent(MotionEvent event) {
+ final float slop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop();
+
+ if (!mView.isLongClickable()) {
+ // We don't do anything unless the view is long clickable.
+ return false;
+ }
+
+ final boolean stylusButtonPressed = isStylusButtonPressed(event);
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mIsButtonPressed = false;
+ if (stylusButtonPressed && mView.performLongClick()) {
+ mIsButtonPressed = true;
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (Utilities.pointInView(mView, event.getX(), event.getY(), slop)) {
+ if (!mIsButtonPressed && stylusButtonPressed && mView.performLongClick()) {
+ mIsButtonPressed = true;
+ return true;
+ } else if (mIsButtonPressed && !stylusButtonPressed) {
+ mIsButtonPressed = false;
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mIsButtonPressed = false;
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Whether a stylus button press is occurring.
+ */
+ public boolean inStylusButtonPressed() {
+ return mIsButtonPressed;
+ }
+
+ /**
+ * Identifies if the provided {@link MotionEvent} is a stylus with the primary stylus button
+ * pressed.
+ *
+ * @param event The event to check.
+ * @return Whether a stylus button press occurred.
+ */
+ private static boolean isStylusButtonPressed(MotionEvent event) {
+ return event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS
+ && ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY)
+ == MotionEvent.BUTTON_SECONDARY);
+ }
+} \ No newline at end of file
diff --git a/src/org/cyanogenmod/wallpaperpicker/ThirdPartyWallpaperPickerListAdapter.java b/src/org/cyanogenmod/wallpaperpicker/ThirdPartyWallpaperPickerListAdapter.java
new file mode 100644
index 0000000..25465aa
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/ThirdPartyWallpaperPickerListAdapter.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 org.cyanogenmod.wallpaperpicker;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import org.cyanogenmod.wallpaperpicker.util.Thunk;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ThirdPartyWallpaperPickerListAdapter extends BaseAdapter implements ListAdapter {
+ private final LayoutInflater mInflater;
+ private final PackageManager mPackageManager;
+ private final int mIconSize;
+
+ private List<ThirdPartyWallpaperTile> mThirdPartyWallpaperPickers =
+ new ArrayList<ThirdPartyWallpaperTile>();
+
+ public static class ThirdPartyWallpaperTile extends WallpaperPickerActivity.WallpaperTileInfo {
+ @Thunk
+ ResolveInfo mResolveInfo;
+ public ThirdPartyWallpaperTile(ResolveInfo resolveInfo) {
+ mResolveInfo = resolveInfo;
+ }
+ @Override
+ public void onClick(WallpaperPickerActivity a) {
+ final ComponentName itemComponentName = new ComponentName(
+ mResolveInfo.activityInfo.packageName, mResolveInfo.activityInfo.name);
+ Intent launchIntent = new Intent(Intent.ACTION_SET_WALLPAPER);
+ launchIntent.setComponent(itemComponentName);
+ a.startActivityForResultSafely(
+ launchIntent, WallpaperPickerActivity.PICK_WALLPAPER_THIRD_PARTY_ACTIVITY);
+ }
+ }
+
+ public ThirdPartyWallpaperPickerListAdapter(Context context) {
+ mInflater = LayoutInflater.from(context);
+ mPackageManager = context.getPackageManager();
+ mIconSize = context.getResources().getDimensionPixelSize(R.dimen.wallpaperItemIconSize);
+ final PackageManager pm = mPackageManager;
+
+ final Intent pickWallpaperIntent = new Intent(Intent.ACTION_SET_WALLPAPER);
+ final List<ResolveInfo> apps =
+ pm.queryIntentActivities(pickWallpaperIntent, 0);
+
+ // Get list of image picker intents
+ Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
+ pickImageIntent.setType("image/*");
+ final List<ResolveInfo> imagePickerActivities =
+ pm.queryIntentActivities(pickImageIntent, 0);
+ final ComponentName[] imageActivities = new ComponentName[imagePickerActivities.size()];
+ for (int i = 0; i < imagePickerActivities.size(); i++) {
+ ActivityInfo activityInfo = imagePickerActivities.get(i).activityInfo;
+ imageActivities[i] = new ComponentName(activityInfo.packageName, activityInfo.name);
+ }
+
+ outerLoop:
+ for (ResolveInfo info : apps) {
+ final ComponentName itemComponentName =
+ new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
+ final String itemPackageName = itemComponentName.getPackageName();
+ // Exclude anything from our own package, and the old Launcher,
+ // and live wallpaper picker
+ if (itemPackageName.equals(context.getPackageName()) ||
+ itemPackageName.equals("com.android.launcher") ||
+ itemPackageName.equals("com.android.wallpaper.livepicker")) {
+ continue;
+ }
+ // Exclude any package that already responds to the image picker intent
+ for (ResolveInfo imagePickerActivityInfo : imagePickerActivities) {
+ if (itemPackageName.equals(
+ imagePickerActivityInfo.activityInfo.packageName)) {
+ continue outerLoop;
+ }
+ }
+ mThirdPartyWallpaperPickers.add(new ThirdPartyWallpaperTile(info));
+ }
+ }
+
+ public int getCount() {
+ return mThirdPartyWallpaperPickers.size();
+ }
+
+ public ThirdPartyWallpaperTile getItem(int position) {
+ return mThirdPartyWallpaperPickers.get(position);
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view;
+
+ if (convertView == null) {
+ view = mInflater.inflate(R.layout.wallpaper_picker_third_party_item, parent, false);
+ } else {
+ view = convertView;
+ }
+
+ ResolveInfo info = mThirdPartyWallpaperPickers.get(position).mResolveInfo;
+ TextView label = (TextView) view.findViewById(R.id.wallpaper_item_label);
+ label.setText(info.loadLabel(mPackageManager));
+ Drawable icon = info.loadIcon(mPackageManager);
+ icon.setBounds(new Rect(0, 0, mIconSize, mIconSize));
+ label.setCompoundDrawables(null, icon, null, null);
+ return view;
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/Utilities.java b/src/org/cyanogenmod/wallpaperpicker/Utilities.java
new file mode 100644
index 0000000..21a22be
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/Utilities.java
@@ -0,0 +1,549 @@
+/*
+ * 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 org.cyanogenmod.wallpaperpicker;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.SearchManager;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.AssetManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PaintFlagsDrawFilter;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.Toast;
+import org.cyanogenmod.wallpaperpicker.util.WallpaperUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Various utilities shared amongst the Launcher's classes.
+ */
+public final class Utilities {
+
+ private static final String TAG = "WallpaperPicker.Utilities";
+
+ private static final float WALLPAPER_SCREENS_SPAN = 2f;
+
+ private static final Rect sOldBounds = new Rect();
+ private static final Canvas sCanvas = new Canvas();
+
+ private static final Pattern sTrimPattern =
+ Pattern.compile("^[\\s|\\p{javaSpaceChar}]*(.*)[\\s|\\p{javaSpaceChar}]*$");
+
+ static {
+ sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG,
+ Paint.FILTER_BITMAP_FLAG));
+ }
+ static int sColors[] = { 0xffff0000, 0xff00ff00, 0xff0000ff };
+ static int sColorIndex = 0;
+
+ private static final int[] sLoc0 = new int[2];
+ private static final int[] sLoc1 = new int[2];
+
+ // TODO: use Build.VERSION_CODES when available
+ public static final boolean ATLEAST_MARSHMALLOW = Build.VERSION.SDK_INT >= 23;
+
+ public static final boolean ATLEAST_LOLLIPOP_MR1 =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1;
+
+ public static final boolean ATLEAST_LOLLIPOP =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
+
+ public static final boolean ATLEAST_KITKAT =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+
+ public static final boolean ATLEAST_JB_MR1 =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
+
+ public static final boolean ATLEAST_JB_MR2 =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
+
+ // To turn on these properties, type
+ // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS]
+ private static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate";
+ private static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY);
+
+ public static boolean isPropertyEnabled(String propertyName) {
+ return Log.isLoggable(propertyName, Log.VERBOSE);
+ }
+
+ /**
+ * Utility method to determine whether the given point, in local coordinates,
+ * is inside the view, where the area of the view is expanded by the slop factor.
+ * This method is called while processing touch-move events to determine if the event
+ * is still within the view.
+ */
+ public static boolean pointInView(View v, float localX, float localY, float slop) {
+ return localX >= -slop && localY >= -slop && localX < (v.getWidth() + slop) &&
+ localY < (v.getHeight() + slop);
+ }
+
+ public static void scaleRect(Rect r, float scale) {
+ if (scale != 1.0f) {
+ r.left = (int) (r.left * scale + 0.5f);
+ r.top = (int) (r.top * scale + 0.5f);
+ r.right = (int) (r.right * scale + 0.5f);
+ r.bottom = (int) (r.bottom * scale + 0.5f);
+ }
+ }
+
+ public static int[] getCenterDeltaInScreenSpace(View v0, View v1, int[] delta) {
+ v0.getLocationInWindow(sLoc0);
+ v1.getLocationInWindow(sLoc1);
+
+ sLoc0[0] += (v0.getMeasuredWidth() * v0.getScaleX()) / 2;
+ sLoc0[1] += (v0.getMeasuredHeight() * v0.getScaleY()) / 2;
+ sLoc1[0] += (v1.getMeasuredWidth() * v1.getScaleX()) / 2;
+ sLoc1[1] += (v1.getMeasuredHeight() * v1.getScaleY()) / 2;
+
+ if (delta == null) {
+ delta = new int[2];
+ }
+
+ delta[0] = sLoc1[0] - sLoc0[0];
+ delta[1] = sLoc1[1] - sLoc0[1];
+
+ return delta;
+ }
+
+ public static void scaleRectAboutCenter(Rect r, float scale) {
+ int cx = r.centerX();
+ int cy = r.centerY();
+ r.offset(-cx, -cy);
+ Utilities.scaleRect(r, scale);
+ r.offset(cx, cy);
+ }
+
+ static boolean isSystemApp(Context context, Intent intent) {
+ PackageManager pm = context.getPackageManager();
+ ComponentName cn = intent.getComponent();
+ String packageName = null;
+ if (cn == null) {
+ ResolveInfo info = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if ((info != null) && (info.activityInfo != null)) {
+ packageName = info.activityInfo.packageName;
+ }
+ } else {
+ packageName = cn.getPackageName();
+ }
+ if (packageName != null) {
+ try {
+ PackageInfo info = pm.getPackageInfo(packageName, 0);
+ return (info != null) && (info.applicationInfo != null) &&
+ ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0);
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * This picks a dominant color, looking for high-saturation, high-value, repeated hues.
+ * @param bitmap The bitmap to scan
+ * @param samples The approximate max number of samples to use.
+ */
+ static int findDominantColorByHue(Bitmap bitmap, int samples) {
+ final int height = bitmap.getHeight();
+ final int width = bitmap.getWidth();
+ int sampleStride = (int) Math.sqrt((height * width) / samples);
+ if (sampleStride < 1) {
+ sampleStride = 1;
+ }
+
+ // This is an out-param, for getting the hsv values for an rgb
+ float[] hsv = new float[3];
+
+ // First get the best hue, by creating a histogram over 360 hue buckets,
+ // where each pixel contributes a score weighted by saturation, value, and alpha.
+ float[] hueScoreHistogram = new float[360];
+ float highScore = -1;
+ int bestHue = -1;
+
+ for (int y = 0; y < height; y += sampleStride) {
+ for (int x = 0; x < width; x += sampleStride) {
+ int argb = bitmap.getPixel(x, y);
+ int alpha = 0xFF & (argb >> 24);
+ if (alpha < 0x80) {
+ // Drop mostly-transparent pixels.
+ continue;
+ }
+ // Remove the alpha channel.
+ int rgb = argb | 0xFF000000;
+ Color.colorToHSV(rgb, hsv);
+ // Bucket colors by the 360 integer hues.
+ int hue = (int) hsv[0];
+ if (hue < 0 || hue >= hueScoreHistogram.length) {
+ // Defensively avoid array bounds violations.
+ continue;
+ }
+ float score = hsv[1] * hsv[2];
+ hueScoreHistogram[hue] += score;
+ if (hueScoreHistogram[hue] > highScore) {
+ highScore = hueScoreHistogram[hue];
+ bestHue = hue;
+ }
+ }
+ }
+
+ SparseArray<Float> rgbScores = new SparseArray<Float>();
+ int bestColor = 0xff000000;
+ highScore = -1;
+ // Go back over the RGB colors that match the winning hue,
+ // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets.
+ // The highest-scoring RGB color wins.
+ for (int y = 0; y < height; y += sampleStride) {
+ for (int x = 0; x < width; x += sampleStride) {
+ int rgb = bitmap.getPixel(x, y) | 0xff000000;
+ Color.colorToHSV(rgb, hsv);
+ int hue = (int) hsv[0];
+ if (hue == bestHue) {
+ float s = hsv[1];
+ float v = hsv[2];
+ int bucket = (int) (s * 100) + (int) (v * 10000);
+ // Score by cumulative saturation * value.
+ float score = s * v;
+ Float oldTotal = rgbScores.get(bucket);
+ float newTotal = oldTotal == null ? score : oldTotal + score;
+ rgbScores.put(bucket, newTotal);
+ if (newTotal > highScore) {
+ highScore = newTotal;
+ // All the colors in the winning bucket are very similar. Last in wins.
+ bestColor = rgb;
+ }
+ }
+ }
+ }
+ return bestColor;
+ }
+
+ /*
+ * Finds a system apk which had a broadcast receiver listening to a particular action.
+ * @param action intent action used to find the apk
+ * @return a pair of apk package name and the resources.
+ */
+ static Pair<String, Resources> findSystemApk(String action, PackageManager pm) {
+ final Intent intent = new Intent(action);
+ for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) {
+ if (info.activityInfo != null &&
+ (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+ final String packageName = info.activityInfo.packageName;
+ try {
+ final Resources res = pm.getResourcesForApplication(packageName);
+ return Pair.create(packageName, res);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Failed to find resources for " + packageName);
+ }
+ }
+ }
+ return null;
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ public static boolean isViewAttachedToWindow(View v) {
+ if (ATLEAST_KITKAT) {
+ return v.isAttachedToWindow();
+ } else {
+ // A proxy call which returns null, if the view is not attached to the window.
+ return v.getKeyDispatcherState() != null;
+ }
+ }
+
+ /*
+ * Finds all system apks which had a broadcast receiver listening to a particular action.
+ * @param action intent action used to find the apk
+ * @return a list of pairs of apk package name and the resources.
+ */
+ static List<Pair<String, Resources>> findSystemApks(String action, PackageManager pm) {
+ final Intent intent = new Intent(action);
+ List<Pair<String, Resources>> systemApks = new ArrayList<Pair<String, Resources>>();
+ for (ResolveInfo info : pm.queryBroadcastReceivers(intent, 0)) {
+ if (info.activityInfo != null &&
+ (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
+ final String packageName = info.activityInfo.packageName;
+ try {
+ final Resources res = pm.getResourcesForApplication(packageName);
+ systemApks.add(Pair.create(packageName, res));
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Failed to find resources for " + packageName);
+ }
+ }
+ }
+ return systemApks;
+ }
+
+ /**
+ * Returns a widget with category {@link AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX}
+ * provided by the same package which is set to be global search activity.
+ * If widgetCategory is not supported, or no such widget is found, returns the first widget
+ * provided by the package.
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public static AppWidgetProviderInfo getSearchWidgetProvider(Context context) {
+ SearchManager searchManager =
+ (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
+ ComponentName searchComponent = searchManager.getGlobalSearchActivity();
+ if (searchComponent == null) return null;
+ String providerPkg = searchComponent.getPackageName();
+
+ AppWidgetProviderInfo defaultWidgetForSearchPackage = null;
+
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ for (AppWidgetProviderInfo info : appWidgetManager.getInstalledProviders()) {
+ if (info.provider.getPackageName().equals(providerPkg)) {
+ if (ATLEAST_JB_MR1) {
+ if ((info.widgetCategory & AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX) != 0) {
+ return info;
+ } else if (defaultWidgetForSearchPackage == null) {
+ defaultWidgetForSearchPackage = info;
+ }
+ } else {
+ return info;
+ }
+ }
+ }
+ return defaultWidgetForSearchPackage;
+ }
+
+ /**
+ * Compresses the bitmap to a byte array for serialization.
+ */
+ public static byte[] flattenBitmap(Bitmap bitmap) {
+ // 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);
+ try {
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
+ out.flush();
+ out.close();
+ return out.toByteArray();
+ } catch (IOException e) {
+ Log.w(TAG, "Could not write bitmap");
+ return null;
+ }
+ }
+
+ /**
+ * Find the first vacant cell, if there is one.
+ *
+ * @param vacant Holds the x and y coordinate of the vacant cell
+ * @param spanX Horizontal cell span.
+ * @param spanY Vertical cell span.
+ *
+ * @return true if a vacant cell was found
+ */
+ public static boolean findVacantCell(int[] vacant, int spanX, int spanY,
+ int xCount, int yCount, boolean[][] occupied) {
+
+ for (int y = 0; (y + spanY) <= yCount; y++) {
+ for (int x = 0; (x + spanX) <= xCount; x++) {
+ boolean available = !occupied[x][y];
+ out: for (int i = x; i < x + spanX; i++) {
+ for (int j = y; j < y + spanY; j++) {
+ available = available && !occupied[i][j];
+ if (!available) break out;
+ }
+ }
+
+ if (available) {
+ vacant[0] = x;
+ vacant[1] = y;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Trims the string, removing all whitespace at the beginning and end of the string.
+ * Non-breaking whitespaces are also removed.
+ */
+ public static String trim(CharSequence s) {
+ if (s == null) {
+ return null;
+ }
+
+ // Just strip any sequence of whitespace or java space characters from the beginning and end
+ Matcher m = sTrimPattern.matcher(s);
+ return m.replaceAll("$1");
+ }
+
+ /**
+ * Calculates the height of a given string at a specific text size.
+ */
+ public static float calculateTextHeight(float textSizePx) {
+ Paint p = new Paint();
+ p.setTextSize(textSizePx);
+ Paint.FontMetrics fm = p.getFontMetrics();
+ return -fm.top + fm.bottom;
+ }
+
+ /**
+ * Convenience println with multiple args.
+ */
+ public static void println(String key, Object... args) {
+ StringBuilder b = new StringBuilder();
+ b.append(key);
+ b.append(": ");
+ boolean isFirstArgument = true;
+ for (Object arg : args) {
+ if (isFirstArgument) {
+ isFirstArgument = false;
+ } else {
+ b.append(", ");
+ }
+ b.append(arg);
+ }
+ System.out.println(b.toString());
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public static boolean isRtl(Resources res) {
+ return ATLEAST_JB_MR1 &&
+ (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
+ }
+
+ public static float dpiFromPx(int size, DisplayMetrics metrics){
+ return (size / metrics.density);
+ }
+ public static int pxFromDp(float size, DisplayMetrics metrics) {
+ return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+ size, metrics));
+ }
+ public static int pxFromSp(float size, DisplayMetrics metrics) {
+ return (int) Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
+ size, metrics));
+ }
+
+ public static boolean isPackageInstalled(Context context, String pkg) {
+ PackageManager packageManager = context.getPackageManager();
+ try {
+ PackageInfo pi = packageManager.getPackageInfo(pkg, 0);
+ return pi.applicationInfo.enabled;
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ public static void startActivityForResultSafely(
+ Activity activity, Intent intent, int requestCode) {
+ try {
+ activity.startActivityForResult(intent, requestCode);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
+ } catch (SecurityException e) {
+ Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
+ Log.e(TAG, "Wallpaper picker does not have the permission to launch " + intent +
+ ". Make sure to create a MAIN intent-filter for the corresponding activity " +
+ "or use the exported attribute for this activity.", e);
+ }
+ }
+
+ public static Bitmap getThemeWallpaper(Context context, String path, String pkgName,
+ boolean thumb) {
+ InputStream is = null;
+ try {
+ Resources res = context.getPackageManager().getResourcesForApplication(pkgName);
+ if (res == null) {
+ return null;
+ }
+
+ AssetManager am = res.getAssets();
+ String[] wallpapers = am.list(path);
+ if (wallpapers == null || wallpapers.length == 0) {
+ return null;
+ }
+ is = am.open(path + File.separator + wallpapers[0]);
+
+ BitmapFactory.Options bounds = new BitmapFactory.Options();
+ bounds.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(is, null, bounds);
+ if ((bounds.outWidth == -1) || (bounds.outHeight == -1))
+ return null;
+
+ int originalSize = (bounds.outHeight > bounds.outWidth) ? bounds.outHeight
+ : bounds.outWidth;
+ Point outSize;
+
+ if (thumb) {
+ outSize = getDefaultThumbnailSize(context.getResources());
+ } else {
+ outSize = WallpaperUtils.getDefaultWallpaperSize(res,
+ ((Activity) context).getWindowManager());
+ }
+ int thumbSampleSize = (outSize.y > outSize.x) ? outSize.y : outSize.x;
+
+ BitmapFactory.Options opts = new BitmapFactory.Options();
+ opts.inSampleSize = originalSize / thumbSampleSize;
+ return BitmapFactory.decodeStream(is, null, opts);
+ } catch (IOException e) {
+ return null;
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ } catch (OutOfMemoryError e) {
+ return null;
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+
+ public static Point getDefaultThumbnailSize(Resources res) {
+ return new Point(res.getDimensionPixelSize(R.dimen.wallpaperThumbnailWidth),
+ res.getDimensionPixelSize(R.dimen.wallpaperThumbnailHeight));
+
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/WallpaperCropActivity.java b/src/org/cyanogenmod/wallpaperpicker/WallpaperCropActivity.java
new file mode 100644
index 0000000..945ccac
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/WallpaperCropActivity.java
@@ -0,0 +1,540 @@
+/*
+ * 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 org.cyanogenmod.wallpaperpicker;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.util.Log;
+import android.view.Display;
+import android.view.View;
+import android.widget.Toast;
+
+import com.android.gallery3d.common.BitmapCropTask;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.photos.BitmapRegionTileSource;
+import com.android.photos.BitmapRegionTileSource.BitmapSource;
+import com.android.photos.BitmapRegionTileSource.BitmapSource.InBitmapProvider;
+import com.android.photos.views.TiledImageRenderer.TileSource;
+
+import org.cyanogenmod.wallpaperpicker.base.BaseActivity;
+import org.cyanogenmod.wallpaperpicker.util.Thunk;
+import org.cyanogenmod.wallpaperpicker.util.WallpaperUtils;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+public class WallpaperCropActivity extends BaseActivity implements Handler.Callback {
+ private static final String LOGTAG = "CropActivity";
+
+ private static final int REQUEST_CODE_STORAGE_PERMISSION_CHECK = 100;
+
+ protected static final String WALLPAPER_WIDTH_KEY = WallpaperUtils.WALLPAPER_WIDTH_KEY;
+ protected static final String WALLPAPER_HEIGHT_KEY = WallpaperUtils.WALLPAPER_HEIGHT_KEY;
+
+ /**
+ * 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;
+ public static final float WALLPAPER_SCREENS_SPAN = WallpaperUtils.WALLPAPER_SCREENS_SPAN;
+
+ private static final int MSG_LOAD_IMAGE = 1;
+
+ protected CropView mCropView;
+ protected View mProgressView;
+ protected Uri mUri;
+ protected View mSetWallpaperButton;
+
+ private HandlerThread mLoaderThread;
+ private Handler mLoaderHandler;
+ @Thunk
+ LoadRequest mCurrentLoadRequest;
+ private byte[] mTempStorageForDecoding = new byte[16 * 1024];
+ // A weak-set of reusable bitmaps
+ @Thunk Set<Bitmap> mReusableBitmaps =
+ Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>());
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (!hasStoragePermissions()) {
+ requestStoragePermissions();
+ } else {
+ load();
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ int[] grantResults) {
+ if (requestCode == REQUEST_CODE_STORAGE_PERMISSION_CHECK) {
+ for (int i = 0; i < permissions.length; i++ ) {
+ final String permission = permissions[i];
+ final int grantResult = grantResults[i];
+ if (permission.equals(Manifest.permission.READ_EXTERNAL_STORAGE)) {
+ if (grantResult == PackageManager.PERMISSION_GRANTED) {
+ load();
+ } else {
+ Toast.makeText(this, getString(R.string.storage_permission_denied),
+ Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
+ }
+ }
+ }
+
+ private void load() {
+ mLoaderThread = new HandlerThread("wallpaper_loader");
+ mLoaderThread.start();
+ mLoaderHandler = new Handler(mLoaderThread.getLooper(), this);
+
+ init();
+ }
+
+ protected void init() {
+ setContentView(R.layout.wallpaper_cropper);
+
+ mCropView = (CropView) findViewById(R.id.cropView);
+ mProgressView = findViewById(R.id.loading);
+
+ Intent cropIntent = getIntent();
+ final Uri imageUri = cropIntent.getData();
+
+ if (imageUri == null) {
+ Log.e(LOGTAG, "No URI passed in intent, exiting WallpaperCropActivity");
+ finish();
+ return;
+ }
+
+ // Action bar
+ // Show the custom action bar view
+ final ActionBar actionBar = getActionBar();
+ actionBar.setCustomView(R.layout.actionbar_set_wallpaper);
+ actionBar.getCustomView().setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ boolean finishActivityWhenDone = true;
+ cropImageAndSetWallpaper(imageUri, null, false, finishActivityWhenDone);
+ }
+ });
+ mSetWallpaperButton = findViewById(R.id.set_wallpaper_button);
+
+ // Load image in background
+ final BitmapRegionTileSource.UriBitmapSource bitmapSource =
+ new BitmapRegionTileSource.UriBitmapSource(getContext(), imageUri);
+ mSetWallpaperButton.setEnabled(false);
+ Runnable onLoad = new Runnable() {
+ public void run() {
+ if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) {
+ Toast.makeText(getContext(), R.string.wallpaper_load_fail,
+ Toast.LENGTH_LONG).show();
+ finish();
+ } else {
+ mSetWallpaperButton.setEnabled(true);
+ }
+ }
+ };
+ setCropViewTileSource(bitmapSource, true, false, null, onLoad);
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mCropView != null) {
+ mCropView.destroy();
+ }
+ if (mLoaderThread != null) {
+ mLoaderThread.quit();
+ }
+ super.onDestroy();
+ }
+
+ /**
+ * This is called on {@link #mLoaderThread}
+ */
+ @Override
+ public boolean handleMessage(Message msg) {
+ if (msg.what == MSG_LOAD_IMAGE) {
+ final LoadRequest req = (LoadRequest) msg.obj;
+ try {
+ req.src.loadInBackground(new InBitmapProvider() {
+
+ @Override
+ public Bitmap forPixelCount(int count) {
+ Bitmap bitmapToReuse = null;
+ // Find the smallest bitmap that satisfies the pixel count limit
+ synchronized (mReusableBitmaps) {
+ int currentBitmapSize = Integer.MAX_VALUE;
+ for (Bitmap b : mReusableBitmaps) {
+ int bitmapSize = b.getWidth() * b.getHeight();
+ if ((bitmapSize >= count) && (bitmapSize < currentBitmapSize)) {
+ bitmapToReuse = b;
+ currentBitmapSize = bitmapSize;
+ }
+ }
+
+ if (bitmapToReuse != null) {
+ mReusableBitmaps.remove(bitmapToReuse);
+ }
+ }
+ return bitmapToReuse;
+ }
+ });
+ } catch (SecurityException securityException) {
+ if (isActivityDestroyed()) {
+ // Temporarily granted permissions are revoked when the activity
+ // finishes, potentially resulting in a SecurityException here.
+ // Even though {@link #isDestroyed} might also return true in different
+ // situations where the configuration changes, we are fine with
+ // catching these cases here as well.
+ return true;
+ } else {
+ // otherwise it had a different cause and we throw it further
+ throw securityException;
+ }
+ }
+
+ req.result = new BitmapRegionTileSource(getContext(), req.src, mTempStorageForDecoding);
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (req == mCurrentLoadRequest) {
+ onLoadRequestComplete(req,
+ req.src.getLoadingState() == BitmapSource.State.LOADED);
+ } else {
+ addReusableBitmap(req.result);
+ }
+ }
+ });
+ return true;
+ }
+ return false;
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ protected boolean isActivityDestroyed() {
+ return Utilities.ATLEAST_JB_MR1 && isDestroyed();
+ }
+
+ @Thunk void addReusableBitmap(TileSource src) {
+ synchronized (mReusableBitmaps) {
+ if (Utilities.ATLEAST_KITKAT && src instanceof BitmapRegionTileSource) {
+ Bitmap preview = ((BitmapRegionTileSource) src).getBitmap();
+ if (preview != null && preview.isMutable()) {
+ mReusableBitmaps.add(preview);
+ }
+ }
+ }
+ }
+
+ protected void onLoadRequestComplete(LoadRequest req, boolean success) {
+ mCurrentLoadRequest = null;
+ if (success) {
+ TileSource oldSrc = mCropView.getTileSource();
+ mCropView.setTileSource(req.result, null);
+ mCropView.setTouchEnabled(req.touchEnabled);
+ if (req.moveToLeft) {
+ mCropView.moveToLeft();
+ }
+ if (req.scaleProvider != null) {
+ mCropView.setScale(req.scaleProvider.getScale(req.result));
+ }
+
+ // Free last image
+ if (oldSrc != null) {
+ // Call yield instead of recycle, as we only want to free GL resource.
+ // We can still reuse the bitmap for decoding any other image.
+ oldSrc.getPreview().yield();
+ }
+ addReusableBitmap(oldSrc);
+ }
+ if (req.postExecute != null) {
+ req.postExecute.run();
+ }
+ mProgressView.setVisibility(View.GONE);
+ }
+
+ public final void setCropViewTileSource(BitmapSource bitmapSource, boolean touchEnabled,
+ boolean moveToLeft, CropViewScaleProvider scaleProvider, Runnable postExecute) {
+ final LoadRequest req = new LoadRequest();
+ req.moveToLeft = moveToLeft;
+ req.src = bitmapSource;
+ req.touchEnabled = touchEnabled;
+ req.postExecute = postExecute;
+ req.scaleProvider = scaleProvider;
+ mCurrentLoadRequest = req;
+
+ // Remove any pending requests
+ mLoaderHandler.removeMessages(MSG_LOAD_IMAGE);
+ Message.obtain(mLoaderHandler, MSG_LOAD_IMAGE, req).sendToTarget();
+
+ // We don't want to show the spinner every time we load an image, because that would be
+ // annoying; instead, only start showing the spinner if loading the image has taken
+ // longer than 1 sec (ie 1000 ms)
+ mProgressView.postDelayed(new Runnable() {
+ public void run() {
+ if (mCurrentLoadRequest == req) {
+ mProgressView.setVisibility(View.VISIBLE);
+ }
+ }
+ }, 1000);
+ }
+
+
+ protected void setWallpaper(Uri uri, boolean isLockScreenPicker,
+ final boolean finishActivityWhenDone) {
+ int rotation = BitmapUtils.getRotationFromExif(getContext(), uri);
+ BitmapCropTask cropTask = new BitmapCropTask(
+ getContext(), uri, null, rotation, 0, 0, true, isLockScreenPicker, false, null);
+ final Point bounds = cropTask.getImageBounds();
+ Runnable onEndCrop = new Runnable() {
+ public void run() {
+ updateWallpaperDimensions(bounds.x, bounds.y);
+ if (finishActivityWhenDone) {
+ setResult(Activity.RESULT_OK);
+ finish();
+ }
+ }
+ };
+ cropTask.setOnEndRunnable(onEndCrop);
+ cropTask.setNoCrop(true);
+ cropTask.execute();
+ }
+
+ protected void cropImageAndSetWallpaper(Resources res, int resId, boolean isLockScreenPicker,
+ final boolean finishActivityWhenDone) {
+ // crop this image and scale it down to the default wallpaper size for
+ // this device
+ int rotation = BitmapUtils.getRotationFromExif(res, resId);
+ Point inSize = mCropView.getSourceDimensions();
+ Point outSize = WallpaperUtils.getDefaultWallpaperSize(getResources(),
+ getWindowManager());
+ RectF crop = Utils.getMaxCropRect(
+ inSize.x, inSize.y, outSize.x, outSize.y, false);
+ Runnable onEndCrop = new Runnable() {
+ public void run() {
+ // Passing 0, 0 will cause launcher to revert to using the
+ // default wallpaper size
+ updateWallpaperDimensions(0, 0);
+ if (finishActivityWhenDone) {
+ setResult(Activity.RESULT_OK);
+ finish();
+ }
+ }
+ };
+ BitmapCropTask cropTask = new BitmapCropTask(getContext(), res, resId,
+ crop, rotation, outSize.x, outSize.y, true, isLockScreenPicker, false, onEndCrop);
+ cropTask.execute();
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ protected void cropImageAndSetWallpaper(Uri uri,
+ BitmapCropTask.OnBitmapCroppedHandler onBitmapCroppedHandler,
+ boolean isLockScreenPicker, final boolean finishActivityWhenDone) {
+ boolean centerCrop = getResources().getBoolean(R.bool.center_crop);
+ // Get the crop
+ boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
+
+ Display d = getWindowManager().getDefaultDisplay();
+
+ Point displaySize = new Point();
+ d.getSize(displaySize);
+ boolean isPortrait = displaySize.x < displaySize.y;
+
+ Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(getResources(),
+ getWindowManager());
+ // Get the crop
+ RectF cropRect = mCropView.getCrop();
+
+ Point inSize = mCropView.getSourceDimensions();
+
+ int cropRotation = mCropView.getImageRotation();
+ float cropScale = mCropView.getWidth() / (float) cropRect.width();
+
+
+ Matrix rotateMatrix = new Matrix();
+ rotateMatrix.setRotate(cropRotation);
+ float[] rotatedInSize = new float[] { inSize.x, inSize.y };
+ rotateMatrix.mapPoints(rotatedInSize);
+ rotatedInSize[0] = Math.abs(rotatedInSize[0]);
+ rotatedInSize[1] = Math.abs(rotatedInSize[1]);
+
+
+ // due to rounding errors in the cropview renderer the edges can be slightly offset
+ // therefore we ensure that the boundaries are sanely defined
+ cropRect.left = Math.max(0, cropRect.left);
+ cropRect.right = Math.min(rotatedInSize[0], cropRect.right);
+ cropRect.top = Math.max(0, cropRect.top);
+ cropRect.bottom = Math.min(rotatedInSize[1], cropRect.bottom);
+
+ // ADJUST CROP WIDTH
+ // Extend the crop all the way to the right, for parallax
+ // (or all the way to the left, in RTL)
+ float extraSpace;
+ if (centerCrop) {
+ extraSpace = 2f * Math.min(rotatedInSize[0] - cropRect.right, cropRect.left);
+ } else {
+ extraSpace = ltr ? rotatedInSize[0] - cropRect.right : cropRect.left;
+ }
+ // Cap the amount of extra width
+ float maxExtraSpace = defaultWallpaperSize.x / cropScale - cropRect.width();
+ extraSpace = Math.min(extraSpace, maxExtraSpace);
+
+ if (centerCrop) {
+ cropRect.left -= extraSpace / 2f;
+ cropRect.right += extraSpace / 2f;
+ } else {
+ if (ltr) {
+ cropRect.right += extraSpace;
+ } else {
+ cropRect.left -= extraSpace;
+ }
+ }
+
+ // ADJUST CROP HEIGHT
+ if (isPortrait) {
+ cropRect.bottom = cropRect.top + defaultWallpaperSize.y / cropScale;
+ } else { // LANDSCAPE
+ float extraPortraitHeight =
+ defaultWallpaperSize.y / cropScale - cropRect.height();
+ float expandHeight =
+ Math.min(Math.min(rotatedInSize[1] - cropRect.bottom, cropRect.top),
+ extraPortraitHeight / 2);
+ cropRect.top -= expandHeight;
+ cropRect.bottom += expandHeight;
+ }
+ final int outWidth = (int) Math.round(cropRect.width() * cropScale);
+ final int outHeight = (int) Math.round(cropRect.height() * cropScale);
+
+ Runnable onEndCrop = new Runnable() {
+ public void run() {
+ updateWallpaperDimensions(outWidth, outHeight);
+ if (finishActivityWhenDone) {
+ setResult(Activity.RESULT_OK);
+ finish();
+ }
+ }
+ };
+ BitmapCropTask cropTask = new BitmapCropTask(getContext(), uri, cropRect, cropRotation,
+ outWidth, outHeight, true, isLockScreenPicker, false, onEndCrop);
+ if (onBitmapCroppedHandler != null) {
+ cropTask.setOnBitmapCropped(onBitmapCroppedHandler);
+ }
+ cropTask.execute();
+ }
+
+ protected void cropImageAndSetWallpaper(String path, String packageName,
+ boolean isLockScreenPicker, final boolean finishActivityWhenDone) {
+ // crop this image and scale it down to the default wallpaper size for
+ // this device
+ Point inSize = mCropView.getSourceDimensions();
+ Point outSize = WallpaperUtils.getDefaultWallpaperSize(getResources(),
+ getWindowManager());
+ RectF cropRect = Utils.getMaxCropRect(
+ inSize.x, inSize.y, outSize.x, outSize.y, false);
+
+ final Resources res;
+ try {
+ res = getPackageManager().getResourcesForApplication(packageName);
+ if (res == null) {
+ return;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ return;
+ }
+
+ Runnable onEndCrop = new Runnable() {
+ public void run() {
+ if (finishActivityWhenDone) {
+ setResult(Activity.RESULT_OK);
+ finish();
+ }
+ }
+ };
+ BitmapCropTask cropTask = new BitmapCropTask(getContext(), res, path, cropRect,
+ 0, outSize.x, outSize.y, true, isLockScreenPicker, false, onEndCrop);
+ if (cropTask != null) {
+ cropTask.execute();
+ }
+ }
+
+ protected void updateWallpaperDimensions(int width, int height) {
+ String spKey = PickerFiles.WALLPAPER_CROP_PREFERENCES_KEY;
+ SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS);
+ SharedPreferences.Editor editor = sp.edit();
+ if (width != 0 && height != 0) {
+ editor.putInt(WALLPAPER_WIDTH_KEY, width);
+ editor.putInt(WALLPAPER_HEIGHT_KEY, height);
+ } else {
+ editor.remove(WALLPAPER_WIDTH_KEY);
+ editor.remove(WALLPAPER_HEIGHT_KEY);
+ }
+ editor.commit();
+ WallpaperUtils.suggestWallpaperDimension(getResources(),
+ sp, getWindowManager(), WallpaperManager.getInstance(getContext()), true);
+ }
+
+ private boolean hasStoragePermissions() {
+ return checkCallingOrSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) ==
+ PackageManager.PERMISSION_GRANTED;
+ }
+
+ private void requestStoragePermissions() {
+ requestPermissions(new String[] {android.Manifest.permission.READ_EXTERNAL_STORAGE},
+ REQUEST_CODE_STORAGE_PERMISSION_CHECK);
+ }
+
+ static class LoadRequest {
+ BitmapSource src;
+ boolean touchEnabled;
+ boolean moveToLeft;
+ Runnable postExecute;
+ CropViewScaleProvider scaleProvider;
+
+ TileSource result;
+ }
+
+ interface CropViewScaleProvider {
+ float getScale(TileSource src);
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/WallpaperPickerActivity.java b/src/org/cyanogenmod/wallpaperpicker/WallpaperPickerActivity.java
new file mode 100644
index 0000000..a5c1d03
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/WallpaperPickerActivity.java
@@ -0,0 +1,1410 @@
+/*
+ * 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 org.cyanogenmod.wallpaperpicker;
+
+import android.animation.LayoutTransition;
+import android.annotation.TargetApi;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.WallpaperManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.Manifest;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.util.Pair;
+import android.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
+import android.view.ViewPropertyAnimator;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.WindowManager;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.HorizontalScrollView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.Toast;
+
+import com.android.gallery3d.common.BitmapCropTask;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
+import com.android.photos.BitmapRegionTileSource;
+import com.android.photos.BitmapRegionTileSource.BitmapSource;
+import com.android.photos.views.TiledImageRenderer.TileSource;
+
+import cyanogenmod.providers.ThemesContract;
+
+import org.cyanogenmod.wallpaperpicker.util.Thunk;
+import org.cyanogenmod.wallpaperpicker.util.WallpaperUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.cyanogenmod.wallpaperpicker.util.Constants.THEME_LOCKSCREEN_PATH;
+import static org.cyanogenmod.wallpaperpicker.util.Constants.THEME_WALLPAPER_PATH;
+
+public class WallpaperPickerActivity extends WallpaperCropActivity {
+ static final String TAG = "WallpaperPickerActivity";
+
+ public static final int IMAGE_PICK = 5;
+ public static final int PICK_WALLPAPER_THIRD_PARTY_ACTIVITY = 6;
+ private static final String TEMP_WALLPAPER_TILES = "TEMP_WALLPAPER_TILES";
+ private static final String SELECTED_INDEX = "SELECTED_INDEX";
+ private static final int FLAG_POST_DELAY_MILLIS = 200;
+ private static final String ACTION_SET_KEYGUARD_WALLPAPER =
+ "android.intent.action.SET_KEYGUARD_WALLPAPER";
+
+ @Thunk
+ View mSelectedTile;
+ @Thunk boolean mIgnoreNextTap;
+ @Thunk OnClickListener mThumbnailOnClickListener;
+
+ @Thunk LinearLayout mWallpapersView;
+ @Thunk HorizontalScrollView mWallpaperScrollContainer;
+ @Thunk View mWallpaperStrip;
+
+ @Thunk ActionMode.Callback mActionModeCallback;
+ @Thunk ActionMode mActionMode;
+
+ @Thunk View.OnLongClickListener mLongClickListener;
+
+ ArrayList<Uri> mTempWallpaperTiles = new ArrayList<Uri>();
+ private SavedWallpaperImages mSavedImages;
+ @Thunk int mSelectedIndex = -1;
+
+ private boolean mIsLockScreenPicker;
+
+ public static abstract class WallpaperTileInfo {
+ protected View mView;
+ protected boolean mIsLockScreenWallpaper;
+ public Drawable mThumb;
+
+ public void setView(View v) {
+ mView = v;
+ }
+ public void onClick(WallpaperPickerActivity a) {}
+ public void onSave(WallpaperPickerActivity a) {}
+ public void onDelete(WallpaperPickerActivity a) {}
+ public boolean isSelectable() { return false; }
+ public boolean isNamelessWallpaper() { return false; }
+ public void onIndexUpdated(CharSequence label) {
+ if (isNamelessWallpaper()) {
+ mView.setContentDescription(label);
+ }
+ }
+ }
+
+ public static class PickImageInfo extends WallpaperTileInfo {
+ @Override
+ public void onClick(WallpaperPickerActivity a) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("image/*");
+ a.startActivityForResultSafely(intent, IMAGE_PICK);
+ }
+ }
+
+ public static class UriWallpaperInfo extends WallpaperTileInfo {
+ private Uri mUri;
+ public UriWallpaperInfo(Uri uri, boolean isLockScreenWallpaper) {
+ mUri = uri;
+ mIsLockScreenWallpaper = isLockScreenWallpaper;
+ }
+ @Override
+ public void onClick(final WallpaperPickerActivity a) {
+ a.setWallpaperButtonEnabled(false);
+ final BitmapRegionTileSource.UriBitmapSource bitmapSource =
+ new BitmapRegionTileSource.UriBitmapSource(a.getContext(), mUri);
+ a.setCropViewTileSource(bitmapSource, true, false, null, new Runnable() {
+
+ @Override
+ public void run() {
+ if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) {
+ a.selectTile(mView);
+ a.setWallpaperButtonEnabled(true);
+ } else {
+ ViewGroup parent = (ViewGroup) mView.getParent();
+ if (parent != null) {
+ parent.removeView(mView);
+ Toast.makeText(a.getContext(), R.string.image_load_fail,
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ });
+ }
+ @Override
+ public void onSave(final WallpaperPickerActivity a) {
+ boolean finishActivityWhenDone = true;
+ BitmapCropTask.OnBitmapCroppedHandler h = new BitmapCropTask.OnBitmapCroppedHandler() {
+ public void onBitmapCropped(byte[] imageBytes) {
+ Point thumbSize = getDefaultThumbnailSize(a.getResources());
+ // rotation is set to 0 since imageBytes has already been correctly rotated
+ Bitmap thumb = createThumbnail(
+ thumbSize, null, null, imageBytes, null, 0, 0, true);
+ a.getSavedImages().writeImage(thumb, imageBytes);
+ }
+ };
+ a.cropImageAndSetWallpaper(mUri, h, mIsLockScreenWallpaper, finishActivityWhenDone);
+ }
+ @Override
+ public boolean isSelectable() {
+ return true;
+ }
+ @Override
+ public boolean isNamelessWallpaper() {
+ return true;
+ }
+ }
+
+ public static class FileWallpaperInfo extends WallpaperTileInfo {
+ private File mFile;
+
+ public FileWallpaperInfo(File target, Drawable thumb, boolean isLockScreenWallpaper) {
+ mFile = target;
+ mThumb = thumb;
+ mIsLockScreenWallpaper = isLockScreenWallpaper;
+ }
+ @Override
+ public void onClick(final WallpaperPickerActivity a) {
+ a.setWallpaperButtonEnabled(false);
+ final BitmapRegionTileSource.UriBitmapSource bitmapSource =
+ new BitmapRegionTileSource.UriBitmapSource(a.getContext(), Uri.fromFile(mFile));
+ a.setCropViewTileSource(bitmapSource, false, true, null, new Runnable() {
+
+ @Override
+ public void run() {
+ if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) {
+ a.setWallpaperButtonEnabled(true);
+ }
+ }
+ });
+ }
+ @Override
+ public void onSave(WallpaperPickerActivity a) {
+ a.setWallpaper(Uri.fromFile(mFile), mIsLockScreenWallpaper, true);
+ }
+ @Override
+ public boolean isSelectable() {
+ return true;
+ }
+ @Override
+ public boolean isNamelessWallpaper() {
+ return true;
+ }
+ }
+
+ public static class ResourceWallpaperInfo extends WallpaperTileInfo {
+ private Resources mResources;
+ private int mResId;
+
+ public ResourceWallpaperInfo(Resources res, int resId, Drawable thumb,
+ boolean isLockScreenWallpaper) {
+ mResources = res;
+ mResId = resId;
+ mThumb = thumb;
+ mIsLockScreenWallpaper = isLockScreenWallpaper;
+ }
+ @Override
+ public void onClick(final WallpaperPickerActivity a) {
+ a.setWallpaperButtonEnabled(false);
+ final BitmapRegionTileSource.ResourceBitmapSource bitmapSource =
+ new BitmapRegionTileSource.ResourceBitmapSource(mResources, mResId);
+ a.setCropViewTileSource(bitmapSource, false, false, new CropViewScaleProvider() {
+
+ @Override
+ public float getScale(TileSource src) {
+ Point wallpaperSize = WallpaperUtils.getDefaultWallpaperSize(
+ a.getResources(), a.getWindowManager());
+ RectF crop = Utils.getMaxCropRect(
+ src.getImageWidth(), src.getImageHeight(),
+ wallpaperSize.x, wallpaperSize.y, false);
+ return wallpaperSize.x / crop.width();
+ }
+ }, new Runnable() {
+
+ @Override
+ public void run() {
+ if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) {
+ a.setWallpaperButtonEnabled(true);
+ }
+ }
+ });
+ }
+ @Override
+ public void onSave(WallpaperPickerActivity a) {
+ boolean finishActivityWhenDone = true;
+ a.cropImageAndSetWallpaper(mResources, mResId, mIsLockScreenWallpaper,
+ finishActivityWhenDone);
+ }
+ @Override
+ public boolean isSelectable() {
+ return true;
+ }
+ @Override
+ public boolean isNamelessWallpaper() {
+ return true;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ public static class DefaultWallpaperInfo extends WallpaperTileInfo {
+ public DefaultWallpaperInfo(Drawable thumb) {
+ mThumb = thumb;
+ mIsLockScreenWallpaper = false;
+ }
+ @Override
+ public void onClick(WallpaperPickerActivity a) {
+ CropView c = a.getCropView();
+ Drawable defaultWallpaper = WallpaperManager.getInstance(a.getContext())
+ .getBuiltInDrawable(c.getWidth(), c.getHeight(), false, 0.5f, 0.5f);
+ if (defaultWallpaper == null) {
+ Log.w(TAG, "Null default wallpaper encountered.");
+ c.setTileSource(null, null);
+ return;
+ }
+
+ LoadRequest req = new LoadRequest();
+ req.moveToLeft = false;
+ req.touchEnabled = false;
+ req.scaleProvider = new CropViewScaleProvider() {
+
+ @Override
+ public float getScale(TileSource src) {
+ return 1f;
+ }
+ };
+ req.result = new DrawableTileSource(a.getContext(),
+ defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE);
+ a.onLoadRequestComplete(req, true);
+ }
+ @Override
+ public void onSave(WallpaperPickerActivity a) {
+ try {
+ WallpaperManager.getInstance(a.getContext()).clear();
+ a.setResult(Activity.RESULT_OK);
+ } catch (IOException e) {
+ Log.w("Setting wallpaper to default threw exception", e);
+ }
+ a.finish();
+ }
+ @Override
+ public boolean isSelectable() {
+ return true;
+ }
+ @Override
+ public boolean isNamelessWallpaper() {
+ return false;
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ public static class NoWallpaperInfo extends WallpaperTileInfo {
+ public NoWallpaperInfo(Drawable thumb, boolean isLockScreenWallpaper) {
+ mThumb = thumb;
+ mIsLockScreenWallpaper = isLockScreenWallpaper;
+ }
+ @Override
+ public void onClick(WallpaperPickerActivity a) {
+ LoadRequest req = new LoadRequest();
+ req.moveToLeft = false;
+ req.touchEnabled = false;
+ req.scaleProvider = new CropViewScaleProvider() {
+
+ @Override
+ public float getScale(TileSource src) {
+ return 1f;
+ }
+ };
+ req.result = new DrawableTileSource(a.getContext(),
+ mThumb, DrawableTileSource.MAX_PREVIEW_SIZE);
+ a.onLoadRequestComplete(req, true);
+ }
+ @Override
+ public void onSave(WallpaperPickerActivity a) {
+ try {
+ if (!mIsLockScreenWallpaper) {
+ BitmapDrawable bd = (BitmapDrawable) mThumb;
+ WallpaperManager.getInstance(a.getContext()).setBitmap(bd.getBitmap());
+ } else {
+ WallpaperManager.getInstance(a.getContext()).clearKeyguardWallpaper();
+ }
+ a.setResult(Activity.RESULT_OK);
+ } catch (IOException e) {
+ Log.w("Setting wallpaper to default threw exception", e);
+ }
+ a.finish();
+ }
+ @Override
+ public boolean isSelectable() {
+ return true;
+ }
+ @Override
+ public boolean isNamelessWallpaper() {
+ return true;
+ }
+ }
+
+ /**
+ * For themes which have regular wallpapers
+ */
+ @TargetApi(Build.VERSION_CODES.M)
+ public static class ThemeWallpaperInfo extends WallpaperTileInfo {
+ String mPackageName;
+ Drawable mThumb;
+ Context mContext;
+
+ public ThemeWallpaperInfo(Context context, String packageName, Drawable thumb,
+ boolean isLockScreenWallpaper) {
+ this.mContext = context;
+ this.mPackageName = packageName;
+ this.mThumb = thumb;
+ mIsLockScreenWallpaper = isLockScreenWallpaper;
+ }
+
+ @Override
+ public void onClick(final WallpaperPickerActivity a) {
+ try {
+ final BitmapRegionTileSource.ThemeBitmapSource source;
+ Resources res = a.getPackageManager().getResourcesForApplication(mPackageName);
+ if (res == null) {
+ return;
+ }
+
+ int rotation = 0;
+ source = new BitmapRegionTileSource.ThemeBitmapSource(res.getAssets(),
+ mIsLockScreenWallpaper ? THEME_LOCKSCREEN_PATH : THEME_WALLPAPER_PATH);
+ a.setCropViewTileSource(source, false, false, new CropViewScaleProvider() {
+
+ @Override
+ public float getScale(TileSource src) {
+ Point wallpaperSize = WallpaperUtils.getDefaultWallpaperSize(
+ a.getResources(), a.getWindowManager());
+ RectF crop = Utils.getMaxCropRect(
+ src.getImageWidth(), src.getImageHeight(),
+ wallpaperSize.x, wallpaperSize.y, false);
+ return wallpaperSize.x / crop.width();
+ }
+ }, new Runnable() {
+
+ @Override
+ public void run() {
+ if (source.getLoadingState() == BitmapSource.State.LOADED) {
+ a.setWallpaperButtonEnabled(true);
+ }
+ }
+ });
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+ }
+
+ @Override
+ public void onSave(WallpaperPickerActivity a) {
+ a.cropImageAndSetWallpaper(
+ mIsLockScreenWallpaper ? THEME_LOCKSCREEN_PATH : THEME_WALLPAPER_PATH,
+ mPackageName,
+ mIsLockScreenWallpaper,
+ true);
+ }
+
+ @Override
+ public boolean isNamelessWallpaper() {
+ return true;
+ }
+
+ @Override
+ public boolean isSelectable() {
+ return true;
+ }
+ }
+
+ /**
+ * shows the system wallpaper behind the window and hides the {@link
+ * #mCropView} if visible
+ * @param visible should the system wallpaper be shown
+ */
+ protected void setSystemWallpaperVisiblity(final boolean visible) {
+ // hide our own wallpaper preview if necessary
+ if(!visible) {
+ mCropView.setVisibility(View.VISIBLE);
+ } else {
+ changeWallpaperFlags(visible);
+ }
+ // the change of the flag must be delayed in order to avoid flickering,
+ // a simple post / double post does not suffice here
+ mCropView.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if(!visible) {
+ changeWallpaperFlags(visible);
+ } else {
+ mCropView.setVisibility(View.INVISIBLE);
+ }
+ }
+ }, FLAG_POST_DELAY_MILLIS);
+ }
+
+ @Thunk void changeWallpaperFlags(boolean visible) {
+ int desiredWallpaperFlag = visible ? WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER : 0;
+ int currentWallpaperFlag = getWindow().getAttributes().flags
+ & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+ if (desiredWallpaperFlag != currentWallpaperFlag) {
+ getWindow().setFlags(desiredWallpaperFlag,
+ WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER);
+ }
+ }
+
+ @Override
+ protected void onLoadRequestComplete(LoadRequest req, boolean success) {
+ super.onLoadRequestComplete(req, success);
+ if (success) {
+ setSystemWallpaperVisiblity(false);
+ }
+ }
+
+ // called by onCreate; this is subclassed to overwrite WallpaperCropActivity
+ protected void init() {
+ setContentView(R.layout.wallpaper_picker);
+
+ mIsLockScreenPicker = ACTION_SET_KEYGUARD_WALLPAPER.equals(getIntent().getAction());
+
+ mCropView = (CropView) findViewById(R.id.cropView);
+ mProgressView = findViewById(R.id.loading);
+ mWallpaperScrollContainer = (HorizontalScrollView) findViewById(R.id.wallpaper_scroll_container);
+ mWallpaperStrip = findViewById(R.id.wallpaper_strip);
+ mCropView.setTouchCallback(new CropView.TouchCallback() {
+ ViewPropertyAnimator mAnim;
+ @Override
+ public void onTouchDown() {
+ if (mAnim != null) {
+ mAnim.cancel();
+ }
+ if (mWallpaperStrip.getAlpha() == 1f) {
+ mIgnoreNextTap = true;
+ }
+ mAnim = mWallpaperStrip.animate();
+ mAnim.alpha(0f)
+ .setDuration(150)
+ .withEndAction(new Runnable() {
+ public void run() {
+ mWallpaperStrip.setVisibility(View.INVISIBLE);
+ }
+ });
+ mAnim.setInterpolator(new AccelerateInterpolator(0.75f));
+ mAnim.start();
+ }
+ @Override
+ public void onTouchUp() {
+ mIgnoreNextTap = false;
+ }
+ @Override
+ public void onTap() {
+ boolean ignoreTap = mIgnoreNextTap;
+ mIgnoreNextTap = false;
+ if (!ignoreTap) {
+ if (mAnim != null) {
+ mAnim.cancel();
+ }
+ mWallpaperStrip.setVisibility(View.VISIBLE);
+ mAnim = mWallpaperStrip.animate();
+ mAnim.alpha(1f)
+ .setDuration(150)
+ .setInterpolator(new DecelerateInterpolator(0.75f));
+ mAnim.start();
+ }
+ }
+ });
+
+ if (mIsLockScreenPicker) {
+ final Bitmap keyguardBmp = WallpaperManager.getInstance(this).getKeyguardBitmap();
+ setCropViewTileSource(new BitmapRegionTileSource.DumbBitmapSource(keyguardBmp), false,
+ true, null, null);
+ } else {
+ mCropView.setVisibility(View.INVISIBLE);
+ }
+
+ mThumbnailOnClickListener = new OnClickListener() {
+ public void onClick(View v) {
+ if (mActionMode != null) {
+ // When CAB is up, clicking toggles the item instead
+ if (v.isLongClickable()) {
+ mLongClickListener.onLongClick(v);
+ }
+ return;
+ }
+ setWallpaperButtonEnabled(true);
+ WallpaperTileInfo info = (WallpaperTileInfo) v.getTag();
+ if (info.isSelectable() && v.getVisibility() == View.VISIBLE) {
+ selectTile(v);
+ }
+ info.onClick(WallpaperPickerActivity.this);
+ }
+ };
+ mLongClickListener = new View.OnLongClickListener() {
+ // Called when the user long-clicks on someView
+ public boolean onLongClick(View view) {
+ CheckableFrameLayout c = (CheckableFrameLayout) view;
+ c.toggle();
+
+ if (mActionMode != null) {
+ mActionMode.invalidate();
+ } else {
+ // Start the CAB using the ActionMode.Callback defined below
+ mActionMode = startActionMode(mActionModeCallback);
+ int childCount = mWallpapersView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ mWallpapersView.getChildAt(i).setSelected(false);
+ }
+ }
+ return true;
+ }
+ };
+
+ mWallpapersView = (LinearLayout) findViewById(R.id.wallpaper_list);
+
+ // Populate the built-in wallpapers
+ ArrayList<WallpaperTileInfo> wallpapers = findBundledWallpapers();
+ mWallpapersView = (LinearLayout) findViewById(R.id.wallpaper_list);
+ SimpleWallpapersAdapter ia = new SimpleWallpapersAdapter(getContext(), wallpapers);
+ populateWallpapersFromAdapter(mWallpapersView, ia, false);
+
+ // Populate the saved wallpapers
+ mSavedImages = new SavedWallpaperImages(getContext(), mIsLockScreenPicker);
+ mSavedImages.loadThumbnailsAndImageIdList();
+ populateWallpapersFromAdapter(mWallpapersView, mSavedImages, false);
+
+ // theme wallpapers
+ new AsyncTask<Void, Void, ArrayList<ThemeWallpaperInfo>>() {
+ @Override
+ protected ArrayList<ThemeWallpaperInfo> doInBackground(Void... params) {
+ return findThemeWallpapers();
+ }
+
+ @Override
+ protected void onPostExecute(ArrayList<ThemeWallpaperInfo> themeWallpaperInfos) {
+ LinearLayout themeList = (LinearLayout) findViewById(R.id.theme_wallpaper_list);
+ ThemeWallpapersAdapter ta = new ThemeWallpapersAdapter(
+ WallpaperPickerActivity.this, themeWallpaperInfos);
+ populateWallpapersFromAdapter(themeList, ta, false);
+ }
+ }.execute((Void) null);
+
+ // Add a tile for the no wallpaper option
+ LinearLayout themeList = (LinearLayout) findViewById(R.id.theme_wallpaper_list);
+ FrameLayout noWallpaperTile = (FrameLayout) getLayoutInflater().
+ inflate(R.layout.wallpaper_picker_no_wallpaper_item, themeList, false);
+ themeList.addView(noWallpaperTile);
+ NoWallpaperInfo noWallpaperInfo = new NoWallpaperInfo(getDrawable(R.drawable.black),
+ mIsLockScreenPicker);
+ noWallpaperTile.setTag(noWallpaperInfo);
+ noWallpaperInfo.setView(noWallpaperTile);
+ noWallpaperTile.setOnClickListener(mThumbnailOnClickListener);
+
+ if (!mIsLockScreenPicker) {
+ // Populate the live wallpapers
+ final LinearLayout liveWallpapersView =
+ (LinearLayout) findViewById(R.id.live_wallpaper_list);
+ final LiveWallpaperListAdapter a = new LiveWallpaperListAdapter(getContext());
+ a.registerDataSetObserver(new DataSetObserver() {
+ public void onChanged() {
+ liveWallpapersView.removeAllViews();
+ populateWallpapersFromAdapter(liveWallpapersView, a, false);
+ initializeScrollForRtl();
+ updateTileIndices();
+ }
+ });
+
+ // Populate the third-party wallpaper pickers
+ final LinearLayout thirdPartyWallpapersView =
+ (LinearLayout) findViewById(R.id.third_party_wallpaper_list);
+ final ThirdPartyWallpaperPickerListAdapter ta =
+ new ThirdPartyWallpaperPickerListAdapter(getContext());
+ populateWallpapersFromAdapter(thirdPartyWallpapersView, ta, false);
+ }
+
+ // Add a tile for the Gallery
+ LinearLayout masterWallpaperList = (LinearLayout) findViewById(R.id.master_wallpaper_list);
+ FrameLayout pickImageTile = (FrameLayout) getLayoutInflater().
+ inflate(R.layout.wallpaper_picker_image_picker_item, masterWallpaperList, false);
+ masterWallpaperList.addView(pickImageTile, 0);
+
+ // Make its background the last photo taken on external storage
+ Bitmap lastPhoto = getThumbnailOfLastPhoto();
+ if (lastPhoto != null) {
+ ImageView galleryThumbnailBg =
+ (ImageView) pickImageTile.findViewById(R.id.wallpaper_image);
+ galleryThumbnailBg.setImageBitmap(lastPhoto);
+ int colorOverlay = getResources().getColor(R.color.wallpaper_picker_translucent_gray);
+ galleryThumbnailBg.setColorFilter(colorOverlay, PorterDuff.Mode.SRC_ATOP);
+ }
+
+ PickImageInfo pickImageInfo = new PickImageInfo();
+ pickImageTile.setTag(pickImageInfo);
+ pickImageInfo.setView(pickImageTile);
+ pickImageTile.setOnClickListener(mThumbnailOnClickListener);
+
+ // Select the first item; wait for a layout pass so that we initialize the dimensions of
+ // cropView or the defaultWallpaperView first
+ mCropView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ if ((right - left) > 0 && (bottom - top) > 0) {
+ if (mSelectedIndex >= 0 && mSelectedIndex < mWallpapersView.getChildCount()) {
+ mThumbnailOnClickListener.onClick(
+ mWallpapersView.getChildAt(mSelectedIndex));
+ setSystemWallpaperVisiblity(false);
+ }
+ v.removeOnLayoutChangeListener(this);
+ }
+ }
+ });
+
+ updateTileIndices();
+
+ // Update the scroll for RTL
+ initializeScrollForRtl();
+
+ // Create smooth layout transitions for when items are deleted
+ final LayoutTransition transitioner = new LayoutTransition();
+ transitioner.setDuration(200);
+ transitioner.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0);
+ transitioner.setAnimator(LayoutTransition.DISAPPEARING, null);
+ mWallpapersView.setLayoutTransition(transitioner);
+
+ // Action bar
+ // Show the custom action bar view
+ final ActionBar actionBar = getActionBar();
+ actionBar.setCustomView(R.layout.actionbar_set_wallpaper);
+ actionBar.getCustomView().setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Ensure that a tile is slelected and loaded.
+ if (mSelectedTile != null && mCropView.getTileSource() != null) {
+ // Prevent user from selecting any new tile.
+ mWallpaperStrip.setVisibility(View.GONE);
+ actionBar.hide();
+
+ WallpaperTileInfo info = (WallpaperTileInfo) mSelectedTile.getTag();
+ info.onSave(WallpaperPickerActivity.this);
+ } else {
+ // no tile was selected, so we just finish the activity and go back
+ setResult(Activity.RESULT_OK);
+ finish();
+ }
+ }
+ });
+ mSetWallpaperButton = findViewById(R.id.set_wallpaper_button);
+
+ // CAB for deleting items
+ mActionModeCallback = new ActionMode.Callback() {
+ // Called when the action mode is created; startActionMode() was called
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ // Inflate a menu resource providing context menu items
+ MenuInflater inflater = mode.getMenuInflater();
+ inflater.inflate(R.menu.cab_delete_wallpapers, menu);
+ return true;
+ }
+
+ private int numCheckedItems() {
+ int childCount = mWallpapersView.getChildCount();
+ int numCheckedItems = 0;
+ for (int i = 0; i < childCount; i++) {
+ CheckableFrameLayout c = (CheckableFrameLayout) mWallpapersView.getChildAt(i);
+ if (c.isChecked()) {
+ numCheckedItems++;
+ }
+ }
+ return numCheckedItems;
+ }
+
+ // Called each time the action mode is shown. Always called after onCreateActionMode,
+ // but may be called multiple times if the mode is invalidated.
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ int numCheckedItems = numCheckedItems();
+ if (numCheckedItems == 0) {
+ mode.finish();
+ return true;
+ } else {
+ mode.setTitle(getResources().getQuantityString(
+ R.plurals.number_of_items_selected, numCheckedItems, numCheckedItems));
+ return true;
+ }
+ }
+
+ // Called when the user selects a contextual menu item
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ int itemId = item.getItemId();
+ if (itemId == R.id.menu_delete) {
+ int childCount = mWallpapersView.getChildCount();
+ ArrayList<View> viewsToRemove = new ArrayList<View>();
+ boolean selectedTileRemoved = false;
+ for (int i = 0; i < childCount; i++) {
+ CheckableFrameLayout c =
+ (CheckableFrameLayout) mWallpapersView.getChildAt(i);
+ if (c.isChecked()) {
+ WallpaperTileInfo info = (WallpaperTileInfo) c.getTag();
+ info.onDelete(WallpaperPickerActivity.this);
+ viewsToRemove.add(c);
+ if (i == mSelectedIndex) {
+ selectedTileRemoved = true;
+ }
+ }
+ }
+ for (View v : viewsToRemove) {
+ mWallpapersView.removeView(v);
+ }
+ if (selectedTileRemoved) {
+ mSelectedIndex = -1;
+ mSelectedTile = null;
+ setSystemWallpaperVisiblity(true);
+ }
+ updateTileIndices();
+ mode.finish(); // Action picked, so close the CAB
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // Called when the user exits the action mode
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ int childCount = mWallpapersView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ CheckableFrameLayout c = (CheckableFrameLayout) mWallpapersView.getChildAt(i);
+ c.setChecked(false);
+ }
+ if (mSelectedTile != null) {
+ mSelectedTile.setSelected(true);
+ }
+ mActionMode = null;
+ }
+ };
+ }
+
+ public void setWallpaperButtonEnabled(boolean enabled) {
+ mSetWallpaperButton.setEnabled(enabled);
+ }
+
+ @Thunk void selectTile(View v) {
+ if (mSelectedTile != null) {
+ mSelectedTile.setSelected(false);
+ mSelectedTile = null;
+ }
+ mSelectedTile = v;
+ v.setSelected(true);
+ mSelectedIndex = mWallpapersView.indexOfChild(v);
+ // TODO: Remove this once the accessibility framework and
+ // services have better support for selection state.
+ v.announceForAccessibility(
+ getContext().getString(R.string.announce_selection, v.getContentDescription()));
+ }
+
+ @Thunk void initializeScrollForRtl() {
+ if (Utilities.isRtl(getResources())) {
+ final ViewTreeObserver observer = mWallpaperScrollContainer.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
+ public void onGlobalLayout() {
+ LinearLayout masterWallpaperList =
+ (LinearLayout) findViewById(R.id.master_wallpaper_list);
+ mWallpaperScrollContainer.scrollTo(masterWallpaperList.getWidth(), 0);
+ mWallpaperScrollContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+ });
+ }
+ }
+
+ protected Bitmap getThumbnailOfLastPhoto() {
+ boolean canReadExternalStorage = getActivity().checkPermission(
+ Manifest.permission.READ_EXTERNAL_STORAGE, Process.myPid(), Process.myUid()) ==
+ PackageManager.PERMISSION_GRANTED;
+
+ if (!canReadExternalStorage) {
+ // MediaStore.Images.Media.EXTERNAL_CONTENT_URI requires
+ // the READ_EXTERNAL_STORAGE permission
+ return null;
+ }
+
+ Cursor cursor = MediaStore.Images.Media.query(getContext().getContentResolver(),
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ new String[] { MediaStore.Images.ImageColumns._ID,
+ MediaStore.Images.ImageColumns.DATE_TAKEN},
+ null, null, MediaStore.Images.ImageColumns.DATE_TAKEN + " DESC LIMIT 1");
+
+ Bitmap thumb = null;
+ if (cursor != null) {
+ if (cursor.moveToNext()) {
+ int id = cursor.getInt(0);
+ thumb = MediaStore.Images.Thumbnails.getThumbnail(getContext().getContentResolver(),
+ id, MediaStore.Images.Thumbnails.MINI_KIND, null);
+ }
+ cursor.close();
+ }
+ return thumb;
+ }
+
+ public void onStop() {
+ super.onStop();
+ mWallpaperStrip = findViewById(R.id.wallpaper_strip);
+ if (mWallpaperStrip != null && mWallpaperStrip.getAlpha() < 1f) {
+ mWallpaperStrip.setAlpha(1f);
+ mWallpaperStrip.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putParcelableArrayList(TEMP_WALLPAPER_TILES, mTempWallpaperTiles);
+ outState.putInt(SELECTED_INDEX, mSelectedIndex);
+ }
+
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ ArrayList<Uri> uris = savedInstanceState.getParcelableArrayList(TEMP_WALLPAPER_TILES);
+ for (Uri uri : uris) {
+ addTemporaryWallpaperTile(uri, true);
+ }
+ mSelectedIndex = savedInstanceState.getInt(SELECTED_INDEX, -1);
+ }
+
+ @Thunk void populateWallpapersFromAdapter(ViewGroup parent, BaseAdapter adapter,
+ boolean addLongPressHandler) {
+ for (int i = 0; i < adapter.getCount(); i++) {
+ FrameLayout thumbnail = (FrameLayout) adapter.getView(i, null, parent);
+ parent.addView(thumbnail, i);
+ WallpaperTileInfo info = (WallpaperTileInfo) adapter.getItem(i);
+ thumbnail.setTag(info);
+ info.setView(thumbnail);
+ if (addLongPressHandler) {
+ addLongPressHandler(thumbnail);
+ }
+ thumbnail.setOnClickListener(mThumbnailOnClickListener);
+ }
+ }
+
+ @Thunk void updateTileIndices() {
+ LinearLayout masterWallpaperList = (LinearLayout) findViewById(R.id.master_wallpaper_list);
+ final int childCount = masterWallpaperList.getChildCount();
+ final Resources res = getResources();
+
+ // Do two passes; the first pass gets the total number of tiles
+ int numTiles = 0;
+ for (int passNum = 0; passNum < 2; passNum++) {
+ int tileIndex = 0;
+ for (int i = 0; i < childCount; i++) {
+ View child = masterWallpaperList.getChildAt(i);
+ LinearLayout subList;
+
+ int subListStart;
+ int subListEnd;
+ if (child.getTag() instanceof WallpaperTileInfo) {
+ subList = masterWallpaperList;
+ subListStart = i;
+ subListEnd = i + 1;
+ } else { // if (child instanceof LinearLayout) {
+ subList = (LinearLayout) child;
+ subListStart = 0;
+ subListEnd = subList.getChildCount();
+ }
+
+ for (int j = subListStart; j < subListEnd; j++) {
+ WallpaperTileInfo info = (WallpaperTileInfo) subList.getChildAt(j).getTag();
+ if (info.isNamelessWallpaper()) {
+ if (passNum == 0) {
+ numTiles++;
+ } else {
+ CharSequence label = res.getString(
+ R.string.wallpaper_accessibility_name, ++tileIndex, numTiles);
+ info.onIndexUpdated(label);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Thunk static Point getDefaultThumbnailSize(Resources res) {
+ return new Point(res.getDimensionPixelSize(R.dimen.wallpaperThumbnailWidth),
+ res.getDimensionPixelSize(R.dimen.wallpaperThumbnailHeight));
+
+ }
+
+ @Thunk static Bitmap createThumbnail(Point size, Context context, Uri uri, byte[] imageBytes,
+ Resources res, int resId, int rotation, boolean leftAligned) {
+ int width = size.x;
+ int height = size.y;
+
+ BitmapCropTask cropTask;
+ if (uri != null) {
+ cropTask = new BitmapCropTask(
+ context, uri, null, rotation, width, height, false, false, true, null);
+ } else if (imageBytes != null) {
+ cropTask = new BitmapCropTask(
+ imageBytes, null, rotation, width, height, false, false, true, null);
+ } else {
+ cropTask = new BitmapCropTask(
+ context, res, resId, null, rotation, width, height, false, false, true, null);
+ }
+ Point bounds = cropTask.getImageBounds();
+ if (bounds == null || bounds.x == 0 || bounds.y == 0) {
+ return null;
+ }
+
+ Matrix rotateMatrix = new Matrix();
+ rotateMatrix.setRotate(rotation);
+ float[] rotatedBounds = new float[] { bounds.x, bounds.y };
+ rotateMatrix.mapPoints(rotatedBounds);
+ rotatedBounds[0] = Math.abs(rotatedBounds[0]);
+ rotatedBounds[1] = Math.abs(rotatedBounds[1]);
+
+ RectF cropRect = Utils.getMaxCropRect(
+ (int) rotatedBounds[0], (int) rotatedBounds[1], width, height, leftAligned);
+ cropTask.setCropBounds(cropRect);
+
+ if (cropTask.cropBitmap()) {
+ return cropTask.getCroppedBitmap();
+ } else {
+ return null;
+ }
+ }
+
+ private void addTemporaryWallpaperTile(final Uri uri, boolean fromRestore) {
+ mTempWallpaperTiles.add(uri);
+ // Add a tile for the image picked from Gallery
+ final FrameLayout pickedImageThumbnail = (FrameLayout) getLayoutInflater().
+ inflate(R.layout.wallpaper_picker_item, mWallpapersView, false);
+ pickedImageThumbnail.setVisibility(View.GONE);
+ mWallpapersView.addView(pickedImageThumbnail, 0);
+
+ // Load the thumbnail
+ final ImageView image = (ImageView) pickedImageThumbnail.findViewById(R.id.wallpaper_image);
+ final Point defaultSize = getDefaultThumbnailSize(this.getResources());
+ final Context context = getContext();
+ new AsyncTask<Void, Bitmap, Bitmap>() {
+ protected Bitmap doInBackground(Void...args) {
+ try {
+ int rotation = BitmapUtils.getRotationFromExif(context, uri);
+ return createThumbnail(defaultSize, context, uri, null, null, 0, rotation, false);
+ } catch (SecurityException securityException) {
+ if (isActivityDestroyed()) {
+ // Temporarily granted permissions are revoked when the activity
+ // finishes, potentially resulting in a SecurityException here.
+ // Even though {@link #isDestroyed} might also return true in different
+ // situations where the configuration changes, we are fine with
+ // catching these cases here as well.
+ cancel(false);
+ } else {
+ // otherwise it had a different cause and we throw it further
+ throw securityException;
+ }
+ return null;
+ }
+ }
+ protected void onPostExecute(Bitmap thumb) {
+ if (!isCancelled() && thumb != null) {
+ image.setImageBitmap(thumb);
+ Drawable thumbDrawable = image.getDrawable();
+ thumbDrawable.setDither(true);
+ pickedImageThumbnail.setVisibility(View.VISIBLE);
+ } else {
+ Log.e(TAG, "Error loading thumbnail for uri=" + uri);
+ }
+ }
+ }.execute();
+
+ UriWallpaperInfo info = new UriWallpaperInfo(uri, mIsLockScreenPicker);
+ pickedImageThumbnail.setTag(info);
+ info.setView(pickedImageThumbnail);
+ addLongPressHandler(pickedImageThumbnail);
+ updateTileIndices();
+ pickedImageThumbnail.setOnClickListener(mThumbnailOnClickListener);
+ if (!fromRestore) {
+ mThumbnailOnClickListener.onClick(pickedImageThumbnail);
+ }
+ }
+
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == IMAGE_PICK && resultCode == Activity.RESULT_OK) {
+ if (data != null && data.getData() != null) {
+ Uri uri = data.getData();
+ addTemporaryWallpaperTile(uri, false);
+ }
+ } else if (requestCode == PICK_WALLPAPER_THIRD_PARTY_ACTIVITY
+ && resultCode == Activity.RESULT_OK) {
+ // Something was set on the third-party activity.
+ setResult(Activity.RESULT_OK);
+ finish();
+ }
+ }
+
+ private void addLongPressHandler(View v) {
+ v.setOnLongClickListener(mLongClickListener);
+
+ // Enable stylus button to also trigger long click.
+ final StylusEventHelper stylusEventHelper = new StylusEventHelper(v);
+ v.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ return stylusEventHelper.checkAndPerformStylusEvent(event);
+ }
+ });
+ }
+
+ private ArrayList<WallpaperTileInfo> findBundledWallpapers() {
+ final PackageManager pm = getContext().getPackageManager();
+ final ArrayList<WallpaperTileInfo> bundled = new ArrayList<WallpaperTileInfo>(24);
+
+ List<Partner> partners = Partner.getAllPartners(pm);
+ boolean hideDefault = false;
+ if (partners != null) {
+ for (Partner partner : partners) {
+ final Resources partnerRes = partner.getResources();
+ final int resId = partnerRes.getIdentifier(Partner.RES_WALLPAPERS, "array",
+ partner.getPackageName());
+ if (resId != 0) {
+ addWallpapers(bundled, partnerRes, partner.getPackageName(), resId);
+ }
+
+ // Add system wallpapers
+ File systemDir = partner.getWallpaperDirectory();
+ if (systemDir != null && systemDir.isDirectory()) {
+ for (File file : systemDir.listFiles()) {
+ if (!file.isFile()) {
+ continue;
+ }
+ String name = file.getName();
+ int dotPos = name.lastIndexOf('.');
+ String extension = "";
+ if (dotPos >= -1) {
+ extension = name.substring(dotPos);
+ name = name.substring(0, dotPos);
+ }
+
+ if (name.endsWith("_small")) {
+ // it is a thumbnail
+ continue;
+ }
+
+ File thumbnail = new File(systemDir, name + "_small" + extension);
+ Bitmap thumb = BitmapFactory.decodeFile(thumbnail.getAbsolutePath());
+ if (thumb != null) {
+ bundled.add(new FileWallpaperInfo(file, new BitmapDrawable(thumb),
+ mIsLockScreenPicker));
+ }
+ }
+ }
+ if (partner.hideDefaultWallpaper()) {
+ hideDefault = true;
+ }
+ }
+ }
+
+ Pair<ApplicationInfo, Integer> r = getWallpaperArrayResourceId();
+ if (r != null) {
+ try {
+ Resources wallpaperRes = getContext().getPackageManager()
+ .getResourcesForApplication(r.first);
+ addWallpapers(bundled, wallpaperRes, r.first.packageName, r.second);
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+ }
+
+ if (!hideDefault && !mIsLockScreenPicker) {
+ // Add an entry for the default wallpaper (stored in system resources)
+ WallpaperTileInfo defaultWallpaperInfo = Utilities.ATLEAST_KITKAT
+ ? getDefaultWallpaper() : getPreKKDefaultWallpaperInfo();
+ if (defaultWallpaperInfo != null) {
+ bundled.add(0, defaultWallpaperInfo);
+ }
+ }
+
+ return bundled;
+ }
+
+ private ArrayList<ThemeWallpaperInfo> findThemeWallpapers() {
+ ArrayList<ThemeWallpaperInfo> themeWallpapers =
+ new ArrayList<ThemeWallpaperInfo>();
+ ContentResolver cr = getContentResolver();
+ String[] projection = {ThemesContract.ThemesColumns.PKG_NAME};
+ String selection = (mIsLockScreenPicker
+ ? ThemesContract.ThemesColumns.MODIFIES_LOCKSCREEN
+ : ThemesContract.ThemesColumns.MODIFIES_LAUNCHER) + "=?";
+ String[] selectionArgs = {"1"};
+ String sortOrder = null;
+ Cursor c = cr.query(ThemesContract.ThemesColumns.CONTENT_URI, projection, selection,
+ selectionArgs, sortOrder);
+ if (c != null) {
+ Bitmap bmp;
+ while (c.moveToNext()) {
+ String pkgName = c.getString(
+ c.getColumnIndexOrThrow(ThemesContract.ThemesColumns.PKG_NAME));
+ bmp = Utilities.getThemeWallpaper(this,
+ mIsLockScreenPicker ? THEME_LOCKSCREEN_PATH: THEME_WALLPAPER_PATH,
+ pkgName, true /* thumb*/);
+ if (bmp != null) {
+ themeWallpapers.add(
+ new ThemeWallpaperInfo(this, pkgName,
+ new BitmapDrawable(getResources(), bmp), mIsLockScreenPicker));
+
+ Log.d("", String.format("Loaded bitmap of size %dx%d for %s",
+ bmp.getWidth(), bmp.getHeight(), pkgName));
+ }
+ }
+ c.close();
+ }
+ return themeWallpapers;
+ }
+
+ private boolean writeImageToFileAsJpeg(File f, Bitmap b) {
+ try {
+ f.createNewFile();
+ FileOutputStream thumbFileStream =
+ getContext().openFileOutput(f.getName(), Context.MODE_PRIVATE);
+ b.compress(Bitmap.CompressFormat.JPEG, 95, thumbFileStream);
+ thumbFileStream.close();
+ return true;
+ } catch (IOException e) {
+ Log.e(TAG, "Error while writing bitmap to file " + e);
+ f.delete();
+ }
+ return false;
+ }
+
+ private File getDefaultThumbFile() {
+ return new File(getContext().getFilesDir(), Build.VERSION.SDK_INT
+ + "_" + PickerFiles.DEFAULT_WALLPAPER_THUMBNAIL);
+ }
+
+ private boolean saveDefaultWallpaperThumb(Bitmap b) {
+ // Delete old thumbnails.
+ new File(getContext().getFilesDir(), PickerFiles.DEFAULT_WALLPAPER_THUMBNAIL_OLD).delete();
+ new File(getContext().getFilesDir(), PickerFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete();
+
+ for (int i = Build.VERSION_CODES.JELLY_BEAN; i < Build.VERSION.SDK_INT; i++) {
+ new File(getContext().getFilesDir(), i + "_"
+ + PickerFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete();
+ }
+ return writeImageToFileAsJpeg(getDefaultThumbFile(), b);
+ }
+
+ private ResourceWallpaperInfo getPreKKDefaultWallpaperInfo() {
+ Resources sysRes = Resources.getSystem();
+ int resId = sysRes.getIdentifier("default_wallpaper", "drawable", "android");
+
+ File defaultThumbFile = getDefaultThumbFile();
+ Bitmap thumb = null;
+ boolean defaultWallpaperExists = false;
+ if (defaultThumbFile.exists()) {
+ thumb = BitmapFactory.decodeFile(defaultThumbFile.getAbsolutePath());
+ defaultWallpaperExists = true;
+ } else {
+ Resources res = getResources();
+ Point defaultThumbSize = getDefaultThumbnailSize(res);
+ int rotation = BitmapUtils.getRotationFromExif(res, resId);
+ thumb = createThumbnail(
+ defaultThumbSize, getContext(), null, null, sysRes, resId, rotation, false);
+ if (thumb != null) {
+ defaultWallpaperExists = saveDefaultWallpaperThumb(thumb);
+ }
+ }
+ if (defaultWallpaperExists) {
+ return new ResourceWallpaperInfo(sysRes, resId, new BitmapDrawable(thumb),
+ mIsLockScreenPicker);
+ }
+ return null;
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private DefaultWallpaperInfo getDefaultWallpaper() {
+ File defaultThumbFile = getDefaultThumbFile();
+ Bitmap thumb = null;
+ boolean defaultWallpaperExists = false;
+ if (defaultThumbFile.exists()) {
+ thumb = BitmapFactory.decodeFile(defaultThumbFile.getAbsolutePath());
+ defaultWallpaperExists = true;
+ } else {
+ Resources res = getResources();
+ Point defaultThumbSize = getDefaultThumbnailSize(res);
+ Drawable wallpaperDrawable = WallpaperManager.getInstance(getContext()).getBuiltInDrawable(
+ defaultThumbSize.x, defaultThumbSize.y, true, 0.5f, 0.5f);
+ if (wallpaperDrawable != null) {
+ thumb = Bitmap.createBitmap(
+ defaultThumbSize.x, defaultThumbSize.y, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(thumb);
+ wallpaperDrawable.setBounds(0, 0, defaultThumbSize.x, defaultThumbSize.y);
+ wallpaperDrawable.draw(c);
+ c.setBitmap(null);
+ }
+ if (thumb != null) {
+ defaultWallpaperExists = saveDefaultWallpaperThumb(thumb);
+ }
+ }
+ if (defaultWallpaperExists) {
+ return new DefaultWallpaperInfo(new BitmapDrawable(thumb));
+ }
+ return null;
+ }
+
+ public Pair<ApplicationInfo, Integer> getWallpaperArrayResourceId() {
+ final String packageName = getResources().getResourcePackageName(R.array.wallpapers);
+ try {
+ ApplicationInfo info = getContext().getPackageManager()
+ .getApplicationInfo(packageName, 0);
+ return new Pair<>(info, R.array.wallpapers);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ private void addWallpapers(ArrayList<WallpaperTileInfo> known, Resources res,
+ String packageName, int listResId) {
+ final String[] extras = res.getStringArray(listResId);
+ for (String extra : extras) {
+ int resId = res.getIdentifier(extra, "drawable", packageName);
+ if (resId != 0) {
+ final int thumbRes = res.getIdentifier(extra + "_small", "drawable", packageName);
+
+ if (thumbRes != 0) {
+ ResourceWallpaperInfo wallpaperInfo =
+ new ResourceWallpaperInfo(res, resId, res.getDrawable(thumbRes),
+ mIsLockScreenPicker);
+ known.add(wallpaperInfo);
+ // Log.d(TAG, "add: [" + packageName + "]: " + extra + " (" + res + ")");
+ }
+ } else {
+ Log.e(TAG, "Couldn't find wallpaper " + extra);
+ }
+ }
+ }
+
+ public CropView getCropView() {
+ return mCropView;
+ }
+
+ public SavedWallpaperImages getSavedImages() {
+ return mSavedImages;
+ }
+
+ private static class SimpleWallpapersAdapter extends ArrayAdapter<WallpaperTileInfo> {
+ private final LayoutInflater mLayoutInflater;
+
+ SimpleWallpapersAdapter(Context context, ArrayList<WallpaperTileInfo> wallpapers) {
+ super(context, R.layout.wallpaper_picker_item, wallpapers);
+ mLayoutInflater = LayoutInflater.from(context);
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Drawable thumb = getItem(position).mThumb;
+ if (thumb == null) {
+ Log.e(TAG, "Error decoding thumbnail for wallpaper #" + position);
+ }
+ return createImageTileView(mLayoutInflater, convertView, parent, thumb);
+ }
+ }
+
+ public static View createImageTileView(LayoutInflater layoutInflater,
+ View convertView, ViewGroup parent, Drawable thumb) {
+ View view;
+
+ if (convertView == null) {
+ view = layoutInflater.inflate(R.layout.wallpaper_picker_item, parent, false);
+ } else {
+ view = convertView;
+ }
+
+ ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image);
+
+ if (thumb != null) {
+ image.setImageDrawable(thumb);
+ thumb.setDither(true);
+ }
+
+ return view;
+ }
+
+ public void startActivityForResultSafely(Intent intent, int requestCode) {
+ Utilities.startActivityForResultSafely(getActivity(), intent, requestCode);
+ }
+
+ private static class ThemeWallpapersAdapter extends BaseAdapter implements ListAdapter {
+ private LayoutInflater mLayoutInflater;
+ private ArrayList<ThemeWallpaperInfo> mWallpapers;
+
+ ThemeWallpapersAdapter(Activity activity, ArrayList<ThemeWallpaperInfo> wallpapers) {
+ mLayoutInflater = activity.getLayoutInflater();
+ mWallpapers = wallpapers;
+ }
+
+ public int getCount() {
+ return mWallpapers.size();
+ }
+
+ public ThemeWallpaperInfo getItem(int position) {
+ return mWallpapers.get(position);
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Drawable thumb = mWallpapers.get(position).mThumb;
+ if (thumb == null) {
+ Log.e(TAG, "Error decoding thumbnail for wallpaper #" + position);
+ }
+ return createImageTileView(mLayoutInflater, convertView, parent, thumb);
+ }
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/base/BaseActivity.java b/src/org/cyanogenmod/wallpaperpicker/base/BaseActivity.java
new file mode 100644
index 0000000..670a61a
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/base/BaseActivity.java
@@ -0,0 +1,21 @@
+package org.cyanogenmod.wallpaperpicker.base;
+
+import android.app.Activity;
+import android.content.Context;
+
+/**
+ * A wrapper over {@link Activity} which allows to override some methods.
+ * The base implementation can change from an Activity to a Fragment (or any other custom
+ * implementation), Callers should not assume that the base class extends Context, instead use
+ * either {@link #getContext} or {@link #getActivity}
+ */
+public class BaseActivity extends Activity {
+
+ public Context getContext() {
+ return this;
+ }
+
+ public Activity getActivity() {
+ return this;
+ }
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/util/Constants.java b/src/org/cyanogenmod/wallpaperpicker/util/Constants.java
new file mode 100644
index 0000000..111fb46
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/util/Constants.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cyanogenmod.wallpaperpicker.util;
+
+public class Constants {
+
+ public static final String THEME_WALLPAPER_PATH = "wallpapers";
+ public static final String THEME_LOCKSCREEN_PATH = "lockscreen";
+
+}
diff --git a/src/org/cyanogenmod/wallpaperpicker/util/Thunk.java b/src/org/cyanogenmod/wallpaperpicker/util/Thunk.java
new file mode 100644
index 0000000..6f35514
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/util/Thunk.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cyanogenmod.wallpaperpicker.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that the given field or method has package visibility solely to prevent the creation
+ * of a synthetic method. In practice, you should treat this field/method as if it were private.
+ * <p>
+ *
+ * When a private method is called from an inner class, the Java compiler generates a simple
+ * package private shim method that the class generated from the inner class can call. This results
+ * in unnecessary bloat and runtime method call overhead. It also gets us closer to the dex method
+ * count limit.
+ * <p>
+ *
+ * If you'd like to see warnings for these synthetic methods in eclipse, turn on:
+ * Window > Preferences > Java > Compiler > Errors/Warnings > "Access to a non-accessible member
+ * of an enclosing type".
+ * <p>
+ *
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.TYPE})
+public @interface Thunk { } \ No newline at end of file
diff --git a/src/org/cyanogenmod/wallpaperpicker/util/WallpaperUtils.java b/src/org/cyanogenmod/wallpaperpicker/util/WallpaperUtils.java
new file mode 100644
index 0000000..ced3844
--- /dev/null
+++ b/src/org/cyanogenmod/wallpaperpicker/util/WallpaperUtils.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cyanogenmod.wallpaperpicker.util;
+
+import android.annotation.TargetApi;
+import android.app.WallpaperManager;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.os.Build;
+import android.view.WindowManager;
+
+import org.cyanogenmod.wallpaperpicker.Utilities;
+
+/**
+ * Utility methods for wallpaper management.
+ */
+public final class WallpaperUtils {
+
+ public static final String WALLPAPER_WIDTH_KEY = "wallpaper.width";
+ public static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height";
+ public static final float WALLPAPER_SCREENS_SPAN = 2f;
+
+ public static void suggestWallpaperDimension(Resources res,
+ final SharedPreferences sharedPrefs,
+ WindowManager windowManager,
+ final WallpaperManager wallpaperManager, boolean fallBackToDefaults) {
+ final Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(res, windowManager);
+ // If we have saved a wallpaper width/height, use that instead
+
+ int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1);
+ int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, -1);
+
+ if (savedWidth == -1 || savedHeight == -1) {
+ if (!fallBackToDefaults) {
+ return;
+ } else {
+ savedWidth = defaultWallpaperSize.x;
+ savedHeight = defaultWallpaperSize.y;
+ }
+ }
+
+ if (savedWidth != wallpaperManager.getDesiredMinimumWidth() ||
+ savedHeight != wallpaperManager.getDesiredMinimumHeight()) {
+ wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight);
+ }
+ }
+
+ /**
+ * As a ratio of screen height, the total distance we want the parallax effect to span
+ * horizontally
+ */
+ public static float wallpaperTravelToScreenWidthRatio(int width, int height) {
+ float aspectRatio = width / (float) height;
+
+ // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width
+ // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width
+ // We will use these two data points to extrapolate how much the wallpaper parallax effect
+ // to span (ie travel) at any aspect ratio:
+
+ final float ASPECT_RATIO_LANDSCAPE = 16/10f;
+ final float ASPECT_RATIO_PORTRAIT = 10/16f;
+ final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f;
+ final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f;
+
+ // To find out the desired width at different aspect ratios, we use the following two
+ // formulas, where the coefficient on x is the aspect ratio (width/height):
+ // (16/10)x + y = 1.5
+ // (10/16)x + y = 1.2
+ // We solve for x and y and end up with a final formula:
+ final float x =
+ (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) /
+ (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT);
+ final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT;
+ return x * aspectRatio + y;
+ }
+
+ private static Point sDefaultWallpaperSize;
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public static Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) {
+ if (sDefaultWallpaperSize == null) {
+ Point minDims = new Point();
+ Point maxDims = new Point();
+ windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
+
+ int maxDim = Math.max(maxDims.x, maxDims.y);
+ int minDim = Math.max(minDims.x, minDims.y);
+
+ if (Utilities.ATLEAST_JB_MR1) {
+ Point realSize = new Point();
+ windowManager.getDefaultDisplay().getRealSize(realSize);
+ maxDim = Math.max(realSize.x, realSize.y);
+ minDim = Math.min(realSize.x, realSize.y);
+ }
+
+ // We need to ensure that there is enough extra space in the wallpaper
+ // for the intended parallax effects
+ final int defaultWidth, defaultHeight;
+ if (res.getConfiguration().smallestScreenWidthDp >= 720) {
+ defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
+ defaultHeight = maxDim;
+ } else {
+ defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
+ defaultHeight = maxDim;
+ }
+ sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight);
+ }
+ return sDefaultWallpaperSize;
+ }
+}