From aa05711971753bb31cc01f46d4ab10fcf9f9af3b Mon Sep 17 00:00:00 2001 From: Michael Jurka Date: Thu, 12 Dec 2013 15:04:25 +0100 Subject: Create separate project for Wallpaper Picker Change-Id: Id9e855780b9fb68c63eb6e9f6c19bcbce28a6fd5 --- .../com/android/gallery3d/common/BitmapUtils.java | 260 +++ .../src/com/android/gallery3d/common/Utils.java | 340 +++ .../gallery3d/exif/ByteBufferInputStream.java | 48 + .../gallery3d/exif/CountedDataInputStream.java | 136 ++ .../src/com/android/gallery3d/exif/ExifData.java | 348 +++ .../com/android/gallery3d/exif/ExifInterface.java | 2407 ++++++++++++++++++++ .../gallery3d/exif/ExifInvalidFormatException.java | 23 + .../com/android/gallery3d/exif/ExifModifier.java | 196 ++ .../android/gallery3d/exif/ExifOutputStream.java | 518 +++++ .../src/com/android/gallery3d/exif/ExifParser.java | 916 ++++++++ .../src/com/android/gallery3d/exif/ExifReader.java | 92 + .../src/com/android/gallery3d/exif/ExifTag.java | 1008 ++++++++ .../src/com/android/gallery3d/exif/IfdData.java | 152 ++ .../src/com/android/gallery3d/exif/IfdId.java | 31 + .../src/com/android/gallery3d/exif/JpegHeader.java | 39 + .../gallery3d/exif/OrderedDataOutputStream.java | 56 + .../src/com/android/gallery3d/exif/Rational.java | 88 + .../android/gallery3d/glrenderer/BasicTexture.java | 212 ++ .../gallery3d/glrenderer/BitmapTexture.java | 54 + .../com/android/gallery3d/glrenderer/GLCanvas.java | 217 ++ .../android/gallery3d/glrenderer/GLES20Canvas.java | 1009 ++++++++ .../android/gallery3d/glrenderer/GLES20IdImpl.java | 42 + .../src/com/android/gallery3d/glrenderer/GLId.java | 33 + .../com/android/gallery3d/glrenderer/GLPaint.java | 41 + .../android/gallery3d/glrenderer/RawTexture.java | 73 + .../com/android/gallery3d/glrenderer/Texture.java | 44 + .../gallery3d/glrenderer/UploadedTexture.java | 298 +++ .../src/com/android/gallery3d/util/IntArray.java | 60 + .../android/launcher3/CheckableFrameLayout.java | 63 + .../src/com/android/launcher3/CropView.java | 322 +++ .../com/android/launcher3/DrawableTileSource.java | 102 + .../launcher3/LiveWallpaperListAdapter.java | 204 ++ .../android/launcher3/SavedWallpaperImages.java | 241 ++ .../ThirdPartyWallpaperPickerListAdapter.java | 139 ++ .../android/launcher3/WallpaperCropActivity.java | 836 +++++++ .../android/launcher3/WallpaperPickerActivity.java | 1044 +++++++++ .../com/android/launcher3/WallpaperRootView.java | 39 + .../com/android/photos/BitmapRegionTileSource.java | 524 +++++ .../photos/views/BlockingGLTextureView.java | 438 ++++ .../android/photos/views/TiledImageRenderer.java | 825 +++++++ .../com/android/photos/views/TiledImageView.java | 386 ++++ 41 files changed, 13904 insertions(+) create mode 100644 WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/common/Utils.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/ByteBufferInputStream.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/CountedDataInputStream.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/ExifData.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/ExifInterface.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/ExifInvalidFormatException.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/ExifModifier.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/ExifOutputStream.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/ExifParser.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/ExifReader.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/ExifTag.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/IfdData.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/IfdId.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/JpegHeader.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/OrderedDataOutputStream.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/exif/Rational.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/glrenderer/BitmapTexture.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/glrenderer/GLCanvas.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20IdImpl.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/glrenderer/GLId.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/glrenderer/GLPaint.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/glrenderer/RawTexture.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/glrenderer/Texture.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java create mode 100644 WallpaperPicker/src/com/android/gallery3d/util/IntArray.java create mode 100644 WallpaperPicker/src/com/android/launcher3/CheckableFrameLayout.java create mode 100644 WallpaperPicker/src/com/android/launcher3/CropView.java create mode 100644 WallpaperPicker/src/com/android/launcher3/DrawableTileSource.java create mode 100644 WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java create mode 100644 WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java create mode 100644 WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java create mode 100644 WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java create mode 100644 WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java create mode 100644 WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java create mode 100644 WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java create mode 100644 WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java create mode 100644 WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java create mode 100644 WallpaperPicker/src/com/android/photos/views/TiledImageView.java (limited to 'WallpaperPicker/src/com/android') diff --git a/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java b/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java new file mode 100644 index 000000000..a671ed2b9 --- /dev/null +++ b/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.os.Build; +import android.util.FloatMath; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class BitmapUtils { + private static final String TAG = "BitmapUtils"; + private static final int DEFAULT_JPEG_QUALITY = 90; + public static final int UNCONSTRAINED = -1; + + private BitmapUtils(){} + + /* + * Compute the sample size as a function of minSideLength + * and maxNumOfPixels. + * minSideLength is used to specify that minimal width or height of a + * bitmap. + * maxNumOfPixels is used to specify the maximal size in pixels that is + * tolerable in terms of memory usage. + * + * The function returns a sample size based on the constraints. + * Both size and minSideLength can be passed in as UNCONSTRAINED, + * which indicates no care of the corresponding constraint. + * The functions prefers returning a sample size that + * generates a smaller bitmap, unless minSideLength = UNCONSTRAINED. + * + * Also, the function rounds up the sample size to a power of 2 or multiple + * of 8 because BitmapFactory only honors sample size this way. + * For example, BitmapFactory downsamples an image by 2 even though the + * request is 3. So we round up the sample size to avoid OOM. + */ + public static int computeSampleSize(int width, int height, + int minSideLength, int maxNumOfPixels) { + int initialSize = computeInitialSampleSize( + width, height, minSideLength, maxNumOfPixels); + + return initialSize <= 8 + ? Utils.nextPowerOf2(initialSize) + : (initialSize + 7) / 8 * 8; + } + + private static int computeInitialSampleSize(int w, int h, + int minSideLength, int maxNumOfPixels) { + if (maxNumOfPixels == UNCONSTRAINED + && minSideLength == UNCONSTRAINED) return 1; + + int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 : + (int) FloatMath.ceil(FloatMath.sqrt((float) (w * h) / maxNumOfPixels)); + + if (minSideLength == UNCONSTRAINED) { + return lowerBound; + } else { + int sampleSize = Math.min(w / minSideLength, h / minSideLength); + return Math.max(sampleSize, lowerBound); + } + } + + // This computes a sample size which makes the longer side at least + // minSideLength long. If that's not possible, return 1. + public static int computeSampleSizeLarger(int w, int h, + int minSideLength) { + int initialSize = Math.max(w / minSideLength, h / minSideLength); + if (initialSize <= 1) return 1; + + return initialSize <= 8 + ? Utils.prevPowerOf2(initialSize) + : initialSize / 8 * 8; + } + + // Find the min x that 1 / x >= scale + public static int computeSampleSizeLarger(float scale) { + int initialSize = (int) FloatMath.floor(1f / scale); + if (initialSize <= 1) return 1; + + return initialSize <= 8 + ? Utils.prevPowerOf2(initialSize) + : initialSize / 8 * 8; + } + + // Find the max x that 1 / x <= scale. + public static int computeSampleSize(float scale) { + Utils.assertTrue(scale > 0); + int initialSize = Math.max(1, (int) FloatMath.ceil(1 / scale)); + return initialSize <= 8 + ? Utils.nextPowerOf2(initialSize) + : (initialSize + 7) / 8 * 8; + } + + public static Bitmap resizeBitmapByScale( + Bitmap bitmap, float scale, boolean recycle) { + int width = Math.round(bitmap.getWidth() * scale); + int height = Math.round(bitmap.getHeight() * scale); + if (width == bitmap.getWidth() + && height == bitmap.getHeight()) return bitmap; + Bitmap target = Bitmap.createBitmap(width, height, getConfig(bitmap)); + Canvas canvas = new Canvas(target); + canvas.scale(scale, scale); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); + canvas.drawBitmap(bitmap, 0, 0, paint); + if (recycle) bitmap.recycle(); + return target; + } + + private static Bitmap.Config getConfig(Bitmap bitmap) { + Bitmap.Config config = bitmap.getConfig(); + if (config == null) { + config = Bitmap.Config.ARGB_8888; + } + return config; + } + + public static Bitmap resizeDownBySideLength( + Bitmap bitmap, int maxLength, boolean recycle) { + int srcWidth = bitmap.getWidth(); + int srcHeight = bitmap.getHeight(); + float scale = Math.min( + (float) maxLength / srcWidth, (float) maxLength / srcHeight); + if (scale >= 1.0f) return bitmap; + return resizeBitmapByScale(bitmap, scale, recycle); + } + + public static Bitmap resizeAndCropCenter(Bitmap bitmap, int size, boolean recycle) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + if (w == size && h == size) return bitmap; + + // scale the image so that the shorter side equals to the target; + // the longer side will be center-cropped. + float scale = (float) size / Math.min(w, h); + + Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap)); + int width = Math.round(scale * bitmap.getWidth()); + int height = Math.round(scale * bitmap.getHeight()); + Canvas canvas = new Canvas(target); + canvas.translate((size - width) / 2f, (size - height) / 2f); + canvas.scale(scale, scale); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG); + canvas.drawBitmap(bitmap, 0, 0, paint); + if (recycle) bitmap.recycle(); + return target; + } + + public static void recycleSilently(Bitmap bitmap) { + if (bitmap == null) return; + try { + bitmap.recycle(); + } catch (Throwable t) { + Log.w(TAG, "unable recycle bitmap", t); + } + } + + public static Bitmap rotateBitmap(Bitmap source, int rotation, boolean recycle) { + if (rotation == 0) return source; + int w = source.getWidth(); + int h = source.getHeight(); + Matrix m = new Matrix(); + m.postRotate(rotation); + Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, w, h, m, true); + if (recycle) source.recycle(); + return bitmap; + } + + public static Bitmap createVideoThumbnail(String filePath) { + // MediaMetadataRetriever is available on API Level 8 + // but is hidden until API Level 10 + Class clazz = null; + Object instance = null; + try { + clazz = Class.forName("android.media.MediaMetadataRetriever"); + instance = clazz.newInstance(); + + Method method = clazz.getMethod("setDataSource", String.class); + method.invoke(instance, filePath); + + // The method name changes between API Level 9 and 10. + if (Build.VERSION.SDK_INT <= 9) { + return (Bitmap) clazz.getMethod("captureFrame").invoke(instance); + } else { + byte[] data = (byte[]) clazz.getMethod("getEmbeddedPicture").invoke(instance); + if (data != null) { + Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + if (bitmap != null) return bitmap; + } + return (Bitmap) clazz.getMethod("getFrameAtTime").invoke(instance); + } + } catch (IllegalArgumentException ex) { + // Assume this is a corrupt video file + } catch (RuntimeException ex) { + // Assume this is a corrupt video file. + } catch (InstantiationException e) { + Log.e(TAG, "createVideoThumbnail", e); + } catch (InvocationTargetException e) { + Log.e(TAG, "createVideoThumbnail", e); + } catch (ClassNotFoundException e) { + Log.e(TAG, "createVideoThumbnail", e); + } catch (NoSuchMethodException e) { + Log.e(TAG, "createVideoThumbnail", e); + } catch (IllegalAccessException e) { + Log.e(TAG, "createVideoThumbnail", e); + } finally { + try { + if (instance != null) { + clazz.getMethod("release").invoke(instance); + } + } catch (Exception ignored) { + } + } + return null; + } + + public static byte[] compressToBytes(Bitmap bitmap) { + return compressToBytes(bitmap, DEFAULT_JPEG_QUALITY); + } + + public static byte[] compressToBytes(Bitmap bitmap, int quality) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(65536); + bitmap.compress(CompressFormat.JPEG, quality, baos); + return baos.toByteArray(); + } + + public static boolean isSupportedByRegionDecoder(String mimeType) { + if (mimeType == null) return false; + mimeType = mimeType.toLowerCase(); + return mimeType.startsWith("image/") && + (!mimeType.equals("image/gif") && !mimeType.endsWith("bmp")); + } + + public static boolean isRotationSupported(String mimeType) { + if (mimeType == null) return false; + mimeType = mimeType.toLowerCase(); + return mimeType.equals("image/jpeg"); + } +} diff --git a/WallpaperPicker/src/com/android/gallery3d/common/Utils.java b/WallpaperPicker/src/com/android/gallery3d/common/Utils.java new file mode 100644 index 000000000..614a081c8 --- /dev/null +++ b/WallpaperPicker/src/com/android/gallery3d/common/Utils.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.Cursor; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.text.TextUtils; +import android.util.Log; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InterruptedIOException; + +public class Utils { + private static final String TAG = "Utils"; + private static final String DEBUG_TAG = "GalleryDebug"; + + private static final long POLY64REV = 0x95AC9329AC4BC9B5L; + private static final long INITIALCRC = 0xFFFFFFFFFFFFFFFFL; + + private static long[] sCrcTable = new long[256]; + + private static final boolean IS_DEBUG_BUILD = + Build.TYPE.equals("eng") || Build.TYPE.equals("userdebug"); + + private static final String MASK_STRING = "********************************"; + + // Throws AssertionError if the input is false. + public static void assertTrue(boolean cond) { + if (!cond) { + throw new AssertionError(); + } + } + + // Throws AssertionError with the message. We had a method having the form + // assertTrue(boolean cond, String message, Object ... args); + // However a call to that method will cause memory allocation even if the + // condition is false (due to autoboxing generated by "Object ... args"), + // so we don't use that anymore. + public static void fail(String message, Object ... args) { + throw new AssertionError( + args.length == 0 ? message : String.format(message, args)); + } + + // Throws NullPointerException if the input is null. + public static T checkNotNull(T object) { + if (object == null) throw new NullPointerException(); + return object; + } + + // Returns true if two input Object are both null or equal + // to each other. + public static boolean equals(Object a, Object b) { + return (a == b) || (a == null ? false : a.equals(b)); + } + + // 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; + } + + // Returns the input value x clamped to the range [min, max]. + public static float clamp(float x, float min, float max) { + if (x > max) return max; + if (x < min) return min; + return x; + } + + // Returns the input value x clamped to the range [min, max]. + public static long clamp(long x, long min, long max) { + if (x > max) return max; + if (x < min) return min; + return x; + } + + public static boolean isOpaque(int color) { + return color >>> 24 == 0xFF; + } + + public static void swap(int[] array, int i, int j) { + int temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + + /** + * A function thats returns a 64-bit crc for string + * + * @param in input string + * @return a 64-bit crc value + */ + public static final long crc64Long(String in) { + if (in == null || in.length() == 0) { + return 0; + } + return crc64Long(getBytes(in)); + } + + static { + // http://bioinf.cs.ucl.ac.uk/downloads/crc64/crc64.c + long part; + for (int i = 0; i < 256; i++) { + part = i; + for (int j = 0; j < 8; j++) { + long x = ((int) part & 1) != 0 ? POLY64REV : 0; + part = (part >> 1) ^ x; + } + sCrcTable[i] = part; + } + } + + public static final long crc64Long(byte[] buffer) { + long crc = INITIALCRC; + for (int k = 0, n = buffer.length; k < n; ++k) { + crc = sCrcTable[(((int) crc) ^ buffer[k]) & 0xff] ^ (crc >> 8); + } + return crc; + } + + public static byte[] getBytes(String in) { + byte[] result = new byte[in.length() * 2]; + int output = 0; + for (char ch : in.toCharArray()) { + result[output++] = (byte) (ch & 0xFF); + result[output++] = (byte) (ch >> 8); + } + return result; + } + + public static void closeSilently(Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (IOException t) { + Log.w(TAG, "close fail ", t); + } + } + + public static int compare(long a, long b) { + return a < b ? -1 : a == b ? 0 : 1; + } + + 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(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 float interpolateAngle( + float source, float target, float progress) { + // interpolate the angle from source to target + // We make the difference in the range of [-179, 180], this is the + // shortest path to change source to target. + float diff = target - source; + if (diff < 0) diff += 360f; + if (diff > 180) diff -= 360f; + + float result = source + diff * progress; + return result < 0 ? result + 360f : result; + } + + public static float interpolateScale( + float source, float target, float progress) { + return source + progress * (target - source); + } + + public static String ensureNotNull(String value) { + return value == null ? "" : value; + } + + public static float parseFloatSafely(String content, float defaultValue) { + if (content == null) return defaultValue; + try { + return Float.parseFloat(content); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + public static int parseIntSafely(String content, int defaultValue) { + if (content == null) return defaultValue; + try { + return Integer.parseInt(content); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + public static boolean isNullOrEmpty(String exifMake) { + return TextUtils.isEmpty(exifMake); + } + + public static void waitWithoutInterrupt(Object object) { + try { + object.wait(); + } catch (InterruptedException e) { + Log.w(TAG, "unexpected interrupt: " + object); + } + } + + public static boolean handleInterrruptedException(Throwable e) { + // A helper to deal with the interrupt exception + // If an interrupt detected, we will setup the bit again. + if (e instanceof InterruptedIOException + || e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + return true; + } + return false; + } + + /** + * @return String with special XML characters escaped. + */ + public static String escapeXml(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0, len = s.length(); i < len; ++i) { + char c = s.charAt(i); + switch (c) { + case '<': sb.append("<"); break; + case '>': sb.append(">"); break; + case '\"': sb.append("""); break; + case '\'': sb.append("'"); break; + case '&': sb.append("&"); break; + default: sb.append(c); + } + } + return sb.toString(); + } + + public static String getUserAgent(Context context) { + PackageInfo packageInfo; + try { + packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + } catch (NameNotFoundException e) { + throw new IllegalStateException("getPackageInfo failed"); + } + return String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s", + packageInfo.packageName, + packageInfo.versionName, + Build.BRAND, + Build.DEVICE, + Build.MODEL, + Build.ID, + Build.VERSION.SDK_INT, + Build.VERSION.RELEASE, + Build.VERSION.INCREMENTAL); + } + + public static String[] copyOf(String[] source, int newSize) { + String[] result = new String[newSize]; + newSize = Math.min(source.length, newSize); + System.arraycopy(source, 0, result, 0, newSize); + return result; + } + + // Mask information for debugging only. It returns info.toString() directly + // for debugging build (i.e., 'eng' and 'userdebug') and returns a mask ("****") + // in release build to protect the information (e.g. for privacy issue). + public static String maskDebugInfo(Object info) { + if (info == null) return null; + String s = info.toString(); + int length = Math.min(s.length(), MASK_STRING.length()); + return IS_DEBUG_BUILD ? s : MASK_STRING.substring(0, length); + } + + // This method should be ONLY used for debugging. + public static void debug(String message, Object ... args) { + Log.v(DEBUG_TAG, String.format(message, args)); + } +} diff --git a/WallpaperPicker/src/com/android/gallery3d/exif/ByteBufferInputStream.java b/WallpaperPicker/src/com/android/gallery3d/exif/ByteBufferInputStream.java new file mode 100644 index 000000000..7fb9f22cc --- /dev/null +++ b/WallpaperPicker/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/WallpaperPicker/src/com/android/gallery3d/exif/CountedDataInputStream.java b/WallpaperPicker/src/com/android/gallery3d/exif/CountedDataInputStream.java new file mode 100644 index 000000000..dfd4a1a10 --- /dev/null +++ b/WallpaperPicker/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/WallpaperPicker/src/com/android/gallery3d/exif/ExifData.java b/WallpaperPicker/src/com/android/gallery3d/exif/ExifData.java new file mode 100644 index 000000000..8422382bb --- /dev/null +++ b/WallpaperPicker/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 mStripBytes = new ArrayList(); + 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 getAllTags() { + ArrayList ret = new ArrayList(); + 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 getAllTagsForIfd(int ifd) { + IfdData d = mIfdDatas[ifd]; + if (d == null) { + return null; + } + ExifTag[] tags = d.getAllTags(); + if (tags == null) { + return null; + } + ArrayList ret = new ArrayList(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 getAllTagsForTagId(short tag) { + ArrayList ret = new ArrayList(); + 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/WallpaperPicker/src/com/android/gallery3d/exif/ExifInterface.java b/WallpaperPicker/src/com/android/gallery3d/exif/ExifInterface.java new file mode 100644 index 000000000..a1cf0fc85 --- /dev/null +++ b/WallpaperPicker/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. + *

+ * 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 sOffsetTags = new HashSet(); + 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 sBannedDefines = new HashSet(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: + *

    + *
  • TOP_LEFT is the normal orientation.
  • + *
  • TOP_RIGHT is a left-right mirror.
  • + *
  • BOTTOM_LEFT is a 180 degree rotation.
  • + *
  • BOTTOM_RIGHT is a top-bottom mirror.
  • + *
  • LEFT_TOP is mirrored about the top-left<->bottom-right axis.
  • + *
  • RIGHT_TOP is a 90 degree clockwise rotation.
  • + *
  • LEFT_BOTTOM is mirrored about the top-right<->bottom-left axis.
  • + *
  • RIGHT_BOTTOM is a 270 degree clockwise rotation.
  • + *
+ */ + 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. + *

+ * e.g. + *

+ * 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 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 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 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 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 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 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 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 new Long(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 new Integer(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 new Byte(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} + *

+ * 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 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 l = new ArrayList(); + 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/WallpaperPicker/src/com/android/gallery3d/exif/ExifInvalidFormatException.java b/WallpaperPicker/src/com/android/gallery3d/exif/ExifInvalidFormatException.java new file mode 100644 index 000000000..bf923ec26 --- /dev/null +++ b/WallpaperPicker/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/WallpaperPicker/src/com/android/gallery3d/exif/ExifModifier.java b/WallpaperPicker/src/com/android/gallery3d/exif/ExifModifier.java new file mode 100644 index 000000000..f00362b6b --- /dev/null +++ b/WallpaperPicker/src/com/android/gallery3d/exif/ExifModifier.java @@ -0,0 +1,196 @@ +/* + * 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.Closeable; +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 mTagOffsets = new ArrayList(); + 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/WallpaperPicker/src/com/android/gallery3d/exif/ExifOutputStream.java b/WallpaperPicker/src/com/android/gallery3d/exif/ExifOutputStream.java new file mode 100644 index 000000000..7ca05f2e0 --- /dev/null +++ b/WallpaperPicker/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. + *

+ * Below is an example of writing EXIF data into a file + * + *

+ * 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();
+ *             }
+ *         }
+ *     }
+ * }
+ * 
+ */ +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 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 stripNullValueTags(ExifData data) { + ArrayList nullTags = new ArrayList(); + 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/WallpaperPicker/src/com/android/gallery3d/exif/ExifParser.java b/WallpaperPicker/src/com/android/gallery3d/exif/ExifParser.java new file mode 100644 index 000000000..5467d423d --- /dev/null +++ b/WallpaperPicker/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. + *

+ * Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the + * parser. + * + *

+ * 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.
+ * }
+ * 
+ */ +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 mCorrespondingEvent = new TreeMap(); + + 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 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. + *

+ * 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. + *

+ * 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 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/WallpaperPicker/src/com/android/gallery3d/exif/ExifReader.java b/WallpaperPicker/src/com/android/gallery3d/exif/ExifReader.java new file mode 100644 index 000000000..68e972fb7 --- /dev/null +++ b/WallpaperPicker/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/WallpaperPicker/src/com/android/gallery3d/exif/ExifTag.java b/WallpaperPicker/src/com/android/gallery3d/exif/ExifTag.java new file mode 100644 index 000000000..b8b387201 --- /dev/null +++ b/WallpaperPicker/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: + *

    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT}, + * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.
  • + *
  • The value overflows.
  • + *
  • The value.length does NOT match the component count in the definition + * for this tag.
  • + *
+ */ + 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: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT}, + * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.
  • + *
  • The value overflows.
  • + *
  • The component count in the definition of this tag is not 1.
  • + *
+ */ + 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: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.
  • + *
  • The value overflows.
  • + *
  • The value.length does NOT match the component count in the definition + * for this tag.
  • + *
+ */ + 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: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.
  • + *
  • The value overflows.
  • + *
  • The component count in the definition for this tag is not 1.
  • + *
+ */ + 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: + *
    + *
  • The data type is not {@link #TYPE_ASCII} or {@link #TYPE_UNDEFINED}.
  • + *
  • The length of the string is not equal to (component count -1) or + * (component count) in the definition for this tag.
  • + *
+ */ + 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: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} + * or {@link #TYPE_RATIONAL}.
  • + *
  • The value overflows.
  • + *
  • The value.length does NOT match the component count in the definition + * for this tag.
  • + *
+ * + * @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: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} + * or {@link #TYPE_RATIONAL}.
  • + *
  • The value overflows.
  • + *
  • The component count in the definition for this tag is not 1.
  • + *
+ * + * @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: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or + * {@link #TYPE_UNDEFINED} .
  • + *
  • The length does NOT match the component count in the definition for + * this tag.
  • + *
+ */ + 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: + *
    + *
  • The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or + * {@link #TYPE_UNDEFINED} .
  • + *
  • The component count in the definition for this tag is not 1.
  • + *
+ */ + 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: + *
    + *
  • The corresponding setValue method for the class of the object passed + * in would fail.
  • + *
  • There is no obvious way to cast the object passed in into an EXIF tag + * type.
  • + *
+ */ + 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/WallpaperPicker/src/com/android/gallery3d/exif/IfdData.java b/WallpaperPicker/src/com/android/gallery3d/exif/IfdData.java new file mode 100644 index 000000000..093944aec --- /dev/null +++ b/WallpaperPicker/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 mExifTags = new HashMap(); + 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/WallpaperPicker/src/com/android/gallery3d/exif/IfdId.java b/WallpaperPicker/src/com/android/gallery3d/exif/IfdId.java new file mode 100644 index 000000000..7842edbd4 --- /dev/null +++ b/WallpaperPicker/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/WallpaperPicker/src/com/android/gallery3d/exif/JpegHeader.java b/WallpaperPicker/src/com/android/gallery3d/exif/JpegHeader.java new file mode 100644 index 000000000..e3e787eff --- /dev/null +++ b/WallpaperPicker/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/WallpaperPicker/src/com/android/gallery3d/exif/OrderedDataOutputStream.java b/WallpaperPicker/src/com/android/gallery3d/exif/OrderedDataOutputStream.java new file mode 100644 index 000000000..428e6b9fc --- /dev/null +++ b/WallpaperPicker/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/WallpaperPicker/src/com/android/gallery3d/exif/Rational.java b/WallpaperPicker/src/com/android/gallery3d/exif/Rational.java new file mode 100644 index 000000000..591d63faf --- /dev/null +++ b/WallpaperPicker/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/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java new file mode 100644 index 000000000..2e77b903f --- /dev/null +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.glrenderer; + +import android.util.Log; + +import com.android.gallery3d.common.Utils; + +import java.util.WeakHashMap; + +// BasicTexture is a Texture corresponds to a real GL texture. +// The state of a BasicTexture indicates whether its data is loaded to GL memory. +// If a BasicTexture is loaded into GL memory, it has a GL texture id. +public abstract class BasicTexture implements Texture { + + @SuppressWarnings("unused") + private static final String TAG = "BasicTexture"; + protected static final int UNSPECIFIED = -1; + + protected static final int STATE_UNLOADED = 0; + protected static final int STATE_LOADED = 1; + protected static final int STATE_ERROR = -1; + + // Log a warning if a texture is larger along a dimension + private static final int MAX_TEXTURE_SIZE = 4096; + + protected int mId = -1; + protected int mState; + + protected int mWidth = UNSPECIFIED; + protected int mHeight = UNSPECIFIED; + + protected int mTextureWidth; + protected int mTextureHeight; + + private boolean mHasBorder; + + protected GLCanvas mCanvasRef = null; + private static WeakHashMap sAllTextures + = new WeakHashMap(); + private static ThreadLocal sInFinalizer = new ThreadLocal(); + + protected BasicTexture(GLCanvas canvas, int id, int state) { + setAssociatedCanvas(canvas); + mId = id; + mState = state; + synchronized (sAllTextures) { + sAllTextures.put(this, null); + } + } + + protected BasicTexture() { + this(null, 0, STATE_UNLOADED); + } + + protected void setAssociatedCanvas(GLCanvas canvas) { + mCanvasRef = canvas; + } + + /** + * 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/WallpaperPicker/src/com/android/gallery3d/glrenderer/BitmapTexture.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/BitmapTexture.java new file mode 100644 index 000000000..100b0b3b9 --- /dev/null +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/BitmapTexture.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.glrenderer; + +import android.graphics.Bitmap; + +import junit.framework.Assert; + +// BitmapTexture is a texture whose content is specified by a fixed Bitmap. +// +// The texture does not own the Bitmap. The user should make sure the Bitmap +// is valid during the texture's lifetime. When the texture is recycled, it +// does not free the Bitmap. +public class BitmapTexture extends UploadedTexture { + protected Bitmap mContentBitmap; + + public BitmapTexture(Bitmap bitmap) { + this(bitmap, false); + } + + public BitmapTexture(Bitmap bitmap, boolean hasBorder) { + super(hasBorder); + Assert.assertTrue(bitmap != null && !bitmap.isRecycled()); + mContentBitmap = bitmap; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + // Do nothing. + } + + @Override + protected Bitmap onGetBitmap() { + return mContentBitmap; + } + + public Bitmap getBitmap() { + return mContentBitmap; + } +} diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLCanvas.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLCanvas.java new file mode 100644 index 000000000..305e90521 --- /dev/null +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLCanvas.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; + +import javax.microedition.khronos.opengles.GL11; + +// +// GLCanvas gives a convenient interface to draw using OpenGL. +// +// When a rectangle is specified in this interface, it means the region +// [x, x+width) * [y, y+height) +// +public interface GLCanvas { + + public GLId getGLId(); + + // Tells GLCanvas the size of the underlying GL surface. This should be + // called before first drawing and when the size of GL surface is changed. + // This is called by GLRoot and should not be called by the clients + // who only want to draw on the GLCanvas. Both width and height must be + // nonnegative. + public abstract void setSize(int width, int height); + + // Clear the drawing buffers. This should only be used by GLRoot. + public abstract void clearBuffer(); + + public abstract void clearBuffer(float[] argb); + + // Sets and gets the current alpha, alpha must be in [0, 1]. + public abstract void setAlpha(float alpha); + + public abstract float getAlpha(); + + // (current alpha) = (current alpha) * alpha + public abstract void multiplyAlpha(float alpha); + + // Change the current transform matrix. + public abstract void translate(float x, float y, float z); + + public abstract void translate(float x, float y); + + public abstract void scale(float sx, float sy, float sz); + + public abstract void rotate(float angle, float x, float y, float z); + + public abstract void multiplyMatrix(float[] mMatrix, int offset); + + // Pushes the configuration state (matrix, and alpha) onto + // a private stack. + public abstract void save(); + + // Same as save(), but only save those specified in saveFlags. + public abstract void save(int saveFlags); + + public static final int SAVE_FLAG_ALL = 0xFFFFFFFF; + public static final int SAVE_FLAG_ALPHA = 0x01; + public static final int SAVE_FLAG_MATRIX = 0x02; + + // Pops from the top of the stack as current configuration state (matrix, + // alpha, and clip). This call balances a previous call to save(), and is + // used to remove all modifications to the configuration state since the + // last save call. + public abstract void restore(); + + // Draws a line using the specified paint from (x1, y1) to (x2, y2). + // (Both end points are included). + public abstract void drawLine(float x1, float y1, float x2, float y2, GLPaint paint); + + // Draws a rectangle using the specified paint from (x1, y1) to (x2, y2). + // (Both end points are included). + public abstract void drawRect(float x1, float y1, float x2, float y2, GLPaint paint); + + // Fills the specified rectangle with the specified color. + public abstract void fillRect(float x, float y, float width, float height, int color); + + // Draws a texture to the specified rectangle. + public abstract void drawTexture( + BasicTexture texture, int x, int y, int width, int height); + + public abstract void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, + int uvBuffer, int indexBuffer, int indexCount); + + // Draws the source rectangle part of the texture to the target rectangle. + public abstract void drawTexture(BasicTexture texture, RectF source, RectF target); + + // Draw a texture with a specified texture transform. + public abstract void drawTexture(BasicTexture texture, float[] mTextureTransform, + int x, int y, int w, int h); + + // Draw two textures to the specified rectangle. The actual texture used is + // from * (1 - ratio) + to * ratio + // The two textures must have the same size. + public abstract void drawMixed(BasicTexture from, int toColor, + float ratio, int x, int y, int w, int h); + + // Draw a region of a texture and a specified color to the specified + // rectangle. The actual color used is from * (1 - ratio) + to * ratio. + // The region of the texture is defined by parameter "src". The target + // rectangle is specified by parameter "target". + public abstract void drawMixed(BasicTexture from, int toColor, + float ratio, RectF src, RectF target); + + // Unloads the specified texture from the canvas. The resource allocated + // to draw the texture will be released. The specified texture will return + // to the unloaded state. This function should be called only from + // BasicTexture or its descendant + public abstract boolean unloadTexture(BasicTexture texture); + + // Delete the specified buffer object, similar to unloadTexture. + public abstract void deleteBuffer(int bufferId); + + // Delete the textures and buffers in GL side. This function should only be + // called in the GL thread. + public abstract void deleteRecycledResources(); + + // Dump statistics information and clear the counters. For debug only. + public abstract void dumpStatisticsAndClear(); + + public abstract void beginRenderTarget(RawTexture texture); + + public abstract void endRenderTarget(); + + /** + * Sets texture parameters to use GL_CLAMP_TO_EDGE for both + * GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T. Sets texture parameters to be + * GL_LINEAR for GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER. + * bindTexture() must be called prior to this. + * + * @param texture The texture to set parameters on. + */ + public abstract void setTextureParameters(BasicTexture texture); + + /** + * Initializes the texture to a size by calling texImage2D on it. + * + * @param texture The texture to initialize the size. + * @param format The texture format (e.g. GL_RGBA) + * @param type The texture type (e.g. GL_UNSIGNED_BYTE) + */ + public abstract void initializeTextureSize(BasicTexture texture, int format, int type); + + /** + * Initializes the texture to a size by calling texImage2D on it. + * + * @param texture The texture to initialize the size. + * @param bitmap The bitmap to initialize the bitmap with. + */ + public abstract void initializeTexture(BasicTexture texture, Bitmap bitmap); + + /** + * Calls glTexSubImage2D to upload a bitmap to the texture. + * + * @param texture The target texture to write to. + * @param xOffset Specifies a texel offset in the x direction within the + * texture array. + * @param yOffset Specifies a texel offset in the y direction within the + * texture array. + * @param format The texture format (e.g. GL_RGBA) + * @param type The texture type (e.g. GL_UNSIGNED_BYTE) + */ + public abstract void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, + Bitmap bitmap, + int format, int type); + + /** + * Generates buffers and uploads the buffer data. + * + * @param buffer The buffer to upload + * @return The buffer ID that was generated. + */ + public abstract int uploadBuffer(java.nio.FloatBuffer buffer); + + /** + * Generates buffers and uploads the element array buffer data. + * + * @param buffer The buffer to upload + * @return The buffer ID that was generated. + */ + public abstract int uploadBuffer(java.nio.ByteBuffer buffer); + + /** + * After LightCycle makes GL calls, this method is called to restore the GL + * configuration to the one expected by GLCanvas. + */ + public abstract void recoverFromLightCycle(); + + /** + * Gets the bounds given by x, y, width, and height as well as the internal + * matrix state. There is no special handling for non-90-degree rotations. + * It only considers the lower-left and upper-right corners as the bounds. + * + * @param bounds The output bounds to write to. + * @param x The left side of the input rectangle. + * @param y The bottom of the input rectangle. + * @param width The width of the input rectangle. + * @param height The height of the input rectangle. + */ + public abstract void getBounds(Rect bounds, int x, int y, int width, int height); +} diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java new file mode 100644 index 000000000..4ead1315e --- /dev/null +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java @@ -0,0 +1,1009 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.gallery3d.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.opengl.Matrix; +import android.util.Log; + +import com.android.gallery3d.util.IntArray; + +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Arrays; + +public class GLES20Canvas implements GLCanvas { + // ************** Constants ********************** + private static final String TAG = GLES20Canvas.class.getSimpleName(); + private static final int FLOAT_SIZE = Float.SIZE / Byte.SIZE; + private static final float OPAQUE_ALPHA = 0.95f; + + private static final int COORDS_PER_VERTEX = 2; + private static final int VERTEX_STRIDE = COORDS_PER_VERTEX * FLOAT_SIZE; + + private static final int COUNT_FILL_VERTEX = 4; + private static final int COUNT_LINE_VERTEX = 2; + private static final int COUNT_RECT_VERTEX = 4; + private static final int OFFSET_FILL_RECT = 0; + private static final int OFFSET_DRAW_LINE = OFFSET_FILL_RECT + COUNT_FILL_VERTEX; + private static final int OFFSET_DRAW_RECT = OFFSET_DRAW_LINE + COUNT_LINE_VERTEX; + + private static final float[] BOX_COORDINATES = { + 0, 0, // Fill rectangle + 1, 0, + 0, 1, + 1, 1, + 0, 0, // Draw line + 1, 1, + 0, 0, // Draw rectangle outline + 0, 1, + 1, 1, + 1, 0, + }; + + private static final float[] BOUNDS_COORDINATES = { + 0, 0, 0, 1, + 1, 1, 0, 1, + }; + + private static final String POSITION_ATTRIBUTE = "aPosition"; + private static final String COLOR_UNIFORM = "uColor"; + private static final String MATRIX_UNIFORM = "uMatrix"; + private static final String TEXTURE_MATRIX_UNIFORM = "uTextureMatrix"; + private static final String TEXTURE_SAMPLER_UNIFORM = "uTextureSampler"; + private static final String ALPHA_UNIFORM = "uAlpha"; + private static final String TEXTURE_COORD_ATTRIBUTE = "aTextureCoordinate"; + + private static final String DRAW_VERTEX_SHADER = "" + + "uniform mat4 " + MATRIX_UNIFORM + ";\n" + + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" + + "void main() {\n" + + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" + + "}\n"; + + private static final String DRAW_FRAGMENT_SHADER = "" + + "precision mediump float;\n" + + "uniform vec4 " + COLOR_UNIFORM + ";\n" + + "void main() {\n" + + " gl_FragColor = " + COLOR_UNIFORM + ";\n" + + "}\n"; + + private static final String TEXTURE_VERTEX_SHADER = "" + + "uniform mat4 " + MATRIX_UNIFORM + ";\n" + + "uniform mat4 " + TEXTURE_MATRIX_UNIFORM + ";\n" + + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" + + " vTextureCoord = (" + TEXTURE_MATRIX_UNIFORM + " * pos).xy;\n" + + "}\n"; + + private static final String MESH_VERTEX_SHADER = "" + + "uniform mat4 " + MATRIX_UNIFORM + ";\n" + + "attribute vec2 " + POSITION_ATTRIBUTE + ";\n" + + "attribute vec2 " + TEXTURE_COORD_ATTRIBUTE + ";\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " vec4 pos = vec4(" + POSITION_ATTRIBUTE + ", 0.0, 1.0);\n" + + " gl_Position = " + MATRIX_UNIFORM + " * pos;\n" + + " vTextureCoord = " + TEXTURE_COORD_ATTRIBUTE + ";\n" + + "}\n"; + + private static final String TEXTURE_FRAGMENT_SHADER = "" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform float " + ALPHA_UNIFORM + ";\n" + + "uniform sampler2D " + TEXTURE_SAMPLER_UNIFORM + ";\n" + + "void main() {\n" + + " gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n" + + " gl_FragColor *= " + ALPHA_UNIFORM + ";\n" + + "}\n"; + + private static final String OES_TEXTURE_FRAGMENT_SHADER = "" + + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform float " + ALPHA_UNIFORM + ";\n" + + "uniform samplerExternalOES " + TEXTURE_SAMPLER_UNIFORM + ";\n" + + "void main() {\n" + + " gl_FragColor = texture2D(" + TEXTURE_SAMPLER_UNIFORM + ", vTextureCoord);\n" + + " gl_FragColor *= " + ALPHA_UNIFORM + ";\n" + + "}\n"; + + private static final int INITIAL_RESTORE_STATE_SIZE = 8; + private static final int MATRIX_SIZE = 16; + + // Keep track of restore state + private float[] mMatrices = new float[INITIAL_RESTORE_STATE_SIZE * MATRIX_SIZE]; + private float[] mAlphas = new float[INITIAL_RESTORE_STATE_SIZE]; + private IntArray mSaveFlags = new IntArray(); + + private int mCurrentAlphaIndex = 0; + private int mCurrentMatrixIndex = 0; + + // Viewport size + private int mWidth; + private int mHeight; + + // Projection matrix + private float[] mProjectionMatrix = new float[MATRIX_SIZE]; + + // Screen size for when we aren't bound to a texture + private int mScreenWidth; + private int mScreenHeight; + + // GL programs + private int mDrawProgram; + private int mTextureProgram; + private int mOesTextureProgram; + private int mMeshProgram; + + // GL buffer containing BOX_COORDINATES + private int mBoxCoordinates; + + // Handle indices -- common + private static final int INDEX_POSITION = 0; + private static final int INDEX_MATRIX = 1; + + // Handle indices -- draw + private static final int INDEX_COLOR = 2; + + // Handle indices -- texture + private static final int INDEX_TEXTURE_MATRIX = 2; + private static final int INDEX_TEXTURE_SAMPLER = 3; + private static final int INDEX_ALPHA = 4; + + // Handle indices -- mesh + private static final int INDEX_TEXTURE_COORD = 2; + + private abstract static class ShaderParameter { + public int handle; + protected final String mName; + + public ShaderParameter(String name) { + mName = name; + } + + public abstract void loadHandle(int program); + } + + private static class UniformShaderParameter extends ShaderParameter { + public UniformShaderParameter(String name) { + super(name); + } + + @Override + public void loadHandle(int program) { + handle = GLES20.glGetUniformLocation(program, mName); + checkError(); + } + } + + private static class AttributeShaderParameter extends ShaderParameter { + public AttributeShaderParameter(String name) { + super(name); + } + + @Override + public void loadHandle(int program) { + handle = GLES20.glGetAttribLocation(program, mName); + checkError(); + } + } + + ShaderParameter[] mDrawParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new UniformShaderParameter(COLOR_UNIFORM), // INDEX_COLOR + }; + ShaderParameter[] mTextureParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX + new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER + new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA + }; + ShaderParameter[] mOesTextureParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX + new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER + new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA + }; + ShaderParameter[] mMeshParameters = { + new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION + new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX + new AttributeShaderParameter(TEXTURE_COORD_ATTRIBUTE), // INDEX_TEXTURE_COORD + new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER + new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA + }; + + private final IntArray mUnboundTextures = new IntArray(); + private final IntArray mDeleteBuffers = new IntArray(); + + // Keep track of statistics for debugging + private int mCountDrawMesh = 0; + private int mCountTextureRect = 0; + private int mCountFillRect = 0; + private int mCountDrawLine = 0; + + // Buffer for framebuffer IDs -- we keep track so we can switch the attached + // texture. + private int[] mFrameBuffer = new int[1]; + + // Bound textures. + private ArrayList mTargetTextures = new ArrayList(); + + // Temporary variables used within calculations + private final float[] mTempMatrix = new float[32]; + private final float[] mTempColor = new float[4]; + private final RectF mTempSourceRect = new RectF(); + private final RectF mTempTargetRect = new RectF(); + private final float[] mTempTextureMatrix = new float[MATRIX_SIZE]; + private final int[] mTempIntArray = new int[1]; + + private static final GLId mGLId = new GLES20IdImpl(); + + public GLES20Canvas() { + Matrix.setIdentityM(mTempTextureMatrix, 0); + Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex); + mAlphas[mCurrentAlphaIndex] = 1f; + mTargetTextures.add(null); + + FloatBuffer boxBuffer = createBuffer(BOX_COORDINATES); + mBoxCoordinates = uploadBuffer(boxBuffer); + + int drawVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DRAW_VERTEX_SHADER); + int textureVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, TEXTURE_VERTEX_SHADER); + int meshVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, MESH_VERTEX_SHADER); + int drawFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DRAW_FRAGMENT_SHADER); + int textureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, TEXTURE_FRAGMENT_SHADER); + int oesTextureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, + OES_TEXTURE_FRAGMENT_SHADER); + + mDrawProgram = assembleProgram(drawVertexShader, drawFragmentShader, mDrawParameters); + mTextureProgram = assembleProgram(textureVertexShader, textureFragmentShader, + mTextureParameters); + mOesTextureProgram = assembleProgram(textureVertexShader, oesTextureFragmentShader, + mOesTextureParameters); + mMeshProgram = assembleProgram(meshVertexShader, textureFragmentShader, mMeshParameters); + GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + checkError(); + } + + private static FloatBuffer createBuffer(float[] values) { + // First create an nio buffer, then create a VBO from it. + int size = values.length * FLOAT_SIZE; + FloatBuffer buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + buffer.put(values, 0, values.length).position(0); + return buffer; + } + + private int assembleProgram(int vertexShader, int fragmentShader, ShaderParameter[] params) { + int program = GLES20.glCreateProgram(); + checkError(); + if (program == 0) { + throw new RuntimeException("Cannot create GL program: " + GLES20.glGetError()); + } + GLES20.glAttachShader(program, vertexShader); + checkError(); + GLES20.glAttachShader(program, fragmentShader); + checkError(); + GLES20.glLinkProgram(program); + checkError(); + int[] mLinkStatus = mTempIntArray; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, mLinkStatus, 0); + if (mLinkStatus[0] != GLES20.GL_TRUE) { + Log.e(TAG, "Could not link program: "); + Log.e(TAG, GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + program = 0; + } + for (int i = 0; i < params.length; i++) { + params[i].loadHandle(program); + } + return program; + } + + private static int loadShader(int type, String shaderCode) { + // create a vertex shader type (GLES20.GL_VERTEX_SHADER) + // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) + int shader = GLES20.glCreateShader(type); + + // add the source code to the shader and compile it + GLES20.glShaderSource(shader, shaderCode); + checkError(); + GLES20.glCompileShader(shader); + checkError(); + + return shader; + } + + @Override + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + GLES20.glViewport(0, 0, mWidth, mHeight); + checkError(); + Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex); + Matrix.orthoM(mProjectionMatrix, 0, 0, width, 0, height, -1, 1); + if (getTargetTexture() == null) { + mScreenWidth = width; + mScreenHeight = height; + Matrix.translateM(mMatrices, mCurrentMatrixIndex, 0, height, 0); + Matrix.scaleM(mMatrices, mCurrentMatrixIndex, 1, -1, 1); + } + } + + @Override + public void clearBuffer() { + GLES20.glClearColor(0f, 0f, 0f, 1f); + checkError(); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + checkError(); + } + + @Override + public void clearBuffer(float[] argb) { + GLES20.glClearColor(argb[1], argb[2], argb[3], argb[0]); + checkError(); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + checkError(); + } + + @Override + public float getAlpha() { + return mAlphas[mCurrentAlphaIndex]; + } + + @Override + public void setAlpha(float alpha) { + mAlphas[mCurrentAlphaIndex] = alpha; + } + + @Override + public void multiplyAlpha(float alpha) { + setAlpha(getAlpha() * alpha); + } + + @Override + public void translate(float x, float y, float z) { + Matrix.translateM(mMatrices, mCurrentMatrixIndex, x, y, z); + } + + // This is a faster version of translate(x, y, z) because + // (1) we knows z = 0, (2) we inline the Matrix.translateM call, + // (3) we unroll the loop + @Override + public void translate(float x, float y) { + int index = mCurrentMatrixIndex; + float[] m = mMatrices; + m[index + 12] += m[index + 0] * x + m[index + 4] * y; + m[index + 13] += m[index + 1] * x + m[index + 5] * y; + m[index + 14] += m[index + 2] * x + m[index + 6] * y; + m[index + 15] += m[index + 3] * x + m[index + 7] * y; + } + + @Override + public void scale(float sx, float sy, float sz) { + Matrix.scaleM(mMatrices, mCurrentMatrixIndex, sx, sy, sz); + } + + @Override + public void rotate(float angle, float x, float y, float z) { + if (angle == 0f) { + return; + } + float[] temp = mTempMatrix; + Matrix.setRotateM(temp, 0, angle, x, y, z); + float[] matrix = mMatrices; + int index = mCurrentMatrixIndex; + Matrix.multiplyMM(temp, MATRIX_SIZE, matrix, index, temp, 0); + System.arraycopy(temp, MATRIX_SIZE, matrix, index, MATRIX_SIZE); + } + + @Override + public void multiplyMatrix(float[] matrix, int offset) { + float[] temp = mTempMatrix; + float[] currentMatrix = mMatrices; + int index = mCurrentMatrixIndex; + Matrix.multiplyMM(temp, 0, currentMatrix, index, matrix, offset); + System.arraycopy(temp, 0, currentMatrix, index, 16); + } + + @Override + public void save() { + save(SAVE_FLAG_ALL); + } + + @Override + public void save(int saveFlags) { + boolean saveAlpha = (saveFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA; + if (saveAlpha) { + float currentAlpha = getAlpha(); + mCurrentAlphaIndex++; + if (mAlphas.length <= mCurrentAlphaIndex) { + mAlphas = Arrays.copyOf(mAlphas, mAlphas.length * 2); + } + mAlphas[mCurrentAlphaIndex] = currentAlpha; + } + boolean saveMatrix = (saveFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX; + if (saveMatrix) { + int currentIndex = mCurrentMatrixIndex; + mCurrentMatrixIndex += MATRIX_SIZE; + if (mMatrices.length <= mCurrentMatrixIndex) { + mMatrices = Arrays.copyOf(mMatrices, mMatrices.length * 2); + } + System.arraycopy(mMatrices, currentIndex, mMatrices, mCurrentMatrixIndex, MATRIX_SIZE); + } + mSaveFlags.add(saveFlags); + } + + @Override + public void restore() { + int restoreFlags = mSaveFlags.removeLast(); + boolean restoreAlpha = (restoreFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA; + if (restoreAlpha) { + mCurrentAlphaIndex--; + } + boolean restoreMatrix = (restoreFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX; + if (restoreMatrix) { + mCurrentMatrixIndex -= MATRIX_SIZE; + } + } + + @Override + public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) { + draw(GLES20.GL_LINE_STRIP, OFFSET_DRAW_LINE, COUNT_LINE_VERTEX, x1, y1, x2 - x1, y2 - y1, + paint); + mCountDrawLine++; + } + + @Override + public void drawRect(float x, float y, float width, float height, GLPaint paint) { + draw(GLES20.GL_LINE_LOOP, OFFSET_DRAW_RECT, COUNT_RECT_VERTEX, x, y, width, height, paint); + mCountDrawLine++; + } + + private void draw(int type, int offset, int count, float x, float y, float width, float height, + GLPaint paint) { + draw(type, offset, count, x, y, width, height, paint.getColor(), paint.getLineWidth()); + } + + private void draw(int type, int offset, int count, float x, float y, float width, float height, + int color, float lineWidth) { + prepareDraw(offset, color, lineWidth); + draw(mDrawParameters, type, count, x, y, width, height); + } + + private void prepareDraw(int offset, int color, float lineWidth) { + GLES20.glUseProgram(mDrawProgram); + checkError(); + if (lineWidth > 0) { + GLES20.glLineWidth(lineWidth); + checkError(); + } + float[] colorArray = getColor(color); + boolean blendingEnabled = (colorArray[3] < 1f); + enableBlending(blendingEnabled); + if (blendingEnabled) { + GLES20.glBlendColor(colorArray[0], colorArray[1], colorArray[2], colorArray[3]); + checkError(); + } + + GLES20.glUniform4fv(mDrawParameters[INDEX_COLOR].handle, 1, colorArray, 0); + setPosition(mDrawParameters, offset); + checkError(); + } + + private float[] getColor(int color) { + float alpha = ((color >>> 24) & 0xFF) / 255f * getAlpha(); + float red = ((color >>> 16) & 0xFF) / 255f * alpha; + float green = ((color >>> 8) & 0xFF) / 255f * alpha; + float blue = (color & 0xFF) / 255f * alpha; + mTempColor[0] = red; + mTempColor[1] = green; + mTempColor[2] = blue; + mTempColor[3] = alpha; + return mTempColor; + } + + private void enableBlending(boolean enableBlending) { + if (enableBlending) { + GLES20.glEnable(GLES20.GL_BLEND); + checkError(); + } else { + GLES20.glDisable(GLES20.GL_BLEND); + checkError(); + } + } + + private void setPosition(ShaderParameter[] params, int offset) { + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mBoxCoordinates); + checkError(); + GLES20.glVertexAttribPointer(params[INDEX_POSITION].handle, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, VERTEX_STRIDE, offset * VERTEX_STRIDE); + checkError(); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + checkError(); + } + + private void draw(ShaderParameter[] params, int type, int count, float x, float y, float width, + float height) { + setMatrix(params, x, y, width, height); + int positionHandle = params[INDEX_POSITION].handle; + GLES20.glEnableVertexAttribArray(positionHandle); + checkError(); + GLES20.glDrawArrays(type, 0, count); + checkError(); + GLES20.glDisableVertexAttribArray(positionHandle); + checkError(); + } + + private void setMatrix(ShaderParameter[] params, float x, float y, float width, float height) { + Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f); + Matrix.scaleM(mTempMatrix, 0, width, height, 1f); + Matrix.multiplyMM(mTempMatrix, MATRIX_SIZE, mProjectionMatrix, 0, mTempMatrix, 0); + GLES20.glUniformMatrix4fv(params[INDEX_MATRIX].handle, 1, false, mTempMatrix, MATRIX_SIZE); + checkError(); + } + + @Override + public void fillRect(float x, float y, float width, float height, int color) { + draw(GLES20.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, COUNT_FILL_VERTEX, x, y, width, height, + color, 0f); + mCountFillRect++; + } + + @Override + public void drawTexture(BasicTexture texture, int x, int y, int width, int height) { + if (width <= 0 || height <= 0) { + return; + } + copyTextureCoordinates(texture, mTempSourceRect); + mTempTargetRect.set(x, y, x + width, y + height); + convertCoordinate(mTempSourceRect, mTempTargetRect, texture); + drawTextureRect(texture, mTempSourceRect, mTempTargetRect); + } + + private static void copyTextureCoordinates(BasicTexture texture, RectF outRect) { + int left = 0; + int top = 0; + int right = texture.getWidth(); + int bottom = texture.getHeight(); + if (texture.hasBorder()) { + left = 1; + top = 1; + right -= 1; + bottom -= 1; + } + outRect.set(left, top, right, bottom); + } + + @Override + public void drawTexture(BasicTexture texture, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) { + return; + } + mTempSourceRect.set(source); + mTempTargetRect.set(target); + + convertCoordinate(mTempSourceRect, mTempTargetRect, texture); + drawTextureRect(texture, mTempSourceRect, mTempTargetRect); + } + + @Override + public void drawTexture(BasicTexture texture, float[] textureTransform, int x, int y, int w, + int h) { + if (w <= 0 || h <= 0) { + return; + } + mTempTargetRect.set(x, y, x + w, y + h); + drawTextureRect(texture, textureTransform, mTempTargetRect); + } + + private void drawTextureRect(BasicTexture texture, RectF source, RectF target) { + setTextureMatrix(source); + drawTextureRect(texture, mTempTextureMatrix, target); + } + + private void setTextureMatrix(RectF source) { + mTempTextureMatrix[0] = source.width(); + mTempTextureMatrix[5] = source.height(); + mTempTextureMatrix[12] = source.left; + mTempTextureMatrix[13] = source.top; + } + + // This function changes the source coordinate to the texture coordinates. + // It also clips the source and target coordinates if it is beyond the + // bound of the texture. + private static void convertCoordinate(RectF source, RectF target, BasicTexture texture) { + int width = texture.getWidth(); + int height = texture.getHeight(); + int texWidth = texture.getTextureWidth(); + int texHeight = texture.getTextureHeight(); + // Convert to texture coordinates + source.left /= texWidth; + source.right /= texWidth; + source.top /= texHeight; + source.bottom /= texHeight; + + // Clip if the rendering range is beyond the bound of the texture. + float xBound = (float) width / texWidth; + if (source.right > xBound) { + target.right = target.left + target.width() * (xBound - source.left) / source.width(); + source.right = xBound; + } + float yBound = (float) height / texHeight; + if (source.bottom > yBound) { + target.bottom = target.top + target.height() * (yBound - source.top) / source.height(); + source.bottom = yBound; + } + } + + private void drawTextureRect(BasicTexture texture, float[] textureMatrix, RectF target) { + ShaderParameter[] params = prepareTexture(texture); + setPosition(params, OFFSET_FILL_RECT); + GLES20.glUniformMatrix4fv(params[INDEX_TEXTURE_MATRIX].handle, 1, false, textureMatrix, 0); + checkError(); + if (texture.isFlippedVertically()) { + save(SAVE_FLAG_MATRIX); + translate(0, target.centerY()); + scale(1, -1, 1); + translate(0, -target.centerY()); + } + draw(params, GLES20.GL_TRIANGLE_STRIP, COUNT_FILL_VERTEX, target.left, target.top, + target.width(), target.height()); + if (texture.isFlippedVertically()) { + restore(); + } + mCountTextureRect++; + } + + private ShaderParameter[] prepareTexture(BasicTexture texture) { + ShaderParameter[] params; + int program; + if (texture.getTarget() == GLES20.GL_TEXTURE_2D) { + params = mTextureParameters; + program = mTextureProgram; + } else { + params = mOesTextureParameters; + program = mOesTextureProgram; + } + prepareTexture(texture, program, params); + return params; + } + + private void prepareTexture(BasicTexture texture, int program, ShaderParameter[] params) { + GLES20.glUseProgram(program); + checkError(); + enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + checkError(); + texture.onBind(this); + GLES20.glBindTexture(texture.getTarget(), texture.getId()); + checkError(); + GLES20.glUniform1i(params[INDEX_TEXTURE_SAMPLER].handle, 0); + checkError(); + GLES20.glUniform1f(params[INDEX_ALPHA].handle, getAlpha()); + checkError(); + } + + @Override + public void drawMesh(BasicTexture texture, int x, int y, int xyBuffer, int uvBuffer, + int indexBuffer, int indexCount) { + prepareTexture(texture, mMeshProgram, mMeshParameters); + + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer); + checkError(); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, xyBuffer); + checkError(); + int positionHandle = mMeshParameters[INDEX_POSITION].handle; + GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, + VERTEX_STRIDE, 0); + checkError(); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, uvBuffer); + checkError(); + int texCoordHandle = mMeshParameters[INDEX_TEXTURE_COORD].handle; + GLES20.glVertexAttribPointer(texCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, + false, VERTEX_STRIDE, 0); + checkError(); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + checkError(); + + GLES20.glEnableVertexAttribArray(positionHandle); + checkError(); + GLES20.glEnableVertexAttribArray(texCoordHandle); + checkError(); + + setMatrix(mMeshParameters, x, y, 1, 1); + GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, indexCount, GLES20.GL_UNSIGNED_BYTE, 0); + checkError(); + + GLES20.glDisableVertexAttribArray(positionHandle); + checkError(); + GLES20.glDisableVertexAttribArray(texCoordHandle); + checkError(); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + checkError(); + mCountDrawMesh++; + } + + @Override + public void drawMixed(BasicTexture texture, int toColor, float ratio, int x, int y, int w, int h) { + copyTextureCoordinates(texture, mTempSourceRect); + mTempTargetRect.set(x, y, x + w, y + h); + drawMixed(texture, toColor, ratio, mTempSourceRect, mTempTargetRect); + } + + @Override + public void drawMixed(BasicTexture texture, int toColor, float ratio, RectF source, RectF target) { + if (target.width() <= 0 || target.height() <= 0) { + return; + } + save(SAVE_FLAG_ALPHA); + + float currentAlpha = getAlpha(); + float cappedRatio = Math.min(1f, Math.max(0f, ratio)); + + float textureAlpha = (1f - cappedRatio) * currentAlpha; + setAlpha(textureAlpha); + drawTexture(texture, source, target); + + float colorAlpha = cappedRatio * currentAlpha; + setAlpha(colorAlpha); + fillRect(target.left, target.top, target.width(), target.height(), toColor); + + restore(); + } + + @Override + public boolean unloadTexture(BasicTexture texture) { + boolean unload = texture.isLoaded(); + if (unload) { + synchronized (mUnboundTextures) { + mUnboundTextures.add(texture.getId()); + } + } + return unload; + } + + @Override + public void deleteBuffer(int bufferId) { + synchronized (mUnboundTextures) { + mDeleteBuffers.add(bufferId); + } + } + + @Override + public void deleteRecycledResources() { + synchronized (mUnboundTextures) { + IntArray ids = mUnboundTextures; + if (mUnboundTextures.size() > 0) { + mGLId.glDeleteTextures(null, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + + ids = mDeleteBuffers; + if (ids.size() > 0) { + mGLId.glDeleteBuffers(null, ids.size(), ids.getInternalArray(), 0); + ids.clear(); + } + } + } + + @Override + public void dumpStatisticsAndClear() { + String line = String.format("MESH:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d", mCountDrawMesh, + mCountTextureRect, mCountFillRect, mCountDrawLine); + mCountDrawMesh = 0; + mCountTextureRect = 0; + mCountFillRect = 0; + mCountDrawLine = 0; + Log.d(TAG, line); + } + + @Override + public void endRenderTarget() { + RawTexture oldTexture = mTargetTextures.remove(mTargetTextures.size() - 1); + RawTexture texture = getTargetTexture(); + setRenderTarget(oldTexture, texture); + restore(); // restore matrix and alpha + } + + @Override + public void beginRenderTarget(RawTexture texture) { + save(); // save matrix and alpha and blending + RawTexture oldTexture = getTargetTexture(); + mTargetTextures.add(texture); + setRenderTarget(oldTexture, texture); + } + + private RawTexture getTargetTexture() { + return mTargetTextures.get(mTargetTextures.size() - 1); + } + + private void setRenderTarget(BasicTexture oldTexture, RawTexture texture) { + if (oldTexture == null && texture != null) { + GLES20.glGenFramebuffers(1, mFrameBuffer, 0); + checkError(); + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer[0]); + checkError(); + } else if (oldTexture != null && texture == null) { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + checkError(); + GLES20.glDeleteFramebuffers(1, mFrameBuffer, 0); + checkError(); + } + + if (texture == null) { + setSize(mScreenWidth, mScreenHeight); + } else { + setSize(texture.getWidth(), texture.getHeight()); + + if (!texture.isLoaded()) { + texture.prepare(this); + } + + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, + texture.getTarget(), texture.getId(), 0); + checkError(); + + checkFramebufferStatus(); + } + } + + private static void checkFramebufferStatus() { + int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER); + if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) { + String msg = ""; + switch (status) { + case GLES20.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + msg = "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"; + break; + case GLES20.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS: + msg = "GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS"; + break; + case GLES20.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + msg = "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"; + break; + case GLES20.GL_FRAMEBUFFER_UNSUPPORTED: + msg = "GL_FRAMEBUFFER_UNSUPPORTED"; + break; + } + throw new RuntimeException(msg + ":" + Integer.toHexString(status)); + } + } + + @Override + public void setTextureParameters(BasicTexture texture) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + } + + @Override + public void initializeTextureSize(BasicTexture texture, int format, int type) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + int width = texture.getTextureWidth(); + int height = texture.getTextureHeight(); + GLES20.glTexImage2D(target, 0, format, width, height, 0, format, type, null); + } + + @Override + public void initializeTexture(BasicTexture texture, Bitmap bitmap) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + GLUtils.texImage2D(target, 0, bitmap, 0); + } + + @Override + public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap, + int format, int type) { + int target = texture.getTarget(); + GLES20.glBindTexture(target, texture.getId()); + checkError(); + GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type); + } + + @Override + public int uploadBuffer(FloatBuffer buf) { + return uploadBuffer(buf, FLOAT_SIZE); + } + + @Override + public int uploadBuffer(ByteBuffer buf) { + return uploadBuffer(buf, 1); + } + + private int uploadBuffer(Buffer buffer, int elementSize) { + mGLId.glGenBuffers(1, mTempIntArray, 0); + checkError(); + int bufferId = mTempIntArray[0]; + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferId); + checkError(); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, buffer.capacity() * elementSize, buffer, + GLES20.GL_STATIC_DRAW); + checkError(); + return bufferId; + } + + public static void checkError() { + int error = GLES20.glGetError(); + if (error != 0) { + Throwable t = new Throwable(); + Log.e(TAG, "GL error: " + error, t); + } + } + + @SuppressWarnings("unused") + private static void printMatrix(String message, float[] m, int offset) { + StringBuilder b = new StringBuilder(message); + for (int i = 0; i < MATRIX_SIZE; i++) { + b.append(' '); + if (i % 4 == 0) { + b.append('\n'); + } + b.append(m[offset + i]); + } + Log.v(TAG, b.toString()); + } + + @Override + public void recoverFromLightCycle() { + GLES20.glViewport(0, 0, mWidth, mHeight); + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); + checkError(); + } + + @Override + public void getBounds(Rect bounds, int x, int y, int width, int height) { + Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f); + Matrix.scaleM(mTempMatrix, 0, width, height, 1f); + Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE, mTempMatrix, 0, BOUNDS_COORDINATES, 0); + Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE + 4, mTempMatrix, 0, BOUNDS_COORDINATES, 4); + bounds.left = Math.round(mTempMatrix[MATRIX_SIZE]); + bounds.right = Math.round(mTempMatrix[MATRIX_SIZE + 4]); + bounds.top = Math.round(mTempMatrix[MATRIX_SIZE + 1]); + bounds.bottom = Math.round(mTempMatrix[MATRIX_SIZE + 5]); + bounds.sort(); + } + + @Override + public GLId getGLId() { + return mGLId; + } +} diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20IdImpl.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20IdImpl.java new file mode 100644 index 000000000..6cd7149cb --- /dev/null +++ b/WallpaperPicker/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/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLId.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLId.java new file mode 100644 index 000000000..3cec558f6 --- /dev/null +++ b/WallpaperPicker/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/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLPaint.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLPaint.java new file mode 100644 index 000000000..16b220690 --- /dev/null +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLPaint.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.glrenderer; + +import junit.framework.Assert; + +public class GLPaint { + private float mLineWidth = 1f; + private int mColor = 0; + + public void setColor(int color) { + mColor = color; + } + + public int getColor() { + return mColor; + } + + public void setLineWidth(float width) { + Assert.assertTrue(width >= 0); + mLineWidth = width; + } + + public float getLineWidth() { + return mLineWidth; + } +} diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/RawTexture.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/RawTexture.java new file mode 100644 index 000000000..93f0fdff9 --- /dev/null +++ b/WallpaperPicker/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/WallpaperPicker/src/com/android/gallery3d/glrenderer/Texture.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/Texture.java new file mode 100644 index 000000000..3dcae4aec --- /dev/null +++ b/WallpaperPicker/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/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java new file mode 100644 index 000000000..f41a979b7 --- /dev/null +++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.glrenderer; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.opengl.GLUtils; + +import junit.framework.Assert; + +import java.util.HashMap; + +import javax.microedition.khronos.opengles.GL11; + +// UploadedTextures use a Bitmap for the content of the texture. +// +// Subclasses should implement onGetBitmap() to provide the Bitmap and +// implement onFreeBitmap(mBitmap) which will be called when the Bitmap +// is not needed anymore. +// +// isContentValid() is meaningful only when the isLoaded() returns true. +// It means whether the content needs to be updated. +// +// The user of this class should call recycle() when the texture is not +// needed anymore. +// +// By default an UploadedTexture is opaque (so it can be drawn faster without +// blending). The user or subclass can override it using setOpaque(). +public abstract class UploadedTexture extends BasicTexture { + + // To prevent keeping allocation the borders, we store those used borders here. + // Since the length will be power of two, it won't use too much memory. + private static HashMap sBorderLines = + new HashMap(); + private static BorderKey sBorderKey = new BorderKey(); + + @SuppressWarnings("unused") + private static final String TAG = "Texture"; + private boolean mContentValid = true; + + // indicate this textures is being uploaded in background + private boolean mIsUploading = false; + private boolean mOpaque = true; + private boolean mThrottled = false; + private static int sUploadedCount; + private static final int UPLOAD_LIMIT = 100; + + protected Bitmap mBitmap; + private int mBorder; + + protected UploadedTexture() { + this(false); + } + + protected UploadedTexture(boolean hasBorder) { + super(null, 0, STATE_UNLOADED); + if (hasBorder) { + setBorder(true); + mBorder = 1; + } + } + + protected void setIsUploading(boolean uploading) { + mIsUploading = uploading; + } + + public boolean isUploading() { + return mIsUploading; + } + + private static class BorderKey implements Cloneable { + public boolean vertical; + public Config config; + public int length; + + @Override + public int hashCode() { + int x = config.hashCode() ^ length; + return vertical ? x : -x; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof BorderKey)) return false; + BorderKey o = (BorderKey) object; + return vertical == o.vertical + && config == o.config && length == o.length; + } + + @Override + public BorderKey clone() { + try { + return (BorderKey) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); + } + } + } + + protected void setThrottled(boolean throttled) { + mThrottled = throttled; + } + + private static Bitmap getBorderLine( + boolean vertical, Config config, int length) { + BorderKey key = sBorderKey; + key.vertical = vertical; + key.config = config; + key.length = length; + Bitmap bitmap = sBorderLines.get(key); + if (bitmap == null) { + bitmap = vertical + ? Bitmap.createBitmap(1, length, config) + : Bitmap.createBitmap(length, 1, config); + sBorderLines.put(key.clone(), bitmap); + } + return bitmap; + } + + private Bitmap getBitmap() { + if (mBitmap == null) { + mBitmap = onGetBitmap(); + int w = mBitmap.getWidth() + mBorder * 2; + int h = mBitmap.getHeight() + mBorder * 2; + if (mWidth == UNSPECIFIED) { + setSize(w, h); + } + } + return mBitmap; + } + + private void freeBitmap() { + Assert.assertTrue(mBitmap != null); + onFreeBitmap(mBitmap); + mBitmap = null; + } + + @Override + public int getWidth() { + if (mWidth == UNSPECIFIED) getBitmap(); + return mWidth; + } + + @Override + public int getHeight() { + if (mWidth == UNSPECIFIED) getBitmap(); + return mHeight; + } + + protected abstract Bitmap onGetBitmap(); + + protected abstract void onFreeBitmap(Bitmap bitmap); + + protected void invalidateContent() { + if (mBitmap != null) freeBitmap(); + mContentValid = false; + mWidth = UNSPECIFIED; + mHeight = UNSPECIFIED; + } + + /** + * Whether the content on GPU is valid. + */ + public boolean isContentValid() { + return isLoaded() && mContentValid; + } + + /** + * Updates the content on GPU's memory. + * @param canvas + */ + public void updateContent(GLCanvas canvas) { + if (!isLoaded()) { + if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) { + return; + } + uploadToCanvas(canvas); + } else if (!mContentValid) { + Bitmap bitmap = getBitmap(); + int format = GLUtils.getInternalFormat(bitmap); + int type = GLUtils.getType(bitmap); + canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type); + freeBitmap(); + mContentValid = true; + } + } + + public static void resetUploadLimit() { + sUploadedCount = 0; + } + + public static boolean uploadLimitReached() { + return sUploadedCount > UPLOAD_LIMIT; + } + + private void uploadToCanvas(GLCanvas canvas) { + + Bitmap bitmap = getBitmap(); + if (bitmap != null) { + try { + int bWidth = bitmap.getWidth(); + int bHeight = bitmap.getHeight(); + int width = bWidth + mBorder * 2; + int height = bHeight + mBorder * 2; + int texWidth = getTextureWidth(); + int texHeight = getTextureHeight(); + + Assert.assertTrue(bWidth <= texWidth && bHeight <= texHeight); + + // Upload the bitmap to a new texture. + mId = canvas.getGLId().generateTexture(); + canvas.setTextureParameters(this); + + if (bWidth == texWidth && bHeight == texHeight) { + canvas.initializeTexture(this, bitmap); + } else { + int format = GLUtils.getInternalFormat(bitmap); + int type = GLUtils.getType(bitmap); + Config config = bitmap.getConfig(); + + canvas.initializeTextureSize(this, format, type); + canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type); + + if (mBorder > 0) { + // Left border + Bitmap line = getBorderLine(true, config, texHeight); + canvas.texSubImage2D(this, 0, 0, line, format, type); + + // Top border + line = getBorderLine(false, config, texWidth); + canvas.texSubImage2D(this, 0, 0, line, format, type); + } + + // Right border + if (mBorder + bWidth < texWidth) { + Bitmap line = getBorderLine(true, config, texHeight); + canvas.texSubImage2D(this, mBorder + bWidth, 0, line, format, type); + } + + // Bottom border + if (mBorder + bHeight < texHeight) { + Bitmap line = getBorderLine(false, config, texWidth); + canvas.texSubImage2D(this, 0, mBorder + bHeight, line, format, type); + } + } + } finally { + freeBitmap(); + } + // Update texture state. + setAssociatedCanvas(canvas); + mState = STATE_LOADED; + mContentValid = true; + } else { + mState = STATE_ERROR; + throw new RuntimeException("Texture load fail, no bitmap"); + } + } + + @Override + protected boolean onBind(GLCanvas canvas) { + updateContent(canvas); + return isContentValid(); + } + + @Override + protected int getTarget() { + return GL11.GL_TEXTURE_2D; + } + + public void setOpaque(boolean isOpaque) { + mOpaque = isOpaque; + } + + @Override + public boolean isOpaque() { + return mOpaque; + } + + @Override + public void recycle() { + super.recycle(); + if (mBitmap != null) freeBitmap(); + } +} diff --git a/WallpaperPicker/src/com/android/gallery3d/util/IntArray.java b/WallpaperPicker/src/com/android/gallery3d/util/IntArray.java new file mode 100644 index 000000000..2c4dc2c83 --- /dev/null +++ b/WallpaperPicker/src/com/android/gallery3d/util/IntArray.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +public class IntArray { + private static final int INIT_CAPACITY = 8; + + private int mData[] = new int[INIT_CAPACITY]; + private int mSize = 0; + + public void add(int value) { + if (mData.length == mSize) { + int temp[] = new int[mSize + mSize]; + System.arraycopy(mData, 0, temp, 0, mSize); + mData = temp; + } + mData[mSize++] = value; + } + + public int removeLast() { + mSize--; + return mData[mSize]; + } + + public int size() { + return mSize; + } + + // For testing only + public int[] toArray(int[] result) { + if (result == null || result.length < mSize) { + result = new int[mSize]; + } + System.arraycopy(mData, 0, result, 0, mSize); + return result; + } + + public int[] getInternalArray() { + return mData; + } + + public void clear() { + mSize = 0; + if (mData.length != INIT_CAPACITY) mData = new int[INIT_CAPACITY]; + } +} diff --git a/WallpaperPicker/src/com/android/launcher3/CheckableFrameLayout.java b/WallpaperPicker/src/com/android/launcher3/CheckableFrameLayout.java new file mode 100644 index 000000000..5b7d82425 --- /dev/null +++ b/WallpaperPicker/src/com/android/launcher3/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 com.android.launcher3; + +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/WallpaperPicker/src/com/android/launcher3/CropView.java b/WallpaperPicker/src/com/android/launcher3/CropView.java new file mode 100644 index 000000000..578b8eafd --- /dev/null +++ b/WallpaperPicker/src/com/android/launcher3/CropView.java @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.FloatMath; +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] = FloatMath.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] = FloatMath.ceil(adjustment[dim]); + } + + mInverseRotateMatrix.mapPoints(adjustment); + mCenterX += adjustment[0]; + mCenterY += adjustment[1]; + updateCenter(); + } + } + + mLastX = x; + mLastY = y; + return true; + } +} diff --git a/WallpaperPicker/src/com/android/launcher3/DrawableTileSource.java b/WallpaperPicker/src/com/android/launcher3/DrawableTileSource.java new file mode 100644 index 000000000..c1f2eff0f --- /dev/null +++ b/WallpaperPicker/src/com/android/launcher3/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 com.android.launcher3; + +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/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java b/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java new file mode 100644 index 000000000..60b253711 --- /dev/null +++ b/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +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.FrameLayout; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.TextView; + +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; + + private List mWallpapers; + + @SuppressWarnings("unchecked") + public LiveWallpaperListAdapter(Context context) { + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mPackageManager = context.getPackageManager(); + + List list = mPackageManager.queryIntentServices( + new Intent(WallpaperService.SERVICE_INTERFACE), + PackageManager.GET_META_DATA); + + mWallpapers = new ArrayList(); + + 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; + } + + WallpaperPickerActivity.setWallpaperItemPaddingToZero((FrameLayout) view); + + 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 { + private Drawable mThumbnail; + private 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.onLiveWallpaperPickerLaunch(); + a.startActivityForResultSafely(preview, WallpaperPickerActivity.PICK_LIVE_WALLPAPER); + } + } + + private class LiveWallpaperEnumerator extends + AsyncTask, LiveWallpaperTile, Void> { + private Context mContext; + private int mWallpaperPosition; + + public LiveWallpaperEnumerator(Context context) { + super(); + mContext = context; + mWallpaperPosition = 0; + } + + @Override + protected Void doInBackground(List... params) { + final PackageManager packageManager = mContext.getPackageManager(); + + List list = params[0]; + + Collections.sort(list, new Comparator() { + 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/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java b/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java new file mode 100644 index 000000000..58add7022 --- /dev/null +++ b/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import android.app.Activity; +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 com.android.photos.BitmapRegionTileSource; + +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 = "Launcher3.SavedWallpaperImages"; + private ImageDb mDb; + ArrayList mImages; + Context mContext; + LayoutInflater mLayoutInflater; + + public static class SavedWallpaperTile extends WallpaperPickerActivity.WallpaperTileInfo { + private int mDbId; + private Drawable mThumb; + public SavedWallpaperTile(int dbId, Drawable thumb) { + mDbId = dbId; + mThumb = thumb; + } + @Override + public void onClick(WallpaperPickerActivity a) { + String imageFilename = a.getSavedImages().getImageFilename(mDbId); + File file = new File(a.getFilesDir(), imageFilename); + BitmapRegionTileSource.FilePathBitmapSource bitmapSource = + new BitmapRegionTileSource.FilePathBitmapSource(file.getAbsolutePath(), 1024); + a.setCropViewTileSource(bitmapSource, false, true, null); + } + @Override + public void onSave(WallpaperPickerActivity a) { + boolean finishActivityWhenDone = true; + String imageFilename = a.getSavedImages().getImageFilename(mDbId); + a.setWallpaper(imageFilename, finishActivityWhenDone); + } + @Override + public void onDelete(WallpaperPickerActivity a) { + a.getSavedImages().deleteImage(mDbId); + } + @Override + public boolean isSelectable() { + return true; + } + @Override + public boolean isNamelessWallpaper() { + return true; + } + } + + public SavedWallpaperImages(Activity context) { + mDb = new ImageDb(context); + mContext = context; + mLayoutInflater = context.getLayoutInflater(); + } + + public void loadThumbnailsAndImageIdList() { + mImages = new ArrayList(); + SQLiteDatabase db = mDb.getReadableDatabase(); + Cursor result = db.query(ImageDb.TABLE_NAME, + new String[] { ImageDb.COLUMN_ID, + ImageDb.COLUMN_IMAGE_THUMBNAIL_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 BitmapDrawable(thumb))); + } + } + 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, position, convertView, parent, thumbDrawable); + } + + public String getImageFilename(int id) { + Pair filenames = getImageFilenames(id); + if (filenames != null) { + return filenames.second; + } + return null; + } + + private Pair 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(thumbFilename, imageFilename); + } else { + return null; + } + } + + public void deleteImage(int id) { + Pair 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 DB_NAME = "saved_wallpaper_images.db"; + 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, new File(context.getCacheDir(), DB_NAME).getPath(), null, DB_VERSION); + // Store the context for later use + mContext = context; + } + + @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/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java b/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java new file mode 100644 index 000000000..7a4d48ca9 --- /dev/null +++ b/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +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.FrameLayout; +import android.widget.ListAdapter; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +public class ThirdPartyWallpaperPickerListAdapter extends BaseAdapter implements ListAdapter { + private static final String LOG_TAG = "LiveWallpaperListAdapter"; + + private final LayoutInflater mInflater; + private final PackageManager mPackageManager; + private final int mIconSize; + + private List mThirdPartyWallpaperPickers = + new ArrayList(); + + public static class ThirdPartyWallpaperTile extends WallpaperPickerActivity.WallpaperTileInfo { + private 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) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + 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 apps = + pm.queryIntentActivities(pickWallpaperIntent, 0); + + // Get list of image picker intents + Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT); + pickImageIntent.setType("image/*"); + final List 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; + } + + WallpaperPickerActivity.setWallpaperItemPaddingToZero((FrameLayout) view); + + 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/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java new file mode 100644 index 000000000..b3ef07309 --- /dev/null +++ b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java @@ -0,0 +1,836 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +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.res.Configuration; +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.os.Bundle; +import android.util.Log; +import android.view.Display; +import android.view.View; +import android.view.WindowManager; +import android.widget.Toast; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.exif.ExifInterface; +import com.android.photos.BitmapRegionTileSource; +import com.android.photos.BitmapRegionTileSource.BitmapSource; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class WallpaperCropActivity extends Activity { + private static final String LOGTAG = "Launcher3.CropActivity"; + + protected static final String WALLPAPER_WIDTH_KEY = "wallpaper.width"; + protected static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height"; + private static final int DEFAULT_COMPRESS_QUALITY = 90; + /** + * The maximum bitmap size we allow to be returned through the intent. + * Intents have a maximum of 1MB in total size. However, the Bitmap seems to + * have some overhead to hit so that we go way below the limit here to make + * sure the intent stays below 1MB.We should consider just returning a byte + * array instead of a Bitmap instance to avoid overhead. + */ + public static final int MAX_BMAP_IN_INTENT = 750000; + private static final float WALLPAPER_SCREENS_SPAN = 2f; + + protected static Point sDefaultWallpaperSize; + + protected CropView mCropView; + protected Uri mUri; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + init(); + if (!enableRotation()) { + setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT); + } + } + + protected void init() { + setContentView(R.layout.wallpaper_cropper); + + mCropView = (CropView) findViewById(R.id.cropView); + + 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, finishActivityWhenDone); + } + }); + + // Load image in background + final BitmapRegionTileSource.UriBitmapSource bitmapSource = + new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024); + Runnable onLoad = new Runnable() { + public void run() { + if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) { + Toast.makeText(WallpaperCropActivity.this, + getString(R.string.wallpaper_load_fail), + Toast.LENGTH_LONG).show(); + finish(); + } + } + }; + setCropViewTileSource(bitmapSource, true, false, onLoad); + } + + public void setCropViewTileSource( + final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled, + final boolean moveToLeft, final Runnable postExecute) { + final Context context = WallpaperCropActivity.this; + final View progressView = findViewById(R.id.loading); + final AsyncTask loadBitmapTask = new AsyncTask() { + protected Void doInBackground(Void...args) { + if (!isCancelled()) { + bitmapSource.loadInBackground(); + } + return null; + } + protected void onPostExecute(Void arg) { + if (!isCancelled()) { + progressView.setVisibility(View.INVISIBLE); + if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) { + mCropView.setTileSource( + new BitmapRegionTileSource(context, bitmapSource), null); + mCropView.setTouchEnabled(touchEnabled); + if (moveToLeft) { + mCropView.moveToLeft(); + } + } + } + if (postExecute != null) { + postExecute.run(); + } + } + }; + // 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) + progressView.postDelayed(new Runnable() { + public void run() { + if (loadBitmapTask.getStatus() != AsyncTask.Status.FINISHED) { + progressView.setVisibility(View.VISIBLE); + } + } + }, 1000); + loadBitmapTask.execute(); + } + + public boolean enableRotation() { + return getResources().getBoolean(R.bool.allow_rotation); + } + + public static String getSharedPreferencesKey() { + return WallpaperCropActivity.class.getName(); + } + + // As a ratio of screen height, the total distance we want the parallax effect to span + // horizontally + private 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; + } + + static protected 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 (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_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 (isScreenLarge(res)) { + 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; + } + + public static int getRotationFromExif(String path) { + return getRotationFromExifHelper(path, null, 0, null, null); + } + + public static int getRotationFromExif(Context context, Uri uri) { + return getRotationFromExifHelper(null, null, 0, context, uri); + } + + public static int getRotationFromExif(Resources res, int resId) { + return getRotationFromExifHelper(null, res, resId, null, null); + } + + private static int getRotationFromExifHelper( + String path, Resources res, int resId, Context context, Uri uri) { + ExifInterface ei = new ExifInterface(); + InputStream is = null; + BufferedInputStream bis = null; + try { + if (path != null) { + ei.readExif(path); + } else 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(LOGTAG, "Getting exif data failed", e); + } finally { + Utils.closeSilently(bis); + Utils.closeSilently(is); + } + return 0; + } + + protected void setWallpaper(String filePath, final boolean finishActivityWhenDone) { + int rotation = getRotationFromExif(filePath); + BitmapCropTask cropTask = new BitmapCropTask( + this, filePath, null, rotation, 0, 0, true, 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, final boolean finishActivityWhenDone) { + // crop this image and scale it down to the default wallpaper size for + // this device + int rotation = getRotationFromExif(res, resId); + Point inSize = mCropView.getSourceDimensions(); + Point outSize = getDefaultWallpaperSize(getResources(), + getWindowManager()); + RectF crop = 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(this, res, resId, + crop, rotation, outSize.x, outSize.y, true, false, onEndCrop); + cropTask.execute(); + } + + private static boolean isScreenLarge(Resources res) { + Configuration config = res.getConfiguration(); + return config.smallestScreenWidthDp >= 720; + } + + protected void cropImageAndSetWallpaper(Uri uri, + OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) { + // 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 = getDefaultWallpaperSize(getResources(), + getWindowManager()); + // Get the crop + RectF cropRect = mCropView.getCrop(); + int cropRotation = mCropView.getImageRotation(); + float cropScale = mCropView.getWidth() / (float) cropRect.width(); + + Point inSize = mCropView.getSourceDimensions(); + 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]); + + // 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 = 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 (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(this, uri, + cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop); + if (onBitmapCroppedHandler != null) { + cropTask.setOnBitmapCropped(onBitmapCroppedHandler); + } + cropTask.execute(); + } + + public interface OnBitmapCroppedHandler { + public void onBitmapCropped(byte[] imageBytes); + } + + protected static class BitmapCropTask extends AsyncTask { + Uri mInUri = null; + Context mContext; + String mInFilePath; + byte[] mInImageBytes; + int mInResId = 0; + RectF mCropBounds = null; + int mOutWidth, mOutHeight; + int mRotation; + String mOutputFormat = "jpg"; // for now + boolean mSetWallpaper; + boolean mSaveCroppedBitmap; + Bitmap mCroppedBitmap; + Runnable mOnEndRunnable; + Resources mResources; + OnBitmapCroppedHandler mOnBitmapCroppedHandler; + boolean mNoCrop; + + public BitmapCropTask(Context c, String filePath, + RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mContext = c; + mInFilePath = filePath; + init(cropBounds, rotation, + outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); + } + + public BitmapCropTask(byte[] imageBytes, + RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mInImageBytes = imageBytes; + init(cropBounds, rotation, + outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); + } + + public BitmapCropTask(Context c, Uri inUri, + RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mContext = c; + mInUri = inUri; + init(cropBounds, rotation, + outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); + } + + public BitmapCropTask(Context c, Resources res, int inResId, + RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mContext = c; + mInResId = inResId; + mResources = res; + init(cropBounds, rotation, + outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable); + } + + private void init(RectF cropBounds, int rotation, int outWidth, int outHeight, + boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) { + mCropBounds = cropBounds; + mRotation = rotation; + mOutWidth = outWidth; + mOutHeight = outHeight; + mSetWallpaper = setWallpaper; + mSaveCroppedBitmap = saveCroppedBitmap; + mOnEndRunnable = onEndRunnable; + } + + public void setOnBitmapCropped(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) { + Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " + + "image byte array given"); + } else { + try { + 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 (FileNotFoundException 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) { + try { + InputStream is = regenerateInputStream(); + if (is != null) { + wallpaperManager.setStream(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; + } + + // Get output compression format + CompressFormat cf = + convertExtensionToCompressFormat(getFileExtension(mOutputFormat)); + + // Compress to byte array + ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); + if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) { + // If we need to set to the wallpaper, set it + if (mSetWallpaper && wallpaperManager != null) { + try { + byte[] outByteArray = tmpOut.toByteArray(); + wallpaperManager.setStream(new ByteArrayInputStream(outByteArray)); + 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(); + } + } + } + + protected void updateWallpaperDimensions(int width, int height) { + String spKey = getSharedPreferencesKey(); + SharedPreferences sp = 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(); + + suggestWallpaperDimension(getResources(), + sp, getWindowManager(), WallpaperManager.getInstance(this)); + } + + static public void suggestWallpaperDimension(Resources res, + final SharedPreferences sharedPrefs, + WindowManager windowManager, + final WallpaperManager wallpaperManager) { + final Point defaultWallpaperSize = getDefaultWallpaperSize(res, windowManager); + // If we have saved a wallpaper width/height, use that instead + int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, defaultWallpaperSize.x); + int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, defaultWallpaperSize.y); + if (savedWidth != wallpaperManager.getDesiredMinimumWidth() || + savedHeight != wallpaperManager.getDesiredMinimumHeight()) { + wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight); + } + } + + protected 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; + } + + protected static CompressFormat convertExtensionToCompressFormat(String extension) { + return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG; + } + + protected static String getFileExtension(String requestFormat) { + String outputFormat = (requestFormat == null) + ? "jpg" + : requestFormat; + outputFormat = outputFormat.toLowerCase(); + return (outputFormat.equals("png") || outputFormat.equals("gif")) + ? "png" // We don't support gif compression. + : "jpg"; + } +} diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java new file mode 100644 index 000000000..465769066 --- /dev/null +++ b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java @@ -0,0 +1,1044 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import android.animation.Animator; +import android.animation.LayoutTransition; +import android.app.ActionBar; +import android.app.Activity; +import android.app.WallpaperInfo; +import android.app.WallpaperManager; +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.Paint; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LevelListDrawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +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.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.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +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.photos.BitmapRegionTileSource; +import com.android.launcher3.settings.SettingsProvider; +import com.android.photos.BitmapRegionTileSource.BitmapSource; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; + +public class WallpaperPickerActivity extends WallpaperCropActivity { + static final String TAG = "Launcher.WallpaperPickerActivity"; + + public static final int IMAGE_PICK = 5; + public static final int PICK_WALLPAPER_THIRD_PARTY_ACTIVITY = 6; + public static final int PICK_LIVE_WALLPAPER = 7; + private static final String TEMP_WALLPAPER_TILES = "TEMP_WALLPAPER_TILES"; + private static final String OLD_DEFAULT_WALLPAPER_THUMBNAIL_FILENAME = "default_thumb.jpg"; + private static final String DEFAULT_WALLPAPER_THUMBNAIL_FILENAME = "default_thumb2.jpg"; + + private static final int MENU_WALLPAPER_SCROLL = 0; + + private View mSelectedTile; + + private boolean mIgnoreNextTap; + private OnClickListener mThumbnailOnClickListener; + + private LinearLayout mWallpapersView; + private View mWallpaperStrip; + + private ActionMode.Callback mActionModeCallback; + private ActionMode mActionMode; + + private View.OnLongClickListener mLongClickListener; + + ArrayList mTempWallpaperTiles = new ArrayList(); + private SavedWallpaperImages mSavedImages; + private WallpaperInfo mLiveWallpaperInfoOnPickerLaunch; + + public static abstract class WallpaperTileInfo { + protected View mView; + 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; + private boolean mFirstClick = true; + private BitmapRegionTileSource.UriBitmapSource mBitmapSource; + public UriWallpaperInfo(Uri uri) { + mUri = uri; + } + @Override + public void onClick(final WallpaperPickerActivity a) { + final Runnable onLoad; + if (!mFirstClick) { + onLoad = null; + } else { + mFirstClick = false; + onLoad = new Runnable() { + public void run() { + if (mBitmapSource != null && + mBitmapSource.getLoadingState() == BitmapSource.State.LOADED) { + mView.setVisibility(View.VISIBLE); + a.selectTile(mView); + } else { + ViewGroup parent = (ViewGroup) mView.getParent(); + if (parent != null) { + parent.removeView(mView); + Toast.makeText(a, + a.getString(R.string.image_load_fail), + Toast.LENGTH_SHORT).show(); + } + } + } + }; + } + mBitmapSource = new BitmapRegionTileSource.UriBitmapSource( + a, mUri, BitmapRegionTileSource.MAX_PREVIEW_SIZE); + a.setCropViewTileSource(mBitmapSource, true, false, onLoad); + } + @Override + public void onSave(final WallpaperPickerActivity a) { + boolean finishActivityWhenDone = true; + OnBitmapCroppedHandler h = new 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, finishActivityWhenDone); + } + @Override + public boolean isSelectable() { + return true; + } + @Override + public boolean isNamelessWallpaper() { + return true; + } + } + + public static class ResourceWallpaperInfo extends WallpaperTileInfo { + private Resources mResources; + private int mResId; + private Drawable mThumb; + + public ResourceWallpaperInfo(Resources res, int resId, Drawable thumb) { + mResources = res; + mResId = resId; + mThumb = thumb; + } + @Override + public void onClick(WallpaperPickerActivity a) { + BitmapRegionTileSource.ResourceBitmapSource bitmapSource = + new BitmapRegionTileSource.ResourceBitmapSource( + mResources, mResId, BitmapRegionTileSource.MAX_PREVIEW_SIZE); + bitmapSource.loadInBackground(); + BitmapRegionTileSource source = new BitmapRegionTileSource(a, bitmapSource); + CropView v = a.getCropView(); + v.setTileSource(source, null); + Point wallpaperSize = WallpaperCropActivity.getDefaultWallpaperSize( + a.getResources(), a.getWindowManager()); + RectF crop = WallpaperCropActivity.getMaxCropRect( + source.getImageWidth(), source.getImageHeight(), + wallpaperSize.x, wallpaperSize.y, false); + v.setScale(wallpaperSize.x / crop.width()); + v.setTouchEnabled(false); + } + @Override + public void onSave(WallpaperPickerActivity a) { + boolean finishActivityWhenDone = true; + a.cropImageAndSetWallpaper(mResources, mResId, finishActivityWhenDone); + } + @Override + public boolean isSelectable() { + return true; + } + @Override + public boolean isNamelessWallpaper() { + return true; + } + } + + public static class DefaultWallpaperInfo extends WallpaperTileInfo { + public Drawable mThumb; + public DefaultWallpaperInfo(Drawable thumb) { + mThumb = thumb; + } + @Override + public void onClick(WallpaperPickerActivity a) { + CropView c = a.getCropView(); + + Drawable defaultWallpaper = WallpaperManager.getInstance(a).getBuiltInDrawable( + c.getWidth(), c.getHeight(), false, 0.5f, 0.5f); + + c.setTileSource( + new DrawableTileSource(a, defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE), null); + c.setScale(1f); + c.setTouchEnabled(false); + } + @Override + public void onSave(WallpaperPickerActivity a) { + try { + WallpaperManager.getInstance(a).clear(); + } 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; + } + } + + public void setWallpaperStripYOffset(float offset) { + mWallpaperStrip.setPadding(0, 0, 0, (int) offset); + } + + // called by onCreate; this is subclassed to overwrite WallpaperCropActivity + protected void init() { + setContentView(R.layout.wallpaper_picker); + + mCropView = (CropView) findViewById(R.id.cropView); + 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(); + } + } + }); + + 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; + } + 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; + } + }; + + // Populate the built-in wallpapers + ArrayList wallpapers = findBundledWallpapers(); + mWallpapersView = (LinearLayout) findViewById(R.id.wallpaper_list); + BuiltInWallpapersAdapter ia = new BuiltInWallpapersAdapter(this, wallpapers); + populateWallpapersFromAdapter(mWallpapersView, ia, false); + + // Populate the saved wallpapers + mSavedImages = new SavedWallpaperImages(this); + mSavedImages.loadThumbnailsAndImageIdList(); + populateWallpapersFromAdapter(mWallpapersView, mSavedImages, true); + + // Populate the live wallpapers + final LinearLayout liveWallpapersView = + (LinearLayout) findViewById(R.id.live_wallpaper_list); + final LiveWallpaperListAdapter a = new LiveWallpaperListAdapter(this); + 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(this); + 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); + setWallpaperItemPaddingToZero(pickImageTile); + 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(getThumbnailOfLastPhoto()); + 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); + + // Add a tile for the default wallpaper + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + DefaultWallpaperInfo defaultWallpaperInfo = getDefaultWallpaper(); + FrameLayout defaultWallpaperTile = (FrameLayout) createImageTileView( + getLayoutInflater(), 0, null, mWallpapersView, defaultWallpaperInfo.mThumb); + setWallpaperItemPaddingToZero(defaultWallpaperTile); + defaultWallpaperTile.setTag(defaultWallpaperInfo); + mWallpapersView.addView(defaultWallpaperTile, 0); + defaultWallpaperTile.setOnClickListener(mThumbnailOnClickListener); + defaultWallpaperInfo.setView(defaultWallpaperTile); + } + + // 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 (mWallpapersView.getChildCount() > 0) { + mThumbnailOnClickListener.onClick(mWallpapersView.getChildAt(0)); + } + 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) { + if (mSelectedTile != null) { + WallpaperTileInfo info = (WallpaperTileInfo) mSelectedTile.getTag(); + info.onSave(WallpaperPickerActivity.this); + } + } + }); + + // 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 viewsToRemove = new ArrayList(); + 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); + } + } + for (View v : viewsToRemove) { + mWallpapersView.removeView(v); + } + 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); + } + mSelectedTile.setSelected(true); + mActionMode = null; + } + }; + } + + private void selectTile(View v) { + if (mSelectedTile != null) { + mSelectedTile.setSelected(false); + mSelectedTile = null; + } + mSelectedTile = v; + v.setSelected(true); + // TODO: Remove this once the accessibility framework and + // services have better support for selection state. + v.announceForAccessibility( + getString(R.string.announce_selection, v.getContentDescription())); + } + + private void initializeScrollForRtl() { + final HorizontalScrollView scroll = + (HorizontalScrollView) findViewById(R.id.wallpaper_scroll_container); + + if (scroll.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + final ViewTreeObserver observer = scroll.getViewTreeObserver(); + observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { + public void onGlobalLayout() { + LinearLayout masterWallpaperList = + (LinearLayout) findViewById(R.id.master_wallpaper_list); + scroll.scrollTo(masterWallpaperList.getWidth(), 0); + scroll.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } + }); + } + } + + protected Bitmap getThumbnailOfLastPhoto() { + Cursor cursor = MediaStore.Images.Media.query(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.moveToFirst()) { + int id = cursor.getInt(0); + thumb = MediaStore.Images.Thumbnails.getThumbnail(getContentResolver(), + id, MediaStore.Images.Thumbnails.MINI_KIND, null); + } + cursor.close(); + } + return thumb; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, MENU_WALLPAPER_SCROLL, 0, + R.string.wallpaper_scroll).setCheckable(true); + + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem wallpaperScroll = menu.findItem(MENU_WALLPAPER_SCROLL); + + wallpaperScroll.setChecked(SettingsProvider.getBoolean(this, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SCROLLING_WALLPAPER_SCROLL, + R.bool.preferences_interface_homescreen_scrolling_wallpaper_scroll_default)); + + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle presses on the action bar items + switch (item.getItemId()) { + case MENU_WALLPAPER_SCROLL: + SettingsProvider.get(this).edit() + .putBoolean(SettingsProvider.SETTINGS_UI_HOMESCREEN_SCROLLING_WALLPAPER_SCROLL, !item.isChecked()) + .commit(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + protected void onStop() { + super.onStop(); + mWallpaperStrip = findViewById(R.id.wallpaper_strip); + if (mWallpaperStrip.getAlpha() < 1f) { + mWallpaperStrip.setAlpha(1f); + mWallpaperStrip.setVisibility(View.VISIBLE); + } + mCropView.destroy(); + } + + protected void onSaveInstanceState(Bundle outState) { + outState.putParcelableArrayList(TEMP_WALLPAPER_TILES, mTempWallpaperTiles); + } + + protected void onRestoreInstanceState(Bundle savedInstanceState) { + ArrayList uris = savedInstanceState.getParcelableArrayList(TEMP_WALLPAPER_TILES); + for (Uri uri : uris) { + addTemporaryWallpaperTile(uri); + } + } + + private 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); + } + } + + private 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); + } + } + } + } + } + } + + private static Point getDefaultThumbnailSize(Resources res) { + return new Point(res.getDimensionPixelSize(R.dimen.wallpaperThumbnailWidth), + res.getDimensionPixelSize(R.dimen.wallpaperThumbnailHeight)); + + } + + private 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, true, null); + } else if (imageBytes != null) { + cropTask = new BitmapCropTask( + imageBytes, null, rotation, width, height, false, true, null); + } else { + cropTask = new BitmapCropTask( + context, res, resId, null, rotation, width, height, 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 = WallpaperCropActivity.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) { + 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); + setWallpaperItemPaddingToZero(pickedImageThumbnail); + 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 = this; + new AsyncTask() { + protected Bitmap doInBackground(Void...args) { + int rotation = WallpaperCropActivity.getRotationFromExif(context, uri); + return createThumbnail(defaultSize, context, uri, null, null, 0, rotation, false); + + } + protected void onPostExecute(Bitmap thumb) { + if (thumb != null) { + image.setImageBitmap(thumb); + Drawable thumbDrawable = image.getDrawable(); + thumbDrawable.setDither(true); + } else { + Log.e(TAG, "Error loading thumbnail for uri=" + uri); + } + } + }.execute(); + + UriWallpaperInfo info = new UriWallpaperInfo(uri); + pickedImageThumbnail.setTag(info); + info.setView(pickedImageThumbnail); + addLongPressHandler(pickedImageThumbnail); + updateTileIndices(); + pickedImageThumbnail.setOnClickListener(mThumbnailOnClickListener); + mThumbnailOnClickListener.onClick(pickedImageThumbnail); + } + + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == IMAGE_PICK && resultCode == RESULT_OK) { + if (data != null && data.getData() != null) { + Uri uri = data.getData(); + addTemporaryWallpaperTile(uri); + } + } else if (requestCode == PICK_WALLPAPER_THIRD_PARTY_ACTIVITY) { + setResult(RESULT_OK); + finish(); + } else if (requestCode == PICK_LIVE_WALLPAPER) { + WallpaperManager wm = WallpaperManager.getInstance(this); + final WallpaperInfo oldLiveWallpaper = mLiveWallpaperInfoOnPickerLaunch; + WallpaperInfo newLiveWallpaper = wm.getWallpaperInfo(); + // Try to figure out if a live wallpaper was set; + if (newLiveWallpaper != null && + (oldLiveWallpaper == null || + !oldLiveWallpaper.getComponent().equals(newLiveWallpaper.getComponent()))) { + // Return if a live wallpaper was set + setResult(RESULT_OK); + finish(); + } + } + } + + static void setWallpaperItemPaddingToZero(FrameLayout frameLayout) { + frameLayout.setPadding(0, 0, 0, 0); + frameLayout.setForeground(new ZeroPaddingDrawable(frameLayout.getForeground())); + } + + private void addLongPressHandler(View v) { + v.setOnLongClickListener(mLongClickListener); + } + + private ArrayList findBundledWallpapers() { + ArrayList bundledWallpapers = + new ArrayList(24); + + Pair r = getWallpaperArrayResourceId(); + if (r != null) { + try { + Resources wallpaperRes = getPackageManager().getResourcesForApplication(r.first); + bundledWallpapers = addWallpapers(wallpaperRes, r.first.packageName, r.second); + } catch (PackageManager.NameNotFoundException e) { + } + } + + // Add an entry for the default wallpaper (stored in system resources) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + ResourceWallpaperInfo defaultWallpaperInfo = getPreKKDefaultWallpaperInfo(); + if (defaultWallpaperInfo != null) { + bundledWallpapers.add(0, defaultWallpaperInfo); + } + } + return bundledWallpapers; + } + + private boolean writeImageToFileAsJpeg(File f, Bitmap b) { + try { + f.createNewFile(); + FileOutputStream thumbFileStream = + 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 ResourceWallpaperInfo getPreKKDefaultWallpaperInfo() { + Resources sysRes = Resources.getSystem(); + int resId = sysRes.getIdentifier("default_wallpaper", "drawable", "android"); + + File defaultThumbFile = new File(getFilesDir(), DEFAULT_WALLPAPER_THUMBNAIL_FILENAME); + 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 = WallpaperCropActivity.getRotationFromExif(res, resId); + thumb = createThumbnail( + defaultThumbSize, this, null, null, sysRes, resId, rotation, false); + if (thumb != null) { + defaultWallpaperExists = writeImageToFileAsJpeg(defaultThumbFile, thumb); + } + } + if (defaultWallpaperExists) { + return new ResourceWallpaperInfo(sysRes, resId, new BitmapDrawable(thumb)); + } + return null; + } + + private DefaultWallpaperInfo getDefaultWallpaper() { + File defaultThumbFile = new File(getFilesDir(), DEFAULT_WALLPAPER_THUMBNAIL_FILENAME); + Bitmap thumb = null; + boolean defaultWallpaperExists = false; + if (defaultThumbFile.exists()) { + thumb = BitmapFactory.decodeFile(defaultThumbFile.getAbsolutePath()); + defaultWallpaperExists = true; + } else { + // Delete old thumbnail file, since we had a bug where the thumbnail wasn't being drawn + // before + new File(getFilesDir(), OLD_DEFAULT_WALLPAPER_THUMBNAIL_FILENAME).delete(); + + Resources res = getResources(); + Point defaultThumbSize = getDefaultThumbnailSize(res); + Drawable wallpaperDrawable = WallpaperManager.getInstance(this).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 = writeImageToFileAsJpeg(defaultThumbFile, thumb); + } + } + if (defaultWallpaperExists) { + return new DefaultWallpaperInfo(new BitmapDrawable(thumb)); + } + return null; + } + + public Pair getWallpaperArrayResourceId() { + // Context.getPackageName() may return the "original" package name, + // com.android.launcher3; Resources needs the real package name, + // com.android.launcher3. So we ask Resources for what it thinks the + // package name should be. + final String packageName = getResources().getResourcePackageName(R.array.wallpapers); + try { + ApplicationInfo info = getPackageManager().getApplicationInfo(packageName, 0); + return new Pair(info, R.array.wallpapers); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + } + + private ArrayList addWallpapers( + Resources res, String packageName, int listResId) { + ArrayList bundledWallpapers = + new ArrayList(24); + 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)); + bundledWallpapers.add(wallpaperInfo); + // Log.d(TAG, "add: [" + packageName + "]: " + extra + " (" + res + ")"); + } + } else { + Log.e(TAG, "Couldn't find wallpaper " + extra); + } + } + return bundledWallpapers; + } + + public CropView getCropView() { + return mCropView; + } + + public SavedWallpaperImages getSavedImages() { + return mSavedImages; + } + + public void onLiveWallpaperPickerLaunch() { + mLiveWallpaperInfoOnPickerLaunch = WallpaperManager.getInstance(this).getWallpaperInfo(); + } + + static class ZeroPaddingDrawable extends LevelListDrawable { + public ZeroPaddingDrawable(Drawable d) { + super(); + addLevel(0, 0, d); + setLevel(0); + } + + @Override + public boolean getPadding(Rect padding) { + padding.set(0, 0, 0, 0); + return true; + } + } + + private static class BuiltInWallpapersAdapter extends BaseAdapter implements ListAdapter { + private LayoutInflater mLayoutInflater; + private ArrayList mWallpapers; + + BuiltInWallpapersAdapter(Activity activity, ArrayList wallpapers) { + mLayoutInflater = activity.getLayoutInflater(); + mWallpapers = wallpapers; + } + + public int getCount() { + return mWallpapers.size(); + } + + public ResourceWallpaperInfo 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, position, convertView, parent, thumb); + } + } + + public static View createImageTileView(LayoutInflater layoutInflater, int position, + View convertView, ViewGroup parent, Drawable thumb) { + View view; + + if (convertView == null) { + view = layoutInflater.inflate(R.layout.wallpaper_picker_item, parent, false); + } else { + view = convertView; + } + + setWallpaperItemPaddingToZero((FrameLayout) view); + + ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image); + + if (thumb != null) { + image.setImageDrawable(thumb); + thumb.setDither(true); + } + + return view; + } + + // In Launcher3, we override this with a method that catches exceptions + // from starting activities; didn't want to copy and paste code into here + public void startActivityForResultSafely(Intent intent, int requestCode) { + startActivityForResult(intent, requestCode); + } +} diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java b/WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java new file mode 100644 index 000000000..ceaa043a7 --- /dev/null +++ b/WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.widget.RelativeLayout; + +public class WallpaperRootView extends RelativeLayout { + private final WallpaperPickerActivity a; + public WallpaperRootView(Context context, AttributeSet attrs) { + super(context, attrs); + a = (WallpaperPickerActivity) context; + } + public WallpaperRootView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + a = (WallpaperPickerActivity) context; + } + + protected boolean fitSystemWindows(Rect insets) { + a.setWallpaperStripYOffset(insets.bottom); + return true; + } +} diff --git a/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java b/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java new file mode 100644 index 000000000..8511de2da --- /dev/null +++ b/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java @@ -0,0 +1,524 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS 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.Resources; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.net.Uri; +import android.os.Build; +import android.os.Build.VERSION_CODES; +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.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 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 boolean REUSE_BITMAP = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + 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; + + public static abstract class BitmapSource { + private SimpleBitmapRegionDecoder mDecoder; + private Bitmap mPreview; + private int mPreviewSize; + private int mRotation; + public enum State { NOT_LOADED, LOADED, ERROR_LOADING }; + private State mState = State.NOT_LOADED; + public BitmapSource(int previewSize) { + mPreviewSize = previewSize; + } + public boolean loadInBackground() { + 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(); + if (mPreviewSize != 0) { + int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE); + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inPreferredConfig = Bitmap.Config.ARGB_8888; + opts.inPreferQualityOverSpeed = true; + + float scale = (float) previewSize / Math.max(width, height); + opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); + opts.inJustDecodeBounds = false; + mPreview = loadPreviewBitmap(opts); + } + mState = State.LOADED; + return true; + } + } + + public State getLoadingState() { + return mState; + } + + public SimpleBitmapRegionDecoder getBitmapRegionDecoder() { + return mDecoder; + } + + public Bitmap getPreviewBitmap() { + return mPreview; + } + + public int getPreviewSize() { + return mPreviewSize; + } + + public int getRotation() { + return mRotation; + } + + public abstract boolean readExif(ExifInterface ei); + public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder(); + public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options); + } + + public static class FilePathBitmapSource extends BitmapSource { + private String mPath; + public FilePathBitmapSource(String path, int previewSize) { + super(previewSize); + 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 (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, int previewSize) { + super(previewSize); + 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; + } catch (IOException e) { + Log.e("BitmapRegionTileSource", "Failure while reading 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.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); + return false; + } catch (IOException e) { + Log.e("BitmapRegionTileSource", "Failed to load 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, int previewSize) { + super(previewSize); + 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; + } + } + } + + 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 Rect mOverlapRegion = new Rect(); + private BitmapFactory.Options mOptions; + private Canvas mCanvas; + + public BitmapRegionTileSource(Context context, BitmapSource source) { + 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 = new byte[16 * 1024]; + int previewSize = source.getPreviewSize(); + if (previewSize != 0) { + previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE); + // Although this is the same size as the Bitmap that is likely already + // loaded, the lifecycle is different and interactions are on a different + // thread. Thus to simplify, this source will decode its own bitmap. + Bitmap preview = decodePreview(source, previewSize); + if (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())); + } + } + } + } + + @Override + public int getTileSize() { + return mTileSize; + } + + @Override + public int getImageWidth() { + return mWidth; + } + + @Override + public int getImageHeight() { + return mHeight; + } + + @Override + public BasicTexture getPreview() { + return mPreview; + } + + @Override + public int getRotation() { + return mRotation; + } + + @Override + public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { + int tileSize = getTileSize(); + if (!REUSE_BITMAP) { + return getTileWithoutReusingBitmap(level, x, y, tileSize); + } + + int t = tileSize << level; + mWantRegion.set(x, y, x + t, y + t); + + if (bitmap == null) { + bitmap = Bitmap.createBitmap(tileSize, tileSize, Bitmap.Config.ARGB_8888); + } + + mOptions.inSampleSize = (1 << level); + mOptions.inBitmap = bitmap; + + try { + bitmap = mDecoder.decodeRegion(mWantRegion, mOptions); + } finally { + if (mOptions.inBitmap != bitmap && mOptions.inBitmap != null) { + mOptions.inBitmap = null; + } + } + + if (bitmap == null) { + Log.w("BitmapRegionTileSource", "fail in decoding region"); + } + return bitmap; + } + + private Bitmap getTileWithoutReusingBitmap( + int level, int x, int y, int tileSize) { + + int t = tileSize << level; + mWantRegion.set(x, y, x + t, y + t); + + mOverlapRegion.set(0, 0, mWidth, mHeight); + + mOptions.inSampleSize = (1 << level); + Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions); + + if (bitmap == null) { + Log.w(TAG, "fail in decoding region"); + } + + if (mWantRegion.equals(mOverlapRegion)) { + return bitmap; + } + + Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888); + if (mCanvas == null) { + mCanvas = new Canvas(); + } + mCanvas.setBitmap(result); + mCanvas.drawBitmap(bitmap, + (mOverlapRegion.left - mWantRegion.left) >> level, + (mOverlapRegion.top - mWantRegion.top) >> level, null); + mCanvas.setBitmap(null); + return result; + } + + /** + * Note that the returned bitmap may have a long edge that's longer + * than the targetSize, but it will always be less than 2x the targetSize + */ + private Bitmap decodePreview(BitmapSource source, int targetSize) { + Bitmap result = source.getPreviewBitmap(); + if (result == null) { + return null; + } + + // We need to resize down if the decoder does not support inSampleSize + // or didn't support the specified inSampleSize (some decoders only do powers of 2) + float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight())); + + if (scale <= 0.5) { + result = BitmapUtils.resizeBitmapByScale(result, scale, true); + } + return ensureGLCompatibleBitmap(result); + } + + private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) { + if (bitmap == null || bitmap.getConfig() != null) { + return bitmap; + } + Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false); + bitmap.recycle(); + return newBitmap; + } +} diff --git a/WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java b/WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java new file mode 100644 index 000000000..8a0505185 --- /dev/null +++ b/WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java @@ -0,0 +1,438 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.photos.views; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.opengl.GLSurfaceView.Renderer; +import android.opengl.GLUtils; +import android.util.Log; +import android.view.TextureView; +import android.view.TextureView.SurfaceTextureListener; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL10; + +/** + * A TextureView that supports blocking rendering for synchronous drawing + */ +public class BlockingGLTextureView extends TextureView + implements SurfaceTextureListener { + + private RenderThread mRenderThread; + + public BlockingGLTextureView(Context context) { + super(context); + setSurfaceTextureListener(this); + } + + public void setRenderer(Renderer renderer) { + if (mRenderThread != null) { + throw new IllegalArgumentException("Renderer already set"); + } + mRenderThread = new RenderThread(renderer); + } + + public void render() { + mRenderThread.render(); + } + + public void destroy() { + if (mRenderThread != null) { + mRenderThread.finish(); + mRenderThread = null; + } + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, + int height) { + mRenderThread.setSurface(surface); + mRenderThread.setSize(width, height); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, + int height) { + mRenderThread.setSize(width, height); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + if (mRenderThread != null) { + mRenderThread.setSurface(null); + } + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + } + + @Override + protected void finalize() throws Throwable { + try { + destroy(); + } catch (Throwable t) { + // Ignore + } + super.finalize(); + } + + /** + * An EGL helper class. + */ + + private static class EglHelper { + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private static final int EGL_OPENGL_ES2_BIT = 4; + + EGL10 mEgl; + EGLDisplay mEglDisplay; + EGLSurface mEglSurface; + EGLConfig mEglConfig; + EGLContext mEglContext; + + private EGLConfig chooseEglConfig() { + int[] configsCount = new int[1]; + EGLConfig[] configs = new EGLConfig[1]; + int[] configSpec = getConfig(); + if (!mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount)) { + throw new IllegalArgumentException("eglChooseConfig failed " + + GLUtils.getEGLErrorString(mEgl.eglGetError())); + } else if (configsCount[0] > 0) { + return configs[0]; + } + return null; + } + + private static int[] getConfig() { + return new int[] { + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_RED_SIZE, 8, + EGL10.EGL_GREEN_SIZE, 8, + EGL10.EGL_BLUE_SIZE, 8, + EGL10.EGL_ALPHA_SIZE, 8, + EGL10.EGL_DEPTH_SIZE, 0, + EGL10.EGL_STENCIL_SIZE, 0, + EGL10.EGL_NONE + }; + } + + EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) { + int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; + return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attribList); + } + + /** + * Initialize EGL for a given configuration spec. + */ + public void start() { + /* + * Get an EGL instance + */ + mEgl = (EGL10) EGLContext.getEGL(); + + /* + * Get to the default display. + */ + mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + + /* + * We can now initialize EGL for that display + */ + int[] version = new int[2]; + if (!mEgl.eglInitialize(mEglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + mEglConfig = chooseEglConfig(); + + /* + * Create an EGL context. We want to do this as rarely as we can, because an + * EGL context is a somewhat heavy object. + */ + mEglContext = createContext(mEgl, mEglDisplay, mEglConfig); + + if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { + mEglContext = null; + throwEglException("createContext"); + } + + mEglSurface = null; + } + + /** + * Create an egl surface for the current SurfaceTexture surface. If a surface + * already exists, destroy it before creating the new surface. + * + * @return true if the surface was created successfully. + */ + public boolean createSurface(SurfaceTexture surface) { + /* + * Check preconditions. + */ + if (mEgl == null) { + throw new RuntimeException("egl not initialized"); + } + if (mEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (mEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + + /* + * The window size has changed, so we need to create a new + * surface. + */ + destroySurfaceImp(); + + /* + * Create an EGL surface we can render into. + */ + if (surface != null) { + mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, null); + } else { + mEglSurface = null; + } + + if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { + int error = mEgl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + } + return false; + } + + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + /* + * Could not make the context current, probably because the underlying + * SurfaceView surface has been destroyed. + */ + logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError()); + return false; + } + + return true; + } + + /** + * Create a GL object for the current EGL context. + */ + public GL10 createGL() { + return (GL10) mEglContext.getGL(); + } + + /** + * Display the current render surface. + * @return the EGL error code from eglSwapBuffers. + */ + public int swap() { + if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { + return mEgl.eglGetError(); + } + return EGL10.EGL_SUCCESS; + } + + public void destroySurface() { + destroySurfaceImp(); + } + + private void destroySurfaceImp() { + if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { + mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT); + mEgl.eglDestroySurface(mEglDisplay, mEglSurface); + mEglSurface = null; + } + } + + public void finish() { + if (mEglContext != null) { + mEgl.eglDestroyContext(mEglDisplay, mEglContext); + mEglContext = null; + } + if (mEglDisplay != null) { + mEgl.eglTerminate(mEglDisplay); + mEglDisplay = null; + } + } + + private void throwEglException(String function) { + throwEglException(function, mEgl.eglGetError()); + } + + public static void throwEglException(String function, int error) { + String message = formatEglError(function, error); + throw new RuntimeException(message); + } + + public static void logEglErrorAsWarning(String tag, String function, int error) { + Log.w(tag, formatEglError(function, error)); + } + + public static String formatEglError(String function, int error) { + return function + " failed: " + error; + } + + } + + private static class RenderThread extends Thread { + private static final int INVALID = -1; + private static final int RENDER = 1; + private static final int CHANGE_SURFACE = 2; + private static final int RESIZE_SURFACE = 3; + private static final int FINISH = 4; + + private EglHelper mEglHelper = new EglHelper(); + + private Object mLock = new Object(); + private int mExecMsgId = INVALID; + private SurfaceTexture mSurface; + private Renderer mRenderer; + private int mWidth, mHeight; + + private boolean mFinished = false; + private GL10 mGL; + + public RenderThread(Renderer renderer) { + super("RenderThread"); + mRenderer = renderer; + start(); + } + + private void checkRenderer() { + if (mRenderer == null) { + throw new IllegalArgumentException("Renderer is null!"); + } + } + + private void checkSurface() { + if (mSurface == null) { + throw new IllegalArgumentException("surface is null!"); + } + } + + public void setSurface(SurfaceTexture surface) { + // If the surface is null we're being torn down, don't need a + // renderer then + if (surface != null) { + checkRenderer(); + } + mSurface = surface; + exec(CHANGE_SURFACE); + } + + public void setSize(int width, int height) { + checkRenderer(); + checkSurface(); + mWidth = width; + mHeight = height; + exec(RESIZE_SURFACE); + } + + public void render() { + checkRenderer(); + if (mSurface != null) { + exec(RENDER); + mSurface.updateTexImage(); + } + } + + public void finish() { + mSurface = null; + exec(FINISH); + try { + join(); + } catch (InterruptedException e) { + // Ignore + } + } + + private void exec(int msgid) { + synchronized (mLock) { + if (mExecMsgId != INVALID) { + throw new IllegalArgumentException( + "Message already set - multithreaded access?"); + } + mExecMsgId = msgid; + mLock.notify(); + try { + mLock.wait(); + } catch (InterruptedException e) { + // Ignore + } + } + } + + private void handleMessageLocked(int what) { + switch (what) { + case CHANGE_SURFACE: + if (mEglHelper.createSurface(mSurface)) { + mGL = mEglHelper.createGL(); + mRenderer.onSurfaceCreated(mGL, mEglHelper.mEglConfig); + } + break; + case RESIZE_SURFACE: + mRenderer.onSurfaceChanged(mGL, mWidth, mHeight); + break; + case RENDER: + mRenderer.onDrawFrame(mGL); + mEglHelper.swap(); + break; + case FINISH: + mEglHelper.destroySurface(); + mEglHelper.finish(); + mFinished = true; + break; + } + } + + @Override + public void run() { + synchronized (mLock) { + mEglHelper.start(); + while (!mFinished) { + while (mExecMsgId == INVALID) { + try { + mLock.wait(); + } catch (InterruptedException e) { + // Ignore + } + } + handleMessageLocked(mExecMsgId); + mExecMsgId = INVALID; + mLock.notify(); + } + mExecMsgId = FINISH; + } + } + } +} diff --git a/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java b/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java new file mode 100644 index 000000000..c4e493b34 --- /dev/null +++ b/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java @@ -0,0 +1,825 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.photos.views; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.RectF; +import android.support.v4.util.LongSparseArray; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.Pools.Pool; +import android.util.Pools.SynchronizedPool; +import android.view.View; +import android.view.WindowManager; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.glrenderer.BasicTexture; +import com.android.gallery3d.glrenderer.GLCanvas; +import com.android.gallery3d.glrenderer.UploadedTexture; + +/** + * Handles laying out, decoding, and drawing of tiles in GL + */ +public class TiledImageRenderer { + public static final int SIZE_UNKNOWN = -1; + + private static final String TAG = "TiledImageRenderer"; + private static final int UPLOAD_LIMIT = 1; + + /* + * This is the tile state in the CPU side. + * Life of a Tile: + * ACTIVATED (initial state) + * --> IN_QUEUE - by queueForDecode() + * --> RECYCLED - by recycleTile() + * IN_QUEUE --> DECODING - by decodeTile() + * --> RECYCLED - by recycleTile) + * DECODING --> RECYCLING - by recycleTile() + * --> DECODED - by decodeTile() + * --> DECODE_FAIL - by decodeTile() + * RECYCLING --> RECYCLED - by decodeTile() + * DECODED --> ACTIVATED - (after the decoded bitmap is uploaded) + * DECODED --> RECYCLED - by recycleTile() + * DECODE_FAIL -> RECYCLED - by recycleTile() + * RECYCLED --> ACTIVATED - by obtainTile() + */ + private static final int STATE_ACTIVATED = 0x01; + private static final int STATE_IN_QUEUE = 0x02; + private static final int STATE_DECODING = 0x04; + private static final int STATE_DECODED = 0x08; + private static final int STATE_DECODE_FAIL = 0x10; + private static final int STATE_RECYCLING = 0x20; + private static final int STATE_RECYCLED = 0x40; + + private static Pool sTilePool = new SynchronizedPool(64); + + // TILE_SIZE must be 2^N + private int mTileSize; + + private TileSource mModel; + private BasicTexture mPreview; + protected int mLevelCount; // cache the value of mScaledBitmaps.length + + // The mLevel variable indicates which level of bitmap we should use. + // Level 0 means the original full-sized bitmap, and a larger value means + // a smaller scaled bitmap (The width and height of each scaled bitmap is + // half size of the previous one). If the value is in [0, mLevelCount), we + // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value + // is mLevelCount + private int mLevel = 0; + + private int mOffsetX; + private int mOffsetY; + + private int mUploadQuota; + private boolean mRenderComplete; + + private final RectF mSourceRect = new RectF(); + private final RectF mTargetRect = new RectF(); + + private final LongSparseArray mActiveTiles = new LongSparseArray(); + + // The following three queue are guarded by mQueueLock + private final Object mQueueLock = new Object(); + private final TileQueue mRecycledQueue = new TileQueue(); + private final TileQueue mUploadQueue = new TileQueue(); + private final TileQueue mDecodeQueue = new TileQueue(); + + // The width and height of the full-sized bitmap + protected int mImageWidth = SIZE_UNKNOWN; + protected int mImageHeight = SIZE_UNKNOWN; + + protected int mCenterX; + protected int mCenterY; + protected float mScale; + protected int mRotation; + + private boolean mLayoutTiles; + + // Temp variables to avoid memory allocation + private final Rect mTileRange = new Rect(); + private final Rect mActiveRange[] = {new Rect(), new Rect()}; + + private TileDecoder mTileDecoder; + private boolean mBackgroundTileUploaded; + + private int mViewWidth, mViewHeight; + private View mParent; + + /** + * Interface for providing tiles to a {@link TiledImageRenderer} + */ + public static interface TileSource { + + /** + * If the source does not care about the tile size, it should use + * {@link TiledImageRenderer#suggestedTileSize(Context)} + */ + public int getTileSize(); + public int getImageWidth(); + public int getImageHeight(); + public int getRotation(); + + /** + * Return a Preview image if available. This will be used as the base layer + * if higher res tiles are not yet available + */ + public BasicTexture getPreview(); + + /** + * The tile returned by this method can be specified this way: Assuming + * the image size is (width, height), first take the intersection of (0, + * 0) - (width, height) and (x, y) - (x + tileSize, y + tileSize). If + * in extending the region, we found some part of the region is outside + * the image, those pixels are filled with black. + * + * If level > 0, it does the same operation on a down-scaled version of + * the original image (down-scaled by a factor of 2^level), but (x, y) + * still refers to the coordinate on the original image. + * + * The method would be called by the decoder thread. + */ + public Bitmap getTile(int level, int x, int y, Bitmap reuse); + } + + public static int suggestedTileSize(Context context) { + return isHighResolution(context) ? 512 : 256; + } + + private static boolean isHighResolution(Context context) { + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) + context.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getMetrics(metrics); + return metrics.heightPixels > 2048 || metrics.widthPixels > 2048; + } + + public TiledImageRenderer(View parent) { + mParent = parent; + mTileDecoder = new TileDecoder(); + mTileDecoder.start(); + } + + public int getViewWidth() { + return mViewWidth; + } + + public int getViewHeight() { + return mViewHeight; + } + + private void invalidate() { + mParent.postInvalidate(); + } + + public void setModel(TileSource model, int rotation) { + if (mModel != model) { + mModel = model; + notifyModelInvalidated(); + } + if (mRotation != rotation) { + mRotation = rotation; + mLayoutTiles = true; + } + } + + private void calculateLevelCount() { + if (mPreview != null) { + mLevelCount = Math.max(0, Utils.ceilLog2( + mImageWidth / (float) mPreview.getWidth())); + } else { + int levels = 1; + int maxDim = Math.max(mImageWidth, mImageHeight); + int t = mTileSize; + while (t < maxDim) { + t <<= 1; + levels++; + } + mLevelCount = levels; + } + } + + public void notifyModelInvalidated() { + invalidateTiles(); + if (mModel == null) { + mImageWidth = 0; + mImageHeight = 0; + mLevelCount = 0; + mPreview = null; + } else { + mImageWidth = mModel.getImageWidth(); + mImageHeight = mModel.getImageHeight(); + mPreview = mModel.getPreview(); + mTileSize = mModel.getTileSize(); + calculateLevelCount(); + } + mLayoutTiles = true; + } + + public void setViewSize(int width, int height) { + mViewWidth = width; + mViewHeight = height; + } + + public void setPosition(int centerX, int centerY, float scale) { + if (mCenterX == centerX && mCenterY == centerY + && mScale == scale) { + return; + } + mCenterX = centerX; + mCenterY = centerY; + mScale = scale; + mLayoutTiles = true; + } + + // Prepare the tiles we want to use for display. + // + // 1. Decide the tile level we want to use for display. + // 2. Decide the tile levels we want to keep as texture (in addition to + // the one we use for display). + // 3. Recycle unused tiles. + // 4. Activate the tiles we want. + private void layoutTiles() { + if (mViewWidth == 0 || mViewHeight == 0 || !mLayoutTiles) { + return; + } + mLayoutTiles = false; + + // The tile levels we want to keep as texture is in the range + // [fromLevel, endLevel). + int fromLevel; + int endLevel; + + // We want to use a texture larger than or equal to the display size. + mLevel = Utils.clamp(Utils.floorLog2(1f / mScale), 0, mLevelCount); + + // We want to keep one more tile level as texture in addition to what + // we use for display. So it can be faster when the scale moves to the + // next level. We choose the level closest to the current scale. + if (mLevel != mLevelCount) { + Rect range = mTileRange; + getRange(range, mCenterX, mCenterY, mLevel, mScale, mRotation); + mOffsetX = Math.round(mViewWidth / 2f + (range.left - mCenterX) * mScale); + mOffsetY = Math.round(mViewHeight / 2f + (range.top - mCenterY) * mScale); + fromLevel = mScale * (1 << mLevel) > 0.75f ? mLevel - 1 : mLevel; + } else { + // Activate the tiles of the smallest two levels. + fromLevel = mLevel - 2; + mOffsetX = Math.round(mViewWidth / 2f - mCenterX * mScale); + mOffsetY = Math.round(mViewHeight / 2f - mCenterY * mScale); + } + + fromLevel = Math.max(0, Math.min(fromLevel, mLevelCount - 2)); + endLevel = Math.min(fromLevel + 2, mLevelCount); + + Rect range[] = mActiveRange; + for (int i = fromLevel; i < endLevel; ++i) { + getRange(range[i - fromLevel], mCenterX, mCenterY, i, mRotation); + } + + // If rotation is transient, don't update the tile. + if (mRotation % 90 != 0) { + return; + } + + synchronized (mQueueLock) { + mDecodeQueue.clean(); + mUploadQueue.clean(); + mBackgroundTileUploaded = false; + + // Recycle unused tiles: if the level of the active tile is outside the + // range [fromLevel, endLevel) or not in the visible range. + int n = mActiveTiles.size(); + for (int i = 0; i < n; i++) { + Tile tile = mActiveTiles.valueAt(i); + int level = tile.mTileLevel; + if (level < fromLevel || level >= endLevel + || !range[level - fromLevel].contains(tile.mX, tile.mY)) { + mActiveTiles.removeAt(i); + i--; + n--; + recycleTile(tile); + } + } + } + + for (int i = fromLevel; i < endLevel; ++i) { + int size = mTileSize << i; + Rect r = range[i - fromLevel]; + for (int y = r.top, bottom = r.bottom; y < bottom; y += size) { + for (int x = r.left, right = r.right; x < right; x += size) { + activateTile(x, y, i); + } + } + } + invalidate(); + } + + private void invalidateTiles() { + synchronized (mQueueLock) { + mDecodeQueue.clean(); + mUploadQueue.clean(); + + // TODO(xx): disable decoder + int n = mActiveTiles.size(); + for (int i = 0; i < n; i++) { + Tile tile = mActiveTiles.valueAt(i); + recycleTile(tile); + } + mActiveTiles.clear(); + } + } + + private void getRange(Rect out, int cX, int cY, int level, int rotation) { + getRange(out, cX, cY, level, 1f / (1 << (level + 1)), rotation); + } + + // If the bitmap is scaled by the given factor "scale", return the + // rectangle containing visible range. The left-top coordinate returned is + // aligned to the tile boundary. + // + // (cX, cY) is the point on the original bitmap which will be put in the + // center of the ImageViewer. + private void getRange(Rect out, + int cX, int cY, int level, float scale, int rotation) { + + double radians = Math.toRadians(-rotation); + double w = mViewWidth; + double h = mViewHeight; + + double cos = Math.cos(radians); + double sin = Math.sin(radians); + int width = (int) Math.ceil(Math.max( + Math.abs(cos * w - sin * h), Math.abs(cos * w + sin * h))); + int height = (int) Math.ceil(Math.max( + Math.abs(sin * w + cos * h), Math.abs(sin * w - cos * h))); + + int left = (int) Math.floor(cX - width / (2f * scale)); + int top = (int) Math.floor(cY - height / (2f * scale)); + int right = (int) Math.ceil(left + width / scale); + int bottom = (int) Math.ceil(top + height / scale); + + // align the rectangle to tile boundary + int size = mTileSize << level; + left = Math.max(0, size * (left / size)); + top = Math.max(0, size * (top / size)); + right = Math.min(mImageWidth, right); + bottom = Math.min(mImageHeight, bottom); + + out.set(left, top, right, bottom); + } + + public void freeTextures() { + mLayoutTiles = true; + + mTileDecoder.finishAndWait(); + synchronized (mQueueLock) { + mUploadQueue.clean(); + mDecodeQueue.clean(); + Tile tile = mRecycledQueue.pop(); + while (tile != null) { + tile.recycle(); + tile = mRecycledQueue.pop(); + } + } + + int n = mActiveTiles.size(); + for (int i = 0; i < n; i++) { + Tile texture = mActiveTiles.valueAt(i); + texture.recycle(); + } + mActiveTiles.clear(); + mTileRange.set(0, 0, 0, 0); + + while (sTilePool.acquire() != null) {} + } + + public boolean draw(GLCanvas canvas) { + layoutTiles(); + uploadTiles(canvas); + + mUploadQuota = UPLOAD_LIMIT; + mRenderComplete = true; + + int level = mLevel; + int rotation = mRotation; + int flags = 0; + if (rotation != 0) { + flags |= GLCanvas.SAVE_FLAG_MATRIX; + } + + if (flags != 0) { + canvas.save(flags); + if (rotation != 0) { + int centerX = mViewWidth / 2, centerY = mViewHeight / 2; + canvas.translate(centerX, centerY); + canvas.rotate(rotation, 0, 0, 1); + canvas.translate(-centerX, -centerY); + } + } + try { + if (level != mLevelCount) { + int size = (mTileSize << level); + float length = size * mScale; + Rect r = mTileRange; + + for (int ty = r.top, i = 0; ty < r.bottom; ty += size, i++) { + float y = mOffsetY + i * length; + for (int tx = r.left, j = 0; tx < r.right; tx += size, j++) { + float x = mOffsetX + j * length; + drawTile(canvas, tx, ty, level, x, y, length); + } + } + } else if (mPreview != null) { + mPreview.draw(canvas, mOffsetX, mOffsetY, + Math.round(mImageWidth * mScale), + Math.round(mImageHeight * mScale)); + } + } finally { + if (flags != 0) { + canvas.restore(); + } + } + + if (mRenderComplete) { + if (!mBackgroundTileUploaded) { + uploadBackgroundTiles(canvas); + } + } else { + invalidate(); + } + return mRenderComplete || mPreview != null; + } + + private void uploadBackgroundTiles(GLCanvas canvas) { + mBackgroundTileUploaded = true; + int n = mActiveTiles.size(); + for (int i = 0; i < n; i++) { + Tile tile = mActiveTiles.valueAt(i); + if (!tile.isContentValid()) { + queueForDecode(tile); + } + } + } + + private void queueForDecode(Tile tile) { + synchronized (mQueueLock) { + if (tile.mTileState == STATE_ACTIVATED) { + tile.mTileState = STATE_IN_QUEUE; + if (mDecodeQueue.push(tile)) { + mQueueLock.notifyAll(); + } + } + } + } + + private void decodeTile(Tile tile) { + synchronized (mQueueLock) { + if (tile.mTileState != STATE_IN_QUEUE) { + return; + } + tile.mTileState = STATE_DECODING; + } + boolean decodeComplete = tile.decode(); + synchronized (mQueueLock) { + if (tile.mTileState == STATE_RECYCLING) { + tile.mTileState = STATE_RECYCLED; + if (tile.mDecodedTile != null) { + sTilePool.release(tile.mDecodedTile); + tile.mDecodedTile = null; + } + mRecycledQueue.push(tile); + return; + } + tile.mTileState = decodeComplete ? STATE_DECODED : STATE_DECODE_FAIL; + if (!decodeComplete) { + return; + } + mUploadQueue.push(tile); + } + invalidate(); + } + + private Tile obtainTile(int x, int y, int level) { + synchronized (mQueueLock) { + Tile tile = mRecycledQueue.pop(); + if (tile != null) { + tile.mTileState = STATE_ACTIVATED; + tile.update(x, y, level); + return tile; + } + return new Tile(x, y, level); + } + } + + private void recycleTile(Tile tile) { + synchronized (mQueueLock) { + if (tile.mTileState == STATE_DECODING) { + tile.mTileState = STATE_RECYCLING; + return; + } + tile.mTileState = STATE_RECYCLED; + if (tile.mDecodedTile != null) { + sTilePool.release(tile.mDecodedTile); + tile.mDecodedTile = null; + } + mRecycledQueue.push(tile); + } + } + + private void activateTile(int x, int y, int level) { + long key = makeTileKey(x, y, level); + Tile tile = mActiveTiles.get(key); + if (tile != null) { + if (tile.mTileState == STATE_IN_QUEUE) { + tile.mTileState = STATE_ACTIVATED; + } + return; + } + tile = obtainTile(x, y, level); + mActiveTiles.put(key, tile); + } + + private Tile getTile(int x, int y, int level) { + return mActiveTiles.get(makeTileKey(x, y, level)); + } + + private static long makeTileKey(int x, int y, int level) { + long result = x; + result = (result << 16) | y; + result = (result << 16) | level; + return result; + } + + private void uploadTiles(GLCanvas canvas) { + int quota = UPLOAD_LIMIT; + Tile tile = null; + while (quota > 0) { + synchronized (mQueueLock) { + tile = mUploadQueue.pop(); + } + if (tile == null) { + break; + } + if (!tile.isContentValid()) { + if (tile.mTileState == STATE_DECODED) { + tile.updateContent(canvas); + --quota; + } else { + Log.w(TAG, "Tile in upload queue has invalid state: " + tile.mTileState); + } + } + } + if (tile != null) { + invalidate(); + } + } + + // Draw the tile to a square at canvas that locates at (x, y) and + // has a side length of length. + private void drawTile(GLCanvas canvas, + int tx, int ty, int level, float x, float y, float length) { + RectF source = mSourceRect; + RectF target = mTargetRect; + target.set(x, y, x + length, y + length); + source.set(0, 0, mTileSize, mTileSize); + + Tile tile = getTile(tx, ty, level); + if (tile != null) { + if (!tile.isContentValid()) { + if (tile.mTileState == STATE_DECODED) { + if (mUploadQuota > 0) { + --mUploadQuota; + tile.updateContent(canvas); + } else { + mRenderComplete = false; + } + } else if (tile.mTileState != STATE_DECODE_FAIL){ + mRenderComplete = false; + queueForDecode(tile); + } + } + if (drawTile(tile, canvas, source, target)) { + return; + } + } + if (mPreview != null) { + int size = mTileSize << level; + float scaleX = (float) mPreview.getWidth() / mImageWidth; + float scaleY = (float) mPreview.getHeight() / mImageHeight; + source.set(tx * scaleX, ty * scaleY, (tx + size) * scaleX, + (ty + size) * scaleY); + canvas.drawTexture(mPreview, source, target); + } + } + + private boolean drawTile( + Tile tile, GLCanvas canvas, RectF source, RectF target) { + while (true) { + if (tile.isContentValid()) { + canvas.drawTexture(tile, source, target); + return true; + } + + // Parent can be divided to four quads and tile is one of the four. + Tile parent = tile.getParentTile(); + if (parent == null) { + return false; + } + if (tile.mX == parent.mX) { + source.left /= 2f; + source.right /= 2f; + } else { + source.left = (mTileSize + source.left) / 2f; + source.right = (mTileSize + source.right) / 2f; + } + if (tile.mY == parent.mY) { + source.top /= 2f; + source.bottom /= 2f; + } else { + source.top = (mTileSize + source.top) / 2f; + source.bottom = (mTileSize + source.bottom) / 2f; + } + tile = parent; + } + } + + private class Tile extends UploadedTexture { + public int mX; + public int mY; + public int mTileLevel; + public Tile mNext; + public Bitmap mDecodedTile; + public volatile int mTileState = STATE_ACTIVATED; + + public Tile(int x, int y, int level) { + mX = x; + mY = y; + mTileLevel = level; + } + + @Override + protected void onFreeBitmap(Bitmap bitmap) { + sTilePool.release(bitmap); + } + + boolean decode() { + // Get a tile from the original image. The tile is down-scaled + // by (1 << mTilelevel) from a region in the original image. + try { + Bitmap reuse = sTilePool.acquire(); + if (reuse != null && reuse.getWidth() != mTileSize) { + reuse = null; + } + mDecodedTile = mModel.getTile(mTileLevel, mX, mY, reuse); + } catch (Throwable t) { + Log.w(TAG, "fail to decode tile", t); + } + return mDecodedTile != null; + } + + @Override + protected Bitmap onGetBitmap() { + Utils.assertTrue(mTileState == STATE_DECODED); + + // We need to override the width and height, so that we won't + // draw beyond the boundaries. + int rightEdge = ((mImageWidth - mX) >> mTileLevel); + int bottomEdge = ((mImageHeight - mY) >> mTileLevel); + setSize(Math.min(mTileSize, rightEdge), Math.min(mTileSize, bottomEdge)); + + Bitmap bitmap = mDecodedTile; + mDecodedTile = null; + mTileState = STATE_ACTIVATED; + return bitmap; + } + + // We override getTextureWidth() and getTextureHeight() here, so the + // texture can be re-used for different tiles regardless of the actual + // size of the tile (which may be small because it is a tile at the + // boundary). + @Override + public int getTextureWidth() { + return mTileSize; + } + + @Override + public int getTextureHeight() { + return mTileSize; + } + + public void update(int x, int y, int level) { + mX = x; + mY = y; + mTileLevel = level; + invalidateContent(); + } + + public Tile getParentTile() { + if (mTileLevel + 1 == mLevelCount) { + return null; + } + int size = mTileSize << (mTileLevel + 1); + int x = size * (mX / size); + int y = size * (mY / size); + return getTile(x, y, mTileLevel + 1); + } + + @Override + public String toString() { + return String.format("tile(%s, %s, %s / %s)", + mX / mTileSize, mY / mTileSize, mLevel, mLevelCount); + } + } + + private static class TileQueue { + private Tile mHead; + + public Tile pop() { + Tile tile = mHead; + if (tile != null) { + mHead = tile.mNext; + } + return tile; + } + + public boolean push(Tile tile) { + if (contains(tile)) { + Log.w(TAG, "Attempting to add a tile already in the queue!"); + return false; + } + boolean wasEmpty = mHead == null; + tile.mNext = mHead; + mHead = tile; + return wasEmpty; + } + + private boolean contains(Tile tile) { + Tile other = mHead; + while (other != null) { + if (other == tile) { + return true; + } + other = other.mNext; + } + return false; + } + + public void clean() { + mHead = null; + } + } + + private class TileDecoder extends Thread { + + public void finishAndWait() { + interrupt(); + try { + join(); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while waiting for TileDecoder thread to finish!"); + } + } + + private Tile waitForTile() throws InterruptedException { + synchronized (mQueueLock) { + while (true) { + Tile tile = mDecodeQueue.pop(); + if (tile != null) { + return tile; + } + mQueueLock.wait(); + } + } + } + + @Override + public void run() { + try { + while (!isInterrupted()) { + Tile tile = waitForTile(); + decodeTile(tile); + } + } catch (InterruptedException ex) { + // We were finished + } + } + + } +} diff --git a/WallpaperPicker/src/com/android/photos/views/TiledImageView.java b/WallpaperPicker/src/com/android/photos/views/TiledImageView.java new file mode 100644 index 000000000..af4199c91 --- /dev/null +++ b/WallpaperPicker/src/com/android/photos/views/TiledImageView.java @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.photos.views; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.RectF; +import android.opengl.GLSurfaceView; +import android.opengl.GLSurfaceView.Renderer; +import android.os.Build; +import android.util.AttributeSet; +import android.view.Choreographer; +import android.view.Choreographer.FrameCallback; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.gallery3d.glrenderer.BasicTexture; +import com.android.gallery3d.glrenderer.GLES20Canvas; +import com.android.photos.views.TiledImageRenderer.TileSource; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView} + * or {@link BlockingGLTextureView}. + */ +public class TiledImageView extends FrameLayout { + + private static final boolean USE_TEXTURE_VIEW = false; + private static final boolean IS_SUPPORTED = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + private static final boolean USE_CHOREOGRAPHER = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + + private BlockingGLTextureView mTextureView; + private GLSurfaceView mGLSurfaceView; + private boolean mInvalPending = false; + private FrameCallback mFrameCallback; + + 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 static boolean isTilingSupported() { + return IS_SUPPORTED; + } + + public TiledImageView(Context context) { + this(context, null); + } + + public TiledImageView(Context context, AttributeSet attrs) { + super(context, attrs); + if (!IS_SUPPORTED) { + return; + } + + mRenderer = new ImageRendererWrapper(); + mRenderer.image = new TiledImageRenderer(this); + View view; + if (USE_TEXTURE_VIEW) { + mTextureView = new BlockingGLTextureView(context); + mTextureView.setRenderer(new TileRenderer()); + view = mTextureView; + } else { + mGLSurfaceView = new GLSurfaceView(context); + mGLSurfaceView.setEGLContextClientVersion(2); + mGLSurfaceView.setRenderer(new TileRenderer()); + mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); + view = mGLSurfaceView; + } + addView(view, new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + //setTileSource(new ColoredTiles()); + } + + public void destroy() { + if (!IS_SUPPORTED) { + return; + } + if (USE_TEXTURE_VIEW) { + mTextureView.destroy(); + } else { + mGLSurfaceView.queueEvent(mFreeTextures); + } + } + + private Runnable mFreeTextures = new Runnable() { + + @Override + public void run() { + mRenderer.image.freeTextures(); + } + }; + + public void onPause() { + if (!IS_SUPPORTED) { + return; + } + if (!USE_TEXTURE_VIEW) { + mGLSurfaceView.onPause(); + } + } + + public void onResume() { + if (!IS_SUPPORTED) { + return; + } + if (!USE_TEXTURE_VIEW) { + mGLSurfaceView.onResume(); + } + } + + public void setTileSource(TileSource source, Runnable isReadyCallback) { + if (!IS_SUPPORTED) { + return; + } + synchronized (mLock) { + mRenderer.source = source; + mRenderer.isReadyCallback = isReadyCallback; + mRenderer.centerX = source != null ? source.getImageWidth() / 2 : 0; + mRenderer.centerY = source != null ? source.getImageHeight() / 2 : 0; + mRenderer.rotation = source != null ? source.getRotation() : 0; + mRenderer.scale = 0; + updateScaleIfNecessaryLocked(mRenderer); + } + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, + int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (!IS_SUPPORTED) { + return; + } + synchronized (mLock) { + updateScaleIfNecessaryLocked(mRenderer); + } + } + + private void updateScaleIfNecessaryLocked(ImageRendererWrapper renderer) { + if (renderer == null || renderer.source == null + || renderer.scale > 0 || getWidth() == 0) { + return; + } + renderer.scale = Math.min( + (float) getWidth() / (float) renderer.source.getImageWidth(), + (float) getHeight() / (float) renderer.source.getImageHeight()); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + if (!IS_SUPPORTED) { + return; + } + if (USE_TEXTURE_VIEW) { + mTextureView.render(); + } + super.dispatchDraw(canvas); + } + + @SuppressLint("NewApi") + @Override + public void setTranslationX(float translationX) { + if (!IS_SUPPORTED) { + return; + } + super.setTranslationX(translationX); + } + + @Override + public void invalidate() { + if (!IS_SUPPORTED) { + return; + } + if (USE_TEXTURE_VIEW) { + super.invalidate(); + mTextureView.invalidate(); + } else { + if (USE_CHOREOGRAPHER) { + invalOnVsync(); + } else { + mGLSurfaceView.requestRender(); + } + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private void invalOnVsync() { + if (!mInvalPending) { + mInvalPending = true; + if (mFrameCallback == null) { + mFrameCallback = new FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + mInvalPending = false; + mGLSurfaceView.requestRender(); + } + }; + } + Choreographer.getInstance().postFrameCallback(mFrameCallback); + } + } + + private RectF mTempRectF = new RectF(); + public void positionFromMatrix(Matrix matrix) { + if (!IS_SUPPORTED) { + return; + } + if (mRenderer.source != null) { + final int rotation = mRenderer.source.getRotation(); + final boolean swap = !(rotation % 180 == 0); + final int width = swap ? mRenderer.source.getImageHeight() + : mRenderer.source.getImageWidth(); + final int height = swap ? mRenderer.source.getImageWidth() + : mRenderer.source.getImageHeight(); + mTempRectF.set(0, 0, width, height); + matrix.mapRect(mTempRectF); + matrix.getValues(mValues); + int cx = width / 2; + int cy = height / 2; + float scale = mValues[Matrix.MSCALE_X]; + int xoffset = Math.round((getWidth() - mTempRectF.width()) / 2 / scale); + int yoffset = Math.round((getHeight() - mTempRectF.height()) / 2 / scale); + if (rotation == 90 || rotation == 180) { + cx += (mTempRectF.left / scale) - xoffset; + } else { + cx -= (mTempRectF.left / scale) - xoffset; + } + if (rotation == 180 || rotation == 270) { + cy += (mTempRectF.top / scale) - yoffset; + } else { + cy -= (mTempRectF.top / scale) - yoffset; + } + mRenderer.scale = scale; + mRenderer.centerX = swap ? cy : cx; + mRenderer.centerY = swap ? cx : cy; + invalidate(); + } + } + + private class TileRenderer implements Renderer { + + private GLES20Canvas mCanvas; + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + mCanvas = new GLES20Canvas(); + BasicTexture.invalidateAllTextures(); + mRenderer.image.setModel(mRenderer.source, mRenderer.rotation); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + mCanvas.setSize(width, height); + mRenderer.image.setViewSize(width, height); + } + + @Override + public void onDrawFrame(GL10 gl) { + mCanvas.clearBuffer(); + Runnable readyCallback; + synchronized (mLock) { + readyCallback = mRenderer.isReadyCallback; + mRenderer.image.setModel(mRenderer.source, mRenderer.rotation); + mRenderer.image.setPosition(mRenderer.centerX, mRenderer.centerY, + mRenderer.scale); + } + boolean complete = mRenderer.image.draw(mCanvas); + if (complete && readyCallback != null) { + synchronized (mLock) { + // Make sure we don't trample on a newly set callback/source + // if it changed while we were rendering + if (mRenderer.isReadyCallback == readyCallback) { + mRenderer.isReadyCallback = null; + } + } + if (readyCallback != null) { + post(readyCallback); + } + } + } + + } + + @SuppressWarnings("unused") + private static class ColoredTiles implements TileSource { + private static final int[] COLORS = new int[] { + Color.RED, + Color.BLUE, + Color.YELLOW, + Color.GREEN, + Color.CYAN, + Color.MAGENTA, + Color.WHITE, + }; + + private Paint mPaint = new Paint(); + private Canvas mCanvas = new Canvas(); + + @Override + public int getTileSize() { + return 256; + } + + @Override + public int getImageWidth() { + return 16384; + } + + @Override + public int getImageHeight() { + return 8192; + } + + @Override + public int getRotation() { + return 0; + } + + @Override + public Bitmap getTile(int level, int x, int y, Bitmap bitmap) { + int tileSize = getTileSize(); + if (bitmap == null) { + bitmap = Bitmap.createBitmap(tileSize, tileSize, + Bitmap.Config.ARGB_8888); + } + mCanvas.setBitmap(bitmap); + mCanvas.drawColor(COLORS[level]); + mPaint.setColor(Color.BLACK); + mPaint.setTextSize(20); + mPaint.setTextAlign(Align.CENTER); + mCanvas.drawText(x + "x" + y, 128, 128, mPaint); + tileSize <<= level; + x /= tileSize; + y /= tileSize; + mCanvas.drawText(x + "x" + y + " @ " + level, 128, 30, mPaint); + mCanvas.setBitmap(null); + return bitmap; + } + + @Override + public BasicTexture getPreview() { + return null; + } + } +} -- cgit v1.2.3