diff options
Diffstat (limited to 'gallerycommon/src/com/android')
36 files changed, 11235 insertions, 0 deletions
diff --git a/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java new file mode 100644 index 000000000..f4de5c9ff --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/ApiHelper.java @@ -0,0 +1,234 @@ +/* + * 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.common; + +import android.app.admin.DevicePolicyManager; +import android.content.ComponentName; +import android.hardware.Camera; +import android.os.Build; +import android.provider.MediaStore.MediaColumns; +import android.view.View; +import android.view.WindowManager; + +import java.lang.reflect.Field; + +public class ApiHelper { + public static interface VERSION_CODES { + // These value are copied from Build.VERSION_CODES + public static final int GINGERBREAD_MR1 = 10; + public static final int HONEYCOMB = 11; + public static final int HONEYCOMB_MR1 = 12; + public static final int HONEYCOMB_MR2 = 13; + public static final int ICE_CREAM_SANDWICH = 14; + public static final int ICE_CREAM_SANDWICH_MR1 = 15; + public static final int JELLY_BEAN = 16; + public static final int JELLY_BEAN_MR1 = 17; + public static final int JELLY_BEAN_MR2 = 18; + } + + public static final boolean AT_LEAST_16 = Build.VERSION.SDK_INT >= 16; + + public static final boolean USE_888_PIXEL_FORMAT = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean ENABLE_PHOTO_EDITOR = + Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; + + public static final boolean HAS_VIEW_SYSTEM_UI_FLAG_LAYOUT_STABLE = + hasField(View.class, "SYSTEM_UI_FLAG_LAYOUT_STABLE"); + + public static final boolean HAS_VIEW_SYSTEM_UI_FLAG_HIDE_NAVIGATION = + hasField(View.class, "SYSTEM_UI_FLAG_HIDE_NAVIGATION"); + + public static final boolean HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT = + hasField(MediaColumns.class, "WIDTH"); + + public static final boolean HAS_REUSING_BITMAP_IN_BITMAP_REGION_DECODER = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_REUSING_BITMAP_IN_BITMAP_FACTORY = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_SET_BEAM_PUSH_URIS = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_SET_DEFALT_BUFFER_SIZE = hasMethod( + "android.graphics.SurfaceTexture", "setDefaultBufferSize", + int.class, int.class); + + public static final boolean HAS_RELEASE_SURFACE_TEXTURE = hasMethod( + "android.graphics.SurfaceTexture", "release"); + + public static final boolean HAS_SURFACE_TEXTURE = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_MTP = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1; + + public static final boolean HAS_AUTO_FOCUS_MOVE_CALLBACK = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_REMOTE_VIEWS_SERVICE = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_INTENT_EXTRA_LOCAL_ONLY = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_SET_SYSTEM_UI_VISIBILITY = + hasMethod(View.class, "setSystemUiVisibility", int.class); + + public static final boolean HAS_FACE_DETECTION; + static { + boolean hasFaceDetection = false; + try { + Class<?> listenerClass = Class.forName( + "android.hardware.Camera$FaceDetectionListener"); + hasFaceDetection = + hasMethod(Camera.class, "setFaceDetectionListener", listenerClass) && + hasMethod(Camera.class, "startFaceDetection") && + hasMethod(Camera.class, "stopFaceDetection") && + hasMethod(Camera.Parameters.class, "getMaxNumDetectedFaces"); + } catch (Throwable t) { + } + HAS_FACE_DETECTION = hasFaceDetection; + } + + public static final boolean HAS_GET_CAMERA_DISABLED = + hasMethod(DevicePolicyManager.class, "getCameraDisabled", ComponentName.class); + + public static final boolean HAS_MEDIA_ACTION_SOUND = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_TIME_LAPSE_RECORDING = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_ZOOM_WHEN_RECORDING = + Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; + + public static final boolean HAS_CAMERA_FOCUS_AREA = + Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; + + public static final boolean HAS_CAMERA_METERING_AREA = + Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; + + public static final boolean HAS_MOTION_EVENT_TRANSFORM = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_EFFECTS_RECORDING = false; + + // "Background" filter does not have "context" input port in jelly bean. + public static final boolean HAS_EFFECTS_RECORDING_CONTEXT_INPUT = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1; + + public static final boolean HAS_GET_SUPPORTED_VIDEO_SIZE = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_SET_ICON_ATTRIBUTE = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_MEDIA_PROVIDER_FILES_TABLE = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_SURFACE_TEXTURE_RECORDING = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_ACTION_BAR = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + // Ex: View.setTranslationX. + public static final boolean HAS_VIEW_TRANSFORM_PROPERTIES = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_CAMERA_HDR = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1; + + public static final boolean HAS_OPTIONS_IN_MUTABLE = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean CAN_START_PREVIEW_IN_JPEG_CALLBACK = + Build.VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH; + + public static final boolean HAS_VIEW_PROPERTY_ANIMATOR = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1; + + public static final boolean HAS_POST_ON_ANIMATION = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_ANNOUNCE_FOR_ACCESSIBILITY = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_OBJECT_ANIMATION = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_GLES20_REQUIRED = + Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + + public static final boolean HAS_ROTATION_ANIMATION = + hasField(WindowManager.LayoutParams.class, "rotationAnimation"); + + public static final boolean HAS_ORIENTATION_LOCK = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2; + + public static final boolean HAS_CANCELLATION_SIGNAL = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + + public static final boolean HAS_MEDIA_MUXER = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2; + + public static final boolean HAS_DISPLAY_LISTENER = + Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1; + + public static int getIntFieldIfExists(Class<?> klass, String fieldName, + Class<?> obj, int defaultVal) { + try { + Field f = klass.getDeclaredField(fieldName); + return f.getInt(obj); + } catch (Exception e) { + return defaultVal; + } + } + + private static boolean hasField(Class<?> klass, String fieldName) { + try { + klass.getDeclaredField(fieldName); + return true; + } catch (NoSuchFieldException e) { + return false; + } + } + + private static boolean hasMethod(String className, String methodName, + Class<?>... parameterTypes) { + try { + Class<?> klass = Class.forName(className); + klass.getDeclaredMethod(methodName, parameterTypes); + return true; + } catch (Throwable th) { + return false; + } + } + + private static boolean hasMethod( + Class<?> klass, String methodName, Class<?> ... paramTypes) { + try { + klass.getDeclaredMethod(methodName, paramTypes); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/AsyncTaskUtil.java b/gallerycommon/src/com/android/gallery3d/common/AsyncTaskUtil.java new file mode 100644 index 000000000..b70c4d365 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/AsyncTaskUtil.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.common; + +import android.os.AsyncTask; +import android.os.Build; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.Executor; + +/** + * Helper class to execute an AsyncTask in parallel if SDK version is 11 or newer. + */ +public class AsyncTaskUtil { + private static Method sMethodExecuteOnExecutor; + private static Executor sExecutor; + static { + if (Build.VERSION.SDK_INT >= 11) { + try { + sExecutor = (Executor) AsyncTask.class.getField("THREAD_POOL_EXECUTOR") + .get(null); + sMethodExecuteOnExecutor = AsyncTask.class.getMethod( + "executeOnExecutor", Executor.class, Object[].class); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + } + + public static <Param> void executeInParallel(AsyncTask<Param, ?, ?> task, Param... params) { + if (Build.VERSION.SDK_INT < 11) { + task.execute(params); + } else { + try { + sMethodExecuteOnExecutor.invoke(task, sExecutor, params); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } + + private AsyncTaskUtil() { + } +} + diff --git a/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java new file mode 100644 index 000000000..a671ed2b9 --- /dev/null +++ b/gallerycommon/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/gallerycommon/src/com/android/gallery3d/common/BlobCache.java b/gallerycommon/src/com/android/gallery3d/common/BlobCache.java new file mode 100644 index 000000000..3c131e591 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/BlobCache.java @@ -0,0 +1,668 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This is an on-disk cache which maps a 64-bits key to a byte array. +// +// It consists of three files: one index file and two data files. One of the +// data files is "active", and the other is "inactive". New entries are +// appended into the active region until it reaches the size limit. At that +// point the active file and the inactive file are swapped, and the new active +// file is truncated to empty (and the index for that file is also cleared). +// The index is a hash table with linear probing. When the load factor reaches +// 0.5, it does the same thing like when the size limit is reached. +// +// The index file format: (all numbers are stored in little-endian) +// [0] Magic number: 0xB3273030 +// [4] MaxEntries: Max number of hash entries per region. +// [8] MaxBytes: Max number of data bytes per region (including header). +// [12] ActiveRegion: The active growing region: 0 or 1. +// [16] ActiveEntries: The number of hash entries used in the active region. +// [20] ActiveBytes: The number of data bytes used in the active region. +// [24] Version number. +// [28] Checksum of [0..28). +// [32] Hash entries for region 0. The size is X = (12 * MaxEntries bytes). +// [32 + X] Hash entries for region 1. The size is also X. +// +// Each hash entry is 12 bytes: 8 bytes key and 4 bytes offset into the data +// file. The offset is 0 when the slot is free. Note that 0 is a valid value +// for key. The keys are used directly as index into a hash table, so they +// should be suitably distributed. +// +// Each data file stores data for one region. The data file is concatenated +// blobs followed by the magic number 0xBD248510. +// +// The blob format: +// [0] Key of this blob +// [8] Checksum of this blob +// [12] Offset of this blob +// [16] Length of this blob (not including header) +// [20] Blob +// +// Below are the interface for BlobCache. The instance of this class does not +// support concurrent use by multiple threads. +// +// public BlobCache(String path, int maxEntries, int maxBytes, boolean reset) throws IOException; +// public void insert(long key, byte[] data) throws IOException; +// public byte[] lookup(long key) throws IOException; +// public void lookup(LookupRequest req) throws IOException; +// public void close(); +// public void syncIndex(); +// public void syncAll(); +// public static void deleteFiles(String path); +// +package com.android.gallery3d.common; + +import android.util.Log; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Arrays; +import java.util.zip.Adler32; + +public class BlobCache implements Closeable { + private static final String TAG = "BlobCache"; + + private static final int MAGIC_INDEX_FILE = 0xB3273030; + private static final int MAGIC_DATA_FILE = 0xBD248510; + + // index header offset + private static final int IH_MAGIC = 0; + private static final int IH_MAX_ENTRIES = 4; + private static final int IH_MAX_BYTES = 8; + private static final int IH_ACTIVE_REGION = 12; + private static final int IH_ACTIVE_ENTRIES = 16; + private static final int IH_ACTIVE_BYTES = 20; + private static final int IH_VERSION = 24; + private static final int IH_CHECKSUM = 28; + private static final int INDEX_HEADER_SIZE = 32; + + private static final int DATA_HEADER_SIZE = 4; + + // blob header offset + private static final int BH_KEY = 0; + private static final int BH_CHECKSUM = 8; + private static final int BH_OFFSET = 12; + private static final int BH_LENGTH = 16; + private static final int BLOB_HEADER_SIZE = 20; + + private RandomAccessFile mIndexFile; + private RandomAccessFile mDataFile0; + private RandomAccessFile mDataFile1; + private FileChannel mIndexChannel; + private MappedByteBuffer mIndexBuffer; + + private int mMaxEntries; + private int mMaxBytes; + private int mActiveRegion; + private int mActiveEntries; + private int mActiveBytes; + private int mVersion; + + private RandomAccessFile mActiveDataFile; + private RandomAccessFile mInactiveDataFile; + private int mActiveHashStart; + private int mInactiveHashStart; + private byte[] mIndexHeader = new byte[INDEX_HEADER_SIZE]; + private byte[] mBlobHeader = new byte[BLOB_HEADER_SIZE]; + private Adler32 mAdler32 = new Adler32(); + + // Creates the cache. Three files will be created: + // path + ".idx", path + ".0", and path + ".1" + // The ".0" file and the ".1" file each stores data for a region. Each of + // them can grow to the size specified by maxBytes. The maxEntries parameter + // specifies the maximum number of entries each region can have. If the + // "reset" parameter is true, the cache will be cleared before use. + public BlobCache(String path, int maxEntries, int maxBytes, boolean reset) + throws IOException { + this(path, maxEntries, maxBytes, reset, 0); + } + + public BlobCache(String path, int maxEntries, int maxBytes, boolean reset, + int version) throws IOException { + mIndexFile = new RandomAccessFile(path + ".idx", "rw"); + mDataFile0 = new RandomAccessFile(path + ".0", "rw"); + mDataFile1 = new RandomAccessFile(path + ".1", "rw"); + mVersion = version; + + if (!reset && loadIndex()) { + return; + } + + resetCache(maxEntries, maxBytes); + + if (!loadIndex()) { + closeAll(); + throw new IOException("unable to load index"); + } + } + + // Delete the files associated with the given path previously created + // by the BlobCache constructor. + public static void deleteFiles(String path) { + deleteFileSilently(path + ".idx"); + deleteFileSilently(path + ".0"); + deleteFileSilently(path + ".1"); + } + + private static void deleteFileSilently(String path) { + try { + new File(path).delete(); + } catch (Throwable t) { + // ignore; + } + } + + // Close the cache. All resources are released. No other method should be + // called after this is called. + @Override + public void close() { + syncAll(); + closeAll(); + } + + private void closeAll() { + closeSilently(mIndexChannel); + closeSilently(mIndexFile); + closeSilently(mDataFile0); + closeSilently(mDataFile1); + } + + // Returns true if loading index is successful. After this method is called, + // mIndexHeader and index header in file should be kept sync. + private boolean loadIndex() { + try { + mIndexFile.seek(0); + mDataFile0.seek(0); + mDataFile1.seek(0); + + byte[] buf = mIndexHeader; + if (mIndexFile.read(buf) != INDEX_HEADER_SIZE) { + Log.w(TAG, "cannot read header"); + return false; + } + + if (readInt(buf, IH_MAGIC) != MAGIC_INDEX_FILE) { + Log.w(TAG, "cannot read header magic"); + return false; + } + + if (readInt(buf, IH_VERSION) != mVersion) { + Log.w(TAG, "version mismatch"); + return false; + } + + mMaxEntries = readInt(buf, IH_MAX_ENTRIES); + mMaxBytes = readInt(buf, IH_MAX_BYTES); + mActiveRegion = readInt(buf, IH_ACTIVE_REGION); + mActiveEntries = readInt(buf, IH_ACTIVE_ENTRIES); + mActiveBytes = readInt(buf, IH_ACTIVE_BYTES); + + int sum = readInt(buf, IH_CHECKSUM); + if (checkSum(buf, 0, IH_CHECKSUM) != sum) { + Log.w(TAG, "header checksum does not match"); + return false; + } + + // Sanity check + if (mMaxEntries <= 0) { + Log.w(TAG, "invalid max entries"); + return false; + } + if (mMaxBytes <= 0) { + Log.w(TAG, "invalid max bytes"); + return false; + } + if (mActiveRegion != 0 && mActiveRegion != 1) { + Log.w(TAG, "invalid active region"); + return false; + } + if (mActiveEntries < 0 || mActiveEntries > mMaxEntries) { + Log.w(TAG, "invalid active entries"); + return false; + } + if (mActiveBytes < DATA_HEADER_SIZE || mActiveBytes > mMaxBytes) { + Log.w(TAG, "invalid active bytes"); + return false; + } + if (mIndexFile.length() != + INDEX_HEADER_SIZE + mMaxEntries * 12 * 2) { + Log.w(TAG, "invalid index file length"); + return false; + } + + // Make sure data file has magic + byte[] magic = new byte[4]; + if (mDataFile0.read(magic) != 4) { + Log.w(TAG, "cannot read data file magic"); + return false; + } + if (readInt(magic, 0) != MAGIC_DATA_FILE) { + Log.w(TAG, "invalid data file magic"); + return false; + } + if (mDataFile1.read(magic) != 4) { + Log.w(TAG, "cannot read data file magic"); + return false; + } + if (readInt(magic, 0) != MAGIC_DATA_FILE) { + Log.w(TAG, "invalid data file magic"); + return false; + } + + // Map index file to memory + mIndexChannel = mIndexFile.getChannel(); + mIndexBuffer = mIndexChannel.map(FileChannel.MapMode.READ_WRITE, + 0, mIndexFile.length()); + mIndexBuffer.order(ByteOrder.LITTLE_ENDIAN); + + setActiveVariables(); + return true; + } catch (IOException ex) { + Log.e(TAG, "loadIndex failed.", ex); + return false; + } + } + + private void setActiveVariables() throws IOException { + mActiveDataFile = (mActiveRegion == 0) ? mDataFile0 : mDataFile1; + mInactiveDataFile = (mActiveRegion == 1) ? mDataFile0 : mDataFile1; + mActiveDataFile.setLength(mActiveBytes); + mActiveDataFile.seek(mActiveBytes); + + mActiveHashStart = INDEX_HEADER_SIZE; + mInactiveHashStart = INDEX_HEADER_SIZE; + + if (mActiveRegion == 0) { + mInactiveHashStart += mMaxEntries * 12; + } else { + mActiveHashStart += mMaxEntries * 12; + } + } + + private void resetCache(int maxEntries, int maxBytes) throws IOException { + mIndexFile.setLength(0); // truncate to zero the index + mIndexFile.setLength(INDEX_HEADER_SIZE + maxEntries * 12 * 2); + mIndexFile.seek(0); + byte[] buf = mIndexHeader; + writeInt(buf, IH_MAGIC, MAGIC_INDEX_FILE); + writeInt(buf, IH_MAX_ENTRIES, maxEntries); + writeInt(buf, IH_MAX_BYTES, maxBytes); + writeInt(buf, IH_ACTIVE_REGION, 0); + writeInt(buf, IH_ACTIVE_ENTRIES, 0); + writeInt(buf, IH_ACTIVE_BYTES, DATA_HEADER_SIZE); + writeInt(buf, IH_VERSION, mVersion); + writeInt(buf, IH_CHECKSUM, checkSum(buf, 0, IH_CHECKSUM)); + mIndexFile.write(buf); + // This is only needed if setLength does not zero the extended part. + // writeZero(mIndexFile, maxEntries * 12 * 2); + + mDataFile0.setLength(0); + mDataFile1.setLength(0); + mDataFile0.seek(0); + mDataFile1.seek(0); + writeInt(buf, 0, MAGIC_DATA_FILE); + mDataFile0.write(buf, 0, 4); + mDataFile1.write(buf, 0, 4); + } + + // Flip the active region and the inactive region. + private void flipRegion() throws IOException { + mActiveRegion = 1 - mActiveRegion; + mActiveEntries = 0; + mActiveBytes = DATA_HEADER_SIZE; + + writeInt(mIndexHeader, IH_ACTIVE_REGION, mActiveRegion); + writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries); + writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes); + updateIndexHeader(); + + setActiveVariables(); + clearHash(mActiveHashStart); + syncIndex(); + } + + // Sync mIndexHeader to the index file. + private void updateIndexHeader() { + writeInt(mIndexHeader, IH_CHECKSUM, + checkSum(mIndexHeader, 0, IH_CHECKSUM)); + mIndexBuffer.position(0); + mIndexBuffer.put(mIndexHeader); + } + + // Clear the hash table starting from the specified offset. + private void clearHash(int hashStart) { + byte[] zero = new byte[1024]; + mIndexBuffer.position(hashStart); + for (int count = mMaxEntries * 12; count > 0;) { + int todo = Math.min(count, 1024); + mIndexBuffer.put(zero, 0, todo); + count -= todo; + } + } + + // Inserts a (key, data) pair into the cache. + public void insert(long key, byte[] data) throws IOException { + if (DATA_HEADER_SIZE + BLOB_HEADER_SIZE + data.length > mMaxBytes) { + throw new RuntimeException("blob is too large!"); + } + + if (mActiveBytes + BLOB_HEADER_SIZE + data.length > mMaxBytes + || mActiveEntries * 2 >= mMaxEntries) { + flipRegion(); + } + + if (!lookupInternal(key, mActiveHashStart)) { + // If we don't have an existing entry with the same key, increase + // the entry count. + mActiveEntries++; + writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries); + } + + insertInternal(key, data, data.length); + updateIndexHeader(); + } + + public void clearEntry(long key) throws IOException { + if (!lookupInternal(key, mActiveHashStart)) { + return; // Nothing to clear + } + byte[] header = mBlobHeader; + Arrays.fill(header, (byte) 0); + mActiveDataFile.seek(mFileOffset); + mActiveDataFile.write(header); + } + + // Appends the data to the active file. It also updates the hash entry. + // The proper hash entry (suitable for insertion or replacement) must be + // pointed by mSlotOffset. + private void insertInternal(long key, byte[] data, int length) + throws IOException { + byte[] header = mBlobHeader; + int sum = checkSum(data); + writeLong(header, BH_KEY, key); + writeInt(header, BH_CHECKSUM, sum); + writeInt(header, BH_OFFSET, mActiveBytes); + writeInt(header, BH_LENGTH, length); + mActiveDataFile.write(header); + mActiveDataFile.write(data, 0, length); + + mIndexBuffer.putLong(mSlotOffset, key); + mIndexBuffer.putInt(mSlotOffset + 8, mActiveBytes); + mActiveBytes += BLOB_HEADER_SIZE + length; + writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes); + } + + public static class LookupRequest { + public long key; // input: the key to find + public byte[] buffer; // input/output: the buffer to store the blob + public int length; // output: the length of the blob + } + + // This method is for one-off lookup. For repeated lookup, use the version + // accepting LookupRequest to avoid repeated memory allocation. + private LookupRequest mLookupRequest = new LookupRequest(); + public byte[] lookup(long key) throws IOException { + mLookupRequest.key = key; + mLookupRequest.buffer = null; + if (lookup(mLookupRequest)) { + return mLookupRequest.buffer; + } else { + return null; + } + } + + // Returns true if the associated blob for the given key is available. + // The blob is stored in the buffer pointed by req.buffer, and the length + // is in stored in the req.length variable. + // + // The user can input a non-null value in req.buffer, and this method will + // try to use that buffer. If that buffer is not large enough, this method + // will allocate a new buffer and assign it to req.buffer. + // + // This method tries not to throw IOException even if the data file is + // corrupted, but it can still throw IOException if things get strange. + public boolean lookup(LookupRequest req) throws IOException { + // Look up in the active region first. + if (lookupInternal(req.key, mActiveHashStart)) { + if (getBlob(mActiveDataFile, mFileOffset, req)) { + return true; + } + } + + // We want to copy the data from the inactive file to the active file + // if it's available. So we keep the offset of the hash entry so we can + // avoid looking it up again. + int insertOffset = mSlotOffset; + + // Look up in the inactive region. + if (lookupInternal(req.key, mInactiveHashStart)) { + if (getBlob(mInactiveDataFile, mFileOffset, req)) { + // If we don't have enough space to insert this blob into + // the active file, just return it. + if (mActiveBytes + BLOB_HEADER_SIZE + req.length > mMaxBytes + || mActiveEntries * 2 >= mMaxEntries) { + return true; + } + // Otherwise copy it over. + mSlotOffset = insertOffset; + try { + insertInternal(req.key, req.buffer, req.length); + mActiveEntries++; + writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries); + updateIndexHeader(); + } catch (Throwable t) { + Log.e(TAG, "cannot copy over"); + } + return true; + } + } + + return false; + } + + + // Copies the blob for the specified offset in the specified file to + // req.buffer. If req.buffer is null or too small, allocate a buffer and + // assign it to req.buffer. + // Returns false if the blob is not available (either the index file is + // not sync with the data file, or one of them is corrupted). The length + // of the blob is stored in the req.length variable. + private boolean getBlob(RandomAccessFile file, int offset, + LookupRequest req) throws IOException { + byte[] header = mBlobHeader; + long oldPosition = file.getFilePointer(); + try { + file.seek(offset); + if (file.read(header) != BLOB_HEADER_SIZE) { + Log.w(TAG, "cannot read blob header"); + return false; + } + long blobKey = readLong(header, BH_KEY); + if (blobKey == 0) { + return false; // This entry has been cleared. + } + if (blobKey != req.key) { + Log.w(TAG, "blob key does not match: " + blobKey); + return false; + } + int sum = readInt(header, BH_CHECKSUM); + int blobOffset = readInt(header, BH_OFFSET); + if (blobOffset != offset) { + Log.w(TAG, "blob offset does not match: " + blobOffset); + return false; + } + int length = readInt(header, BH_LENGTH); + if (length < 0 || length > mMaxBytes - offset - BLOB_HEADER_SIZE) { + Log.w(TAG, "invalid blob length: " + length); + return false; + } + if (req.buffer == null || req.buffer.length < length) { + req.buffer = new byte[length]; + } + + byte[] blob = req.buffer; + req.length = length; + + if (file.read(blob, 0, length) != length) { + Log.w(TAG, "cannot read blob data"); + return false; + } + if (checkSum(blob, 0, length) != sum) { + Log.w(TAG, "blob checksum does not match: " + sum); + return false; + } + return true; + } catch (Throwable t) { + Log.e(TAG, "getBlob failed.", t); + return false; + } finally { + file.seek(oldPosition); + } + } + + // Tries to look up a key in the specified hash region. + // Returns true if the lookup is successful. + // The slot offset in the index file is saved in mSlotOffset. If the lookup + // is successful, it's the slot found. Otherwise it's the slot suitable for + // insertion. + // If the lookup is successful, the file offset is also saved in + // mFileOffset. + private int mSlotOffset; + private int mFileOffset; + private boolean lookupInternal(long key, int hashStart) { + int slot = (int) (key % mMaxEntries); + if (slot < 0) slot += mMaxEntries; + int slotBegin = slot; + while (true) { + int offset = hashStart + slot * 12; + long candidateKey = mIndexBuffer.getLong(offset); + int candidateOffset = mIndexBuffer.getInt(offset + 8); + if (candidateOffset == 0) { + mSlotOffset = offset; + return false; + } else if (candidateKey == key) { + mSlotOffset = offset; + mFileOffset = candidateOffset; + return true; + } else { + if (++slot >= mMaxEntries) { + slot = 0; + } + if (slot == slotBegin) { + Log.w(TAG, "corrupted index: clear the slot."); + mIndexBuffer.putInt(hashStart + slot * 12 + 8, 0); + } + } + } + } + + public void syncIndex() { + try { + mIndexBuffer.force(); + } catch (Throwable t) { + Log.w(TAG, "sync index failed", t); + } + } + + public void syncAll() { + syncIndex(); + try { + mDataFile0.getFD().sync(); + } catch (Throwable t) { + Log.w(TAG, "sync data file 0 failed", t); + } + try { + mDataFile1.getFD().sync(); + } catch (Throwable t) { + Log.w(TAG, "sync data file 1 failed", t); + } + } + + // This is for testing only. + // + // Returns the active count (mActiveEntries). This also verifies that + // the active count matches matches what's inside the hash region. + int getActiveCount() { + int count = 0; + for (int i = 0; i < mMaxEntries; i++) { + int offset = mActiveHashStart + i * 12; + long candidateKey = mIndexBuffer.getLong(offset); + int candidateOffset = mIndexBuffer.getInt(offset + 8); + if (candidateOffset != 0) ++count; + } + if (count == mActiveEntries) { + return count; + } else { + Log.e(TAG, "wrong active count: " + mActiveEntries + " vs " + count); + return -1; // signal failure. + } + } + + int checkSum(byte[] data) { + mAdler32.reset(); + mAdler32.update(data); + return (int) mAdler32.getValue(); + } + + int checkSum(byte[] data, int offset, int nbytes) { + mAdler32.reset(); + mAdler32.update(data, offset, nbytes); + return (int) mAdler32.getValue(); + } + + static void closeSilently(Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (Throwable t) { + // do nothing + } + } + + static int readInt(byte[] buf, int offset) { + return (buf[offset] & 0xff) + | ((buf[offset + 1] & 0xff) << 8) + | ((buf[offset + 2] & 0xff) << 16) + | ((buf[offset + 3] & 0xff) << 24); + } + + static long readLong(byte[] buf, int offset) { + long result = buf[offset + 7] & 0xff; + for (int i = 6; i >= 0; i--) { + result = (result << 8) | (buf[offset + i] & 0xff); + } + return result; + } + + static void writeInt(byte[] buf, int offset, int value) { + for (int i = 0; i < 4; i++) { + buf[offset + i] = (byte) (value & 0xff); + value >>= 8; + } + } + + static void writeLong(byte[] buf, int offset, long value) { + for (int i = 0; i < 8; i++) { + buf[offset + i] = (byte) (value & 0xff); + value >>= 8; + } + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/Entry.java b/gallerycommon/src/com/android/gallery3d/common/Entry.java new file mode 100644 index 000000000..3f1644e65 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/Entry.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public abstract class Entry { + public static final String[] ID_PROJECTION = { "_id" }; + + public static interface Columns { + public static final String ID = "_id"; + } + + // The primary key of the entry. + @Column("_id") + public long id = 0; + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface Table { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface Column { + String value(); + + boolean indexed() default false; + + boolean fullText() default false; + + String defaultValue() default ""; + + boolean unique() default false; + } + + public void clear() { + id = 0; + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java new file mode 100644 index 000000000..7bf7e431c --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java @@ -0,0 +1,542 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.common; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.util.ArrayList; + +public final class EntrySchema { + @SuppressWarnings("unused") + private static final String TAG = "EntrySchema"; + + public static final int TYPE_STRING = 0; + public static final int TYPE_BOOLEAN = 1; + public static final int TYPE_SHORT = 2; + public static final int TYPE_INT = 3; + public static final int TYPE_LONG = 4; + public static final int TYPE_FLOAT = 5; + public static final int TYPE_DOUBLE = 6; + public static final int TYPE_BLOB = 7; + private static final String SQLITE_TYPES[] = { + "TEXT", "INTEGER", "INTEGER", "INTEGER", "INTEGER", "REAL", "REAL", "NONE" }; + + private static final String FULL_TEXT_INDEX_SUFFIX = "_fulltext"; + + private final String mTableName; + private final ColumnInfo[] mColumnInfo; + private final String[] mProjection; + private final boolean mHasFullTextIndex; + + public EntrySchema(Class<? extends Entry> clazz) { + // Get table and column metadata from reflection. + ColumnInfo[] columns = parseColumnInfo(clazz); + mTableName = parseTableName(clazz); + mColumnInfo = columns; + + // Cache the list of projection columns and check for full-text columns. + String[] projection = {}; + boolean hasFullTextIndex = false; + if (columns != null) { + projection = new String[columns.length]; + for (int i = 0; i != columns.length; ++i) { + ColumnInfo column = columns[i]; + projection[i] = column.name; + if (column.fullText) { + hasFullTextIndex = true; + } + } + } + mProjection = projection; + mHasFullTextIndex = hasFullTextIndex; + } + + public String getTableName() { + return mTableName; + } + + public ColumnInfo[] getColumnInfo() { + return mColumnInfo; + } + + public String[] getProjection() { + return mProjection; + } + + public int getColumnIndex(String columnName) { + for (ColumnInfo column : mColumnInfo) { + if (column.name.equals(columnName)) { + return column.projectionIndex; + } + } + return -1; + } + + public ColumnInfo getColumn(String columnName) { + int index = getColumnIndex(columnName); + return (index < 0) ? null : mColumnInfo[index]; + } + + private void logExecSql(SQLiteDatabase db, String sql) { + db.execSQL(sql); + } + + public <T extends Entry> T cursorToObject(Cursor cursor, T object) { + try { + for (ColumnInfo column : mColumnInfo) { + int columnIndex = column.projectionIndex; + Field field = column.field; + switch (column.type) { + case TYPE_STRING: + field.set(object, cursor.isNull(columnIndex) + ? null + : cursor.getString(columnIndex)); + break; + case TYPE_BOOLEAN: + field.setBoolean(object, cursor.getShort(columnIndex) == 1); + break; + case TYPE_SHORT: + field.setShort(object, cursor.getShort(columnIndex)); + break; + case TYPE_INT: + field.setInt(object, cursor.getInt(columnIndex)); + break; + case TYPE_LONG: + field.setLong(object, cursor.getLong(columnIndex)); + break; + case TYPE_FLOAT: + field.setFloat(object, cursor.getFloat(columnIndex)); + break; + case TYPE_DOUBLE: + field.setDouble(object, cursor.getDouble(columnIndex)); + break; + case TYPE_BLOB: + field.set(object, cursor.isNull(columnIndex) + ? null + : cursor.getBlob(columnIndex)); + break; + } + } + return object; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private void setIfNotNull(Field field, Object object, Object value) + throws IllegalAccessException { + if (value != null) field.set(object, value); + } + + /** + * Converts the ContentValues to the object. The ContentValues may not + * contain values for all the fields in the object. + */ + public <T extends Entry> T valuesToObject(ContentValues values, T object) { + try { + for (ColumnInfo column : mColumnInfo) { + String columnName = column.name; + Field field = column.field; + switch (column.type) { + case TYPE_STRING: + setIfNotNull(field, object, values.getAsString(columnName)); + break; + case TYPE_BOOLEAN: + setIfNotNull(field, object, values.getAsBoolean(columnName)); + break; + case TYPE_SHORT: + setIfNotNull(field, object, values.getAsShort(columnName)); + break; + case TYPE_INT: + setIfNotNull(field, object, values.getAsInteger(columnName)); + break; + case TYPE_LONG: + setIfNotNull(field, object, values.getAsLong(columnName)); + break; + case TYPE_FLOAT: + setIfNotNull(field, object, values.getAsFloat(columnName)); + break; + case TYPE_DOUBLE: + setIfNotNull(field, object, values.getAsDouble(columnName)); + break; + case TYPE_BLOB: + setIfNotNull(field, object, values.getAsByteArray(columnName)); + break; + } + } + return object; + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public void objectToValues(Entry object, ContentValues values) { + try { + for (ColumnInfo column : mColumnInfo) { + String columnName = column.name; + Field field = column.field; + switch (column.type) { + case TYPE_STRING: + values.put(columnName, (String) field.get(object)); + break; + case TYPE_BOOLEAN: + values.put(columnName, field.getBoolean(object)); + break; + case TYPE_SHORT: + values.put(columnName, field.getShort(object)); + break; + case TYPE_INT: + values.put(columnName, field.getInt(object)); + break; + case TYPE_LONG: + values.put(columnName, field.getLong(object)); + break; + case TYPE_FLOAT: + values.put(columnName, field.getFloat(object)); + break; + case TYPE_DOUBLE: + values.put(columnName, field.getDouble(object)); + break; + case TYPE_BLOB: + values.put(columnName, (byte[]) field.get(object)); + break; + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public String toDebugString(Entry entry) { + try { + StringBuilder sb = new StringBuilder(); + sb.append("ID=").append(entry.id); + for (ColumnInfo column : mColumnInfo) { + String columnName = column.name; + Field field = column.field; + Object value = field.get(entry); + sb.append(" ").append(columnName).append("=") + .append((value == null) ? "null" : value.toString()); + } + return sb.toString(); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public String toDebugString(Entry entry, String... columnNames) { + try { + StringBuilder sb = new StringBuilder(); + sb.append("ID=").append(entry.id); + for (String columnName : columnNames) { + ColumnInfo column = getColumn(columnName); + Field field = column.field; + Object value = field.get(entry); + sb.append(" ").append(columnName).append("=") + .append((value == null) ? "null" : value.toString()); + } + return sb.toString(); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + public Cursor queryAll(SQLiteDatabase db) { + return db.query(mTableName, mProjection, null, null, null, null, null); + } + + public boolean queryWithId(SQLiteDatabase db, long id, Entry entry) { + Cursor cursor = db.query(mTableName, mProjection, "_id=?", + new String[] {Long.toString(id)}, null, null, null); + boolean success = false; + if (cursor.moveToFirst()) { + cursorToObject(cursor, entry); + success = true; + } + cursor.close(); + return success; + } + + public long insertOrReplace(SQLiteDatabase db, Entry entry) { + ContentValues values = new ContentValues(); + objectToValues(entry, values); + if (entry.id == 0) { + values.remove("_id"); + } + long id = db.replace(mTableName, "_id", values); + entry.id = id; + return id; + } + + public boolean deleteWithId(SQLiteDatabase db, long id) { + return db.delete(mTableName, "_id=?", new String[] { Long.toString(id) }) == 1; + } + + public void createTables(SQLiteDatabase db) { + // Wrapped class must have a @Table.Definition. + String tableName = mTableName; + Utils.assertTrue(tableName != null); + + // Add the CREATE TABLE statement for the main table. + StringBuilder sql = new StringBuilder("CREATE TABLE "); + sql.append(tableName); + sql.append(" (_id INTEGER PRIMARY KEY AUTOINCREMENT"); + StringBuilder unique = new StringBuilder(); + for (ColumnInfo column : mColumnInfo) { + if (!column.isId()) { + sql.append(','); + sql.append(column.name); + sql.append(' '); + sql.append(SQLITE_TYPES[column.type]); + if (!TextUtils.isEmpty(column.defaultValue)) { + sql.append(" DEFAULT "); + sql.append(column.defaultValue); + } + if (column.unique) { + if (unique.length() == 0) { + unique.append(column.name); + } else { + unique.append(',').append(column.name); + } + } + } + } + if (unique.length() > 0) { + sql.append(",UNIQUE(").append(unique).append(')'); + } + sql.append(");"); + logExecSql(db, sql.toString()); + sql.setLength(0); + + // Create indexes for all indexed columns. + for (ColumnInfo column : mColumnInfo) { + // Create an index on the indexed columns. + if (column.indexed) { + sql.append("CREATE INDEX "); + sql.append(tableName); + sql.append("_index_"); + sql.append(column.name); + sql.append(" ON "); + sql.append(tableName); + sql.append(" ("); + sql.append(column.name); + sql.append(");"); + logExecSql(db, sql.toString()); + sql.setLength(0); + } + } + + if (mHasFullTextIndex) { + // Add an FTS virtual table if using full-text search. + String ftsTableName = tableName + FULL_TEXT_INDEX_SUFFIX; + sql.append("CREATE VIRTUAL TABLE "); + sql.append(ftsTableName); + sql.append(" USING FTS3 (_id INTEGER PRIMARY KEY"); + for (ColumnInfo column : mColumnInfo) { + if (column.fullText) { + // Add the column to the FTS table. + String columnName = column.name; + sql.append(','); + sql.append(columnName); + sql.append(" TEXT"); + } + } + sql.append(");"); + logExecSql(db, sql.toString()); + sql.setLength(0); + + // Build an insert statement that will automatically keep the FTS + // table in sync. + StringBuilder insertSql = new StringBuilder("INSERT OR REPLACE INTO "); + insertSql.append(ftsTableName); + insertSql.append(" (_id"); + for (ColumnInfo column : mColumnInfo) { + if (column.fullText) { + insertSql.append(','); + insertSql.append(column.name); + } + } + insertSql.append(") VALUES (new._id"); + for (ColumnInfo column : mColumnInfo) { + if (column.fullText) { + insertSql.append(",new."); + insertSql.append(column.name); + } + } + insertSql.append(");"); + String insertSqlString = insertSql.toString(); + + // Add an insert trigger. + sql.append("CREATE TRIGGER "); + sql.append(tableName); + sql.append("_insert_trigger AFTER INSERT ON "); + sql.append(tableName); + sql.append(" FOR EACH ROW BEGIN "); + sql.append(insertSqlString); + sql.append("END;"); + logExecSql(db, sql.toString()); + sql.setLength(0); + + // Add an update trigger. + sql.append("CREATE TRIGGER "); + sql.append(tableName); + sql.append("_update_trigger AFTER UPDATE ON "); + sql.append(tableName); + sql.append(" FOR EACH ROW BEGIN "); + sql.append(insertSqlString); + sql.append("END;"); + logExecSql(db, sql.toString()); + sql.setLength(0); + + // Add a delete trigger. + sql.append("CREATE TRIGGER "); + sql.append(tableName); + sql.append("_delete_trigger AFTER DELETE ON "); + sql.append(tableName); + sql.append(" FOR EACH ROW BEGIN DELETE FROM "); + sql.append(ftsTableName); + sql.append(" WHERE _id = old._id; END;"); + logExecSql(db, sql.toString()); + sql.setLength(0); + } + } + + public void dropTables(SQLiteDatabase db) { + String tableName = mTableName; + StringBuilder sql = new StringBuilder("DROP TABLE IF EXISTS "); + sql.append(tableName); + sql.append(';'); + logExecSql(db, sql.toString()); + sql.setLength(0); + + if (mHasFullTextIndex) { + sql.append("DROP TABLE IF EXISTS "); + sql.append(tableName); + sql.append(FULL_TEXT_INDEX_SUFFIX); + sql.append(';'); + logExecSql(db, sql.toString()); + } + + } + + public void deleteAll(SQLiteDatabase db) { + StringBuilder sql = new StringBuilder("DELETE FROM "); + sql.append(mTableName); + sql.append(";"); + logExecSql(db, sql.toString()); + } + + private String parseTableName(Class<? extends Object> clazz) { + // Check for a table annotation. + Entry.Table table = clazz.getAnnotation(Entry.Table.class); + if (table == null) { + return null; + } + + // Return the table name. + return table.value(); + } + + private ColumnInfo[] parseColumnInfo(Class<? extends Object> clazz) { + ArrayList<ColumnInfo> columns = new ArrayList<ColumnInfo>(); + while (clazz != null) { + parseColumnInfo(clazz, columns); + clazz = clazz.getSuperclass(); + } + + // Return a list. + ColumnInfo[] columnList = new ColumnInfo[columns.size()]; + columns.toArray(columnList); + return columnList; + } + + private void parseColumnInfo(Class<? extends Object> clazz, ArrayList<ColumnInfo> columns) { + // Gather metadata from each annotated field. + Field[] fields = clazz.getDeclaredFields(); // including non-public fields + for (int i = 0; i != fields.length; ++i) { + // Get column metadata from the annotation. + Field field = fields[i]; + Entry.Column info = ((AnnotatedElement) field).getAnnotation(Entry.Column.class); + if (info == null) continue; + + // Determine the field type. + int type; + Class<?> fieldType = field.getType(); + if (fieldType == String.class) { + type = TYPE_STRING; + } else if (fieldType == boolean.class) { + type = TYPE_BOOLEAN; + } else if (fieldType == short.class) { + type = TYPE_SHORT; + } else if (fieldType == int.class) { + type = TYPE_INT; + } else if (fieldType == long.class) { + type = TYPE_LONG; + } else if (fieldType == float.class) { + type = TYPE_FLOAT; + } else if (fieldType == double.class) { + type = TYPE_DOUBLE; + } else if (fieldType == byte[].class) { + type = TYPE_BLOB; + } else { + throw new IllegalArgumentException( + "Unsupported field type for column: " + fieldType.getName()); + } + + // Add the column to the array. + int index = columns.size(); + columns.add(new ColumnInfo(info.value(), type, info.indexed(), info.unique(), + info.fullText(), info.defaultValue(), field, index)); + } + } + + public static final class ColumnInfo { + private static final String ID_KEY = "_id"; + + public final String name; + public final int type; + public final boolean indexed; + public final boolean unique; + public final boolean fullText; + public final String defaultValue; + public final Field field; + public final int projectionIndex; + + public ColumnInfo(String name, int type, boolean indexed, boolean unique, + boolean fullText, String defaultValue, Field field, int projectionIndex) { + this.name = name.toLowerCase(); + this.type = type; + this.indexed = indexed; + this.unique = unique; + this.fullText = fullText; + this.defaultValue = defaultValue; + this.field = field; + this.projectionIndex = projectionIndex; + + field.setAccessible(true); // in order to set non-public fields + } + + public boolean isId() { + return ID_KEY.equals(name); + } + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/FileCache.java b/gallerycommon/src/com/android/gallery3d/common/FileCache.java new file mode 100644 index 000000000..d7deda6fa --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/FileCache.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.common; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import com.android.gallery3d.common.Entry.Table; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; + +public class FileCache implements Closeable { + private static final int LRU_CAPACITY = 4; + private static final int MAX_DELETE_COUNT = 16; + + private static final String TAG = "FileCache"; + private static final String TABLE_NAME = FileEntry.SCHEMA.getTableName(); + private static final String FILE_PREFIX = "download"; + private static final String FILE_POSTFIX = ".tmp"; + + private static final String QUERY_WHERE = + FileEntry.Columns.HASH_CODE + "=? AND " + FileEntry.Columns.CONTENT_URL + "=?"; + private static final String ID_WHERE = FileEntry.Columns.ID + "=?"; + private static final String[] PROJECTION_SIZE_SUM = + {String.format("sum(%s)", FileEntry.Columns.SIZE)}; + private static final String FREESPACE_PROJECTION[] = { + FileEntry.Columns.ID, FileEntry.Columns.FILENAME, + FileEntry.Columns.CONTENT_URL, FileEntry.Columns.SIZE}; + private static final String FREESPACE_ORDER_BY = + String.format("%s ASC", FileEntry.Columns.LAST_ACCESS); + + private final LruCache<String, CacheEntry> mEntryMap = + new LruCache<String, CacheEntry>(LRU_CAPACITY); + + private File mRootDir; + private long mCapacity; + private boolean mInitialized = false; + private long mTotalBytes; + + private DatabaseHelper mDbHelper; + + public static final class CacheEntry { + private long id; + public String contentUrl; + public File cacheFile; + + private CacheEntry(long id, String contentUrl, File cacheFile) { + this.id = id; + this.contentUrl = contentUrl; + this.cacheFile = cacheFile; + } + } + + public static void deleteFiles(Context context, File rootDir, String dbName) { + try { + context.getDatabasePath(dbName).delete(); + File[] files = rootDir.listFiles(); + if (files == null) return; + for (File file : rootDir.listFiles()) { + String name = file.getName(); + if (file.isFile() && name.startsWith(FILE_PREFIX) + && name.endsWith(FILE_POSTFIX)) file.delete(); + } + } catch (Throwable t) { + Log.w(TAG, "cannot reset database", t); + } + } + + public FileCache(Context context, File rootDir, String dbName, long capacity) { + mRootDir = Utils.checkNotNull(rootDir); + mCapacity = capacity; + mDbHelper = new DatabaseHelper(context, dbName); + } + + @Override + public void close() { + mDbHelper.close(); + } + + public void store(String downloadUrl, File file) { + if (!mInitialized) initialize(); + + Utils.assertTrue(file.getParentFile().equals(mRootDir)); + FileEntry entry = new FileEntry(); + entry.hashCode = Utils.crc64Long(downloadUrl); + entry.contentUrl = downloadUrl; + entry.filename = file.getName(); + entry.size = file.length(); + entry.lastAccess = System.currentTimeMillis(); + if (entry.size >= mCapacity) { + file.delete(); + throw new IllegalArgumentException("file too large: " + entry.size); + } + synchronized (this) { + FileEntry original = queryDatabase(downloadUrl); + if (original != null) { + file.delete(); + entry.filename = original.filename; + entry.size = original.size; + } else { + mTotalBytes += entry.size; + } + FileEntry.SCHEMA.insertOrReplace( + mDbHelper.getWritableDatabase(), entry); + if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT); + } + } + + public CacheEntry lookup(String downloadUrl) { + if (!mInitialized) initialize(); + CacheEntry entry; + synchronized (mEntryMap) { + entry = mEntryMap.get(downloadUrl); + } + + if (entry != null) { + synchronized (this) { + updateLastAccess(entry.id); + } + return entry; + } + + synchronized (this) { + FileEntry file = queryDatabase(downloadUrl); + if (file == null) return null; + entry = new CacheEntry( + file.id, downloadUrl, new File(mRootDir, file.filename)); + if (!entry.cacheFile.isFile()) { // file has been removed + try { + mDbHelper.getWritableDatabase().delete( + TABLE_NAME, ID_WHERE, new String[] {String.valueOf(file.id)}); + mTotalBytes -= file.size; + } catch (Throwable t) { + Log.w(TAG, "cannot delete entry: " + file.filename, t); + } + return null; + } + synchronized (mEntryMap) { + mEntryMap.put(downloadUrl, entry); + } + return entry; + } + } + + private FileEntry queryDatabase(String downloadUrl) { + long hash = Utils.crc64Long(downloadUrl); + String whereArgs[] = new String[] {String.valueOf(hash), downloadUrl}; + Cursor cursor = mDbHelper.getReadableDatabase().query(TABLE_NAME, + FileEntry.SCHEMA.getProjection(), + QUERY_WHERE, whereArgs, null, null, null); + try { + if (!cursor.moveToNext()) return null; + FileEntry entry = new FileEntry(); + FileEntry.SCHEMA.cursorToObject(cursor, entry); + updateLastAccess(entry.id); + return entry; + } finally { + cursor.close(); + } + } + + private void updateLastAccess(long id) { + ContentValues values = new ContentValues(); + values.put(FileEntry.Columns.LAST_ACCESS, System.currentTimeMillis()); + mDbHelper.getWritableDatabase().update(TABLE_NAME, + values, ID_WHERE, new String[] {String.valueOf(id)}); + } + + public File createFile() throws IOException { + return File.createTempFile(FILE_PREFIX, FILE_POSTFIX, mRootDir); + } + + private synchronized void initialize() { + if (mInitialized) return; + + if (!mRootDir.isDirectory()) { + mRootDir.mkdirs(); + if (!mRootDir.isDirectory()) { + throw new RuntimeException("cannot create: " + mRootDir.getAbsolutePath()); + } + } + + Cursor cursor = mDbHelper.getReadableDatabase().query( + TABLE_NAME, PROJECTION_SIZE_SUM, + null, null, null, null, null); + try { + if (cursor.moveToNext()) mTotalBytes = cursor.getLong(0); + } finally { + cursor.close(); + } + if (mTotalBytes > mCapacity) freeSomeSpaceIfNeed(MAX_DELETE_COUNT); + + // Mark initialized when everything above went through. If an exception was thrown, + // initialize() will be retried later. + mInitialized = true; + } + + private void freeSomeSpaceIfNeed(int maxDeleteFileCount) { + Cursor cursor = mDbHelper.getReadableDatabase().query( + TABLE_NAME, FREESPACE_PROJECTION, + null, null, null, null, FREESPACE_ORDER_BY); + try { + while (maxDeleteFileCount > 0 + && mTotalBytes > mCapacity && cursor.moveToNext()) { + long id = cursor.getLong(0); + String path = cursor.getString(1); + String url = cursor.getString(2); + long size = cursor.getLong(3); + + synchronized (mEntryMap) { + // if some one still uses it + if (mEntryMap.containsKey(url)) continue; + } + + --maxDeleteFileCount; + if (new File(mRootDir, path).delete()) { + mTotalBytes -= size; + mDbHelper.getWritableDatabase().delete(TABLE_NAME, + ID_WHERE, new String[]{String.valueOf(id)}); + } else { + Log.w(TAG, "unable to delete file: " + path); + } + } + } finally { + cursor.close(); + } + } + + @Table("files") + private static class FileEntry extends Entry { + public static final EntrySchema SCHEMA = new EntrySchema(FileEntry.class); + + public interface Columns extends Entry.Columns { + public static final String HASH_CODE = "hash_code"; + public static final String CONTENT_URL = "content_url"; + public static final String FILENAME = "filename"; + public static final String SIZE = "size"; + public static final String LAST_ACCESS = "last_access"; + } + + @Column(value = Columns.HASH_CODE, indexed = true) + public long hashCode; + + @Column(Columns.CONTENT_URL) + public String contentUrl; + + @Column(Columns.FILENAME) + public String filename; + + @Column(Columns.SIZE) + public long size; + + @Column(value = Columns.LAST_ACCESS, indexed = true) + public long lastAccess; + + @Override + public String toString() { + return new StringBuilder() + .append("hash_code: ").append(hashCode).append(", ") + .append("content_url").append(contentUrl).append(", ") + .append("last_access").append(lastAccess).append(", ") + .append("filename").append(filename).toString(); + } + } + + private final class DatabaseHelper extends SQLiteOpenHelper { + public static final int DATABASE_VERSION = 1; + + public DatabaseHelper(Context context, String dbName) { + super(context, dbName, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + FileEntry.SCHEMA.createTables(db); + + // delete old files + for (File file : mRootDir.listFiles()) { + if (!file.delete()) { + Log.w(TAG, "fail to remove: " + file.getAbsolutePath()); + } + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + //reset everything + FileEntry.SCHEMA.dropTables(db); + onCreate(db); + } + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java b/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java new file mode 100644 index 000000000..2847e57ce --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.common; + +import java.io.IOException; +import java.io.InputStream; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; + +/** + * MD5-based digest Wrapper. + */ +public class Fingerprint { + // Instance of the MessageDigest using our specified digest algorithm. + private static final MessageDigest DIGESTER; + + /** + * Name of the digest algorithm we use in {@link java.security.MessageDigest} + */ + private static final String DIGEST_MD5 = "md5"; + + // Version 1 streamId prefix. + // Hard coded stream id length limit is 40-chars. Don't ask! + private static final String STREAM_ID_CS_PREFIX = "cs_01_"; + + // 16 bytes for 128-bit fingerprint + private static final int FINGERPRINT_BYTE_LENGTH; + + // length of prefix + 32 hex chars for 128-bit fingerprint + private static final int STREAM_ID_CS_01_LENGTH; + + static { + try { + DIGESTER = MessageDigest.getInstance(DIGEST_MD5); + FINGERPRINT_BYTE_LENGTH = DIGESTER.getDigestLength(); + STREAM_ID_CS_01_LENGTH = STREAM_ID_CS_PREFIX.length() + + (FINGERPRINT_BYTE_LENGTH * 2); + } catch (NoSuchAlgorithmException e) { + // can't continue, but really shouldn't happen + throw new IllegalStateException(e); + } + } + + // md5 digest bytes. + private final byte[] mMd5Digest; + + /** + * Creates a new Fingerprint. + */ + public Fingerprint(byte[] bytes) { + if ((bytes == null) || (bytes.length != FINGERPRINT_BYTE_LENGTH)) { + throw new IllegalArgumentException(); + } + mMd5Digest = bytes; + } + + /** + * Creates a Fingerprint based on the contents of a file. + * + * Note that this will close() stream after calculating the digest. + * @param byteCount length of original data will be stored at byteCount[0] as a side product + * of the fingerprint calculation + */ + public static Fingerprint fromInputStream(InputStream stream, long[] byteCount) + throws IOException { + DigestInputStream in = null; + long count = 0; + try { + in = new DigestInputStream(stream, DIGESTER); + byte[] bytes = new byte[8192]; + while (true) { + // scan through file to compute a fingerprint. + int n = in.read(bytes); + if (n < 0) break; + count += n; + } + } finally { + if (in != null) in.close(); + } + if ((byteCount != null) && (byteCount.length > 0)) byteCount[0] = count; + return new Fingerprint(in.getMessageDigest().digest()); + } + + /** + * Decodes a string stream id to a 128-bit fingerprint. + */ + public static Fingerprint fromStreamId(String streamId) { + if ((streamId == null) + || !streamId.startsWith(STREAM_ID_CS_PREFIX) + || (streamId.length() != STREAM_ID_CS_01_LENGTH)) { + throw new IllegalArgumentException("bad streamId: " + streamId); + } + + // decode the hex bytes of the fingerprint portion + byte[] bytes = new byte[FINGERPRINT_BYTE_LENGTH]; + int byteIdx = 0; + for (int idx = STREAM_ID_CS_PREFIX.length(); idx < STREAM_ID_CS_01_LENGTH; + idx += 2) { + int value = (toDigit(streamId, idx) << 4) | toDigit(streamId, idx + 1); + bytes[byteIdx++] = (byte) (value & 0xff); + } + return new Fingerprint(bytes); + } + + /** + * Scans a list of strings for a valid streamId. + * + * @param streamIdList list of stream id's to be scanned + * @return valid fingerprint or null if it can't be found + */ + public static Fingerprint extractFingerprint(List<String> streamIdList) { + for (String streamId : streamIdList) { + if (streamId.startsWith(STREAM_ID_CS_PREFIX)) { + return fromStreamId(streamId); + } + } + return null; + } + + /** + * Encodes a 128-bit fingerprint as a string stream id. + * + * Stream id string is limited to 40 characters, which could be digits, lower case ASCII and + * underscores. + */ + public String toStreamId() { + StringBuilder streamId = new StringBuilder(STREAM_ID_CS_PREFIX); + appendHexFingerprint(streamId, mMd5Digest); + return streamId.toString(); + } + + public byte[] getBytes() { + return mMd5Digest; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Fingerprint)) return false; + Fingerprint other = (Fingerprint) obj; + return Arrays.equals(mMd5Digest, other.mMd5Digest); + } + + public boolean equals(byte[] md5Digest) { + return Arrays.equals(mMd5Digest, md5Digest); + } + + @Override + public int hashCode() { + return Arrays.hashCode(mMd5Digest); + } + + // Utility methods. + + private static int toDigit(String streamId, int index) { + int digit = Character.digit(streamId.charAt(index), 16); + if (digit < 0) { + throw new IllegalArgumentException("illegal hex digit in " + streamId); + } + return digit; + } + + private static void appendHexFingerprint(StringBuilder sb, byte[] bytes) { + for (int idx = 0; idx < FINGERPRINT_BYTE_LENGTH; idx++) { + int value = bytes[idx]; + sb.append(Integer.toHexString((value >> 4) & 0x0f)); + sb.append(Integer.toHexString(value& 0x0f)); + } + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java b/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java new file mode 100644 index 000000000..18b7a8875 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.common; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; + +import org.apache.http.HttpVersion; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.params.ConnManagerParams; +import org.apache.http.params.CoreProtocolPNames; +import org.apache.http.params.HttpParams; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Constructs {@link HttpClient} instances and isolates client code from API + * level differences. + */ +public final class HttpClientFactory { + // TODO: migrate GDataClient to use this util method instead of apache's + // DefaultHttpClient. + /** + * Creates an HttpClient with the userAgent string constructed from the + * package name contained in the context. + * @return the client + */ + public static HttpClient newHttpClient(Context context) { + return HttpClientFactory.newHttpClient(getUserAgent(context)); + } + + /** + * Creates an HttpClient with the specified userAgent string. + * @param userAgent the userAgent string + * @return the client + */ + public static HttpClient newHttpClient(String userAgent) { + // AndroidHttpClient is available on all platform releases, + // but is hidden until API Level 8 + try { + Class<?> clazz = Class.forName("android.net.http.AndroidHttpClient"); + Method newInstance = clazz.getMethod("newInstance", String.class); + Object instance = newInstance.invoke(null, userAgent); + + HttpClient client = (HttpClient) instance; + + // ensure we default to HTTP 1.1 + HttpParams params = client.getParams(); + params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1); + + // AndroidHttpClient sets these two parameters thusly by default: + // HttpConnectionParams.setSoTimeout(params, 60 * 1000); + // HttpConnectionParams.setConnectionTimeout(params, 60 * 1000); + + // however it doesn't set this one... + ConnManagerParams.setTimeout(params, 60 * 1000); + + return client; + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** + * Closes an HttpClient. + */ + public static void close(HttpClient client) { + // AndroidHttpClient is available on all platform releases, + // but is hidden until API Level 8 + try { + Class<?> clazz = client.getClass(); + Method method = clazz.getMethod("close", (Class<?>[]) null); + method.invoke(client, (Object[]) null); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private static String sUserAgent = null; + + private static String getUserAgent(Context context) { + if (sUserAgent == null) { + PackageInfo pi; + try { + pi = context.getPackageManager().getPackageInfo( + context.getPackageName(), 0); + } catch (NameNotFoundException e) { + throw new IllegalStateException("getPackageInfo failed"); + } + sUserAgent = String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s", + pi.packageName, + pi.versionName, + Build.BRAND, + Build.DEVICE, + Build.MODEL, + Build.ID, + Build.VERSION.SDK_INT, + Build.VERSION.RELEASE, + Build.VERSION.INCREMENTAL); + } + return sUserAgent; + } + + private HttpClientFactory() { + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/LruCache.java b/gallerycommon/src/com/android/gallery3d/common/LruCache.java new file mode 100644 index 000000000..81dabf773 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/LruCache.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.common; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * An LRU cache which stores recently inserted entries and all entries ever + * inserted which still has a strong reference elsewhere. + */ +public class LruCache<K, V> { + + private final HashMap<K, V> mLruMap; + private final HashMap<K, Entry<K, V>> mWeakMap = + new HashMap<K, Entry<K, V>>(); + private ReferenceQueue<V> mQueue = new ReferenceQueue<V>(); + + @SuppressWarnings("serial") + public LruCache(final int capacity) { + mLruMap = new LinkedHashMap<K, V>(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { + return size() > capacity; + } + }; + } + + private static class Entry<K, V> extends WeakReference<V> { + K mKey; + + public Entry(K key, V value, ReferenceQueue<V> queue) { + super(value, queue); + mKey = key; + } + } + + @SuppressWarnings("unchecked") + private void cleanUpWeakMap() { + Entry<K, V> entry = (Entry<K, V>) mQueue.poll(); + while (entry != null) { + mWeakMap.remove(entry.mKey); + entry = (Entry<K, V>) mQueue.poll(); + } + } + + public synchronized boolean containsKey(K key) { + cleanUpWeakMap(); + return mWeakMap.containsKey(key); + } + + public synchronized V put(K key, V value) { + cleanUpWeakMap(); + mLruMap.put(key, value); + Entry<K, V> entry = mWeakMap.put( + key, new Entry<K, V>(key, value, mQueue)); + return entry == null ? null : entry.get(); + } + + public synchronized V get(K key) { + cleanUpWeakMap(); + V value = mLruMap.get(key); + if (value != null) return value; + Entry<K, V> entry = mWeakMap.get(key); + return entry == null ? null : entry.get(); + } + + public synchronized void clear() { + mLruMap.clear(); + mWeakMap.clear(); + mQueue = new ReferenceQueue<V>(); + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/OverScroller.java b/gallerycommon/src/com/android/gallery3d/common/OverScroller.java new file mode 100644 index 000000000..1ab7953d2 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/OverScroller.java @@ -0,0 +1,958 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.hardware.SensorManager; +import android.util.FloatMath; +import android.util.Log; +import android.view.ViewConfiguration; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +/** + * This class encapsulates scrolling with the ability to overshoot the bounds + * of a scrolling operation. This class is a drop-in replacement for + * {@link android.widget.Scroller} in most cases. + */ +public class OverScroller { + private int mMode; + + private final SplineOverScroller mScrollerX; + private final SplineOverScroller mScrollerY; + + private Interpolator mInterpolator; + + private final boolean mFlywheel; + + private static final int DEFAULT_DURATION = 250; + private static final int SCROLL_MODE = 0; + private static final int FLING_MODE = 1; + + /** + * Creates an OverScroller with a viscous fluid scroll interpolator and flywheel. + * @param context + */ + public OverScroller(Context context) { + this(context, null); + } + + /** + * Creates an OverScroller with flywheel enabled. + * @param context The context of this application. + * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will + * be used. + */ + public OverScroller(Context context, Interpolator interpolator) { + this(context, interpolator, true); + } + + /** + * Creates an OverScroller. + * @param context The context of this application. + * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will + * be used. + * @param flywheel If true, successive fling motions will keep on increasing scroll speed. + * @hide + */ + public OverScroller(Context context, Interpolator interpolator, boolean flywheel) { + mInterpolator = interpolator; + mFlywheel = flywheel; + mScrollerX = new SplineOverScroller(); + mScrollerY = new SplineOverScroller(); + + SplineOverScroller.initFromContext(context); + } + + /** + * Creates an OverScroller with flywheel enabled. + * @param context The context of this application. + * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will + * be used. + * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the + * velocity which is preserved in the bounce when the horizontal edge is reached. A null value + * means no bounce. This behavior is no longer supported and this coefficient has no effect. + * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This + * behavior is no longer supported and this coefficient has no effect. + * !deprecated Use {!link #OverScroller(Context, Interpolator, boolean)} instead. + */ + public OverScroller(Context context, Interpolator interpolator, + float bounceCoefficientX, float bounceCoefficientY) { + this(context, interpolator, true); + } + + /** + * Creates an OverScroller. + * @param context The context of this application. + * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will + * be used. + * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the + * velocity which is preserved in the bounce when the horizontal edge is reached. A null value + * means no bounce. This behavior is no longer supported and this coefficient has no effect. + * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This + * behavior is no longer supported and this coefficient has no effect. + * @param flywheel If true, successive fling motions will keep on increasing scroll speed. + * !deprecated Use {!link OverScroller(Context, Interpolator, boolean)} instead. + */ + public OverScroller(Context context, Interpolator interpolator, + float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) { + this(context, interpolator, flywheel); + } + + void setInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + } + + /** + * The amount of friction applied to flings. The default value + * is {@link ViewConfiguration#getScrollFriction}. + * + * @param friction A scalar dimension-less value representing the coefficient of + * friction. + */ + public final void setFriction(float friction) { + mScrollerX.setFriction(friction); + mScrollerY.setFriction(friction); + } + + /** + * + * Returns whether the scroller has finished scrolling. + * + * @return True if the scroller has finished scrolling, false otherwise. + */ + public final boolean isFinished() { + return mScrollerX.mFinished && mScrollerY.mFinished; + } + + /** + * Force the finished field to a particular value. Contrary to + * {@link #abortAnimation()}, forcing the animation to finished + * does NOT cause the scroller to move to the final x and y + * position. + * + * @param finished The new finished value. + */ + public final void forceFinished(boolean finished) { + mScrollerX.mFinished = mScrollerY.mFinished = finished; + } + + /** + * Returns the current X offset in the scroll. + * + * @return The new X offset as an absolute distance from the origin. + */ + public final int getCurrX() { + return mScrollerX.mCurrentPosition; + } + + /** + * Returns the current Y offset in the scroll. + * + * @return The new Y offset as an absolute distance from the origin. + */ + public final int getCurrY() { + return mScrollerY.mCurrentPosition; + } + + /** + * Returns the absolute value of the current velocity. + * + * @return The original velocity less the deceleration, norm of the X and Y velocity vector. + */ + public float getCurrVelocity() { + float squaredNorm = mScrollerX.mCurrVelocity * mScrollerX.mCurrVelocity; + squaredNorm += mScrollerY.mCurrVelocity * mScrollerY.mCurrVelocity; + return FloatMath.sqrt(squaredNorm); + } + + /** + * Returns the start X offset in the scroll. + * + * @return The start X offset as an absolute distance from the origin. + */ + public final int getStartX() { + return mScrollerX.mStart; + } + + /** + * Returns the start Y offset in the scroll. + * + * @return The start Y offset as an absolute distance from the origin. + */ + public final int getStartY() { + return mScrollerY.mStart; + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final X offset as an absolute distance from the origin. + */ + public final int getFinalX() { + return mScrollerX.mFinal; + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final Y offset as an absolute distance from the origin. + */ + public final int getFinalY() { + return mScrollerY.mFinal; + } + + /** + * Returns how long the scroll event will take, in milliseconds. + * + * @return The duration of the scroll in milliseconds. + * + * @hide Pending removal once nothing depends on it + * @deprecated OverScrollers don't necessarily have a fixed duration. + * This function will lie to the best of its ability. + */ + @Deprecated + public final int getDuration() { + return Math.max(mScrollerX.mDuration, mScrollerY.mDuration); + } + + /** + * Extend the scroll animation. This allows a running animation to scroll + * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. + * + * @param extend Additional time to scroll in milliseconds. + * @see #setFinalX(int) + * @see #setFinalY(int) + * + * @hide Pending removal once nothing depends on it + * @deprecated OverScrollers don't necessarily have a fixed duration. + * Instead of setting a new final position and extending + * the duration of an existing scroll, use startScroll + * to begin a new animation. + */ + @Deprecated + public void extendDuration(int extend) { + mScrollerX.extendDuration(extend); + mScrollerY.extendDuration(extend); + } + + /** + * Sets the final position (X) for this scroller. + * + * @param newX The new X offset as an absolute distance from the origin. + * @see #extendDuration(int) + * @see #setFinalY(int) + * + * @hide Pending removal once nothing depends on it + * @deprecated OverScroller's final position may change during an animation. + * Instead of setting a new final position and extending + * the duration of an existing scroll, use startScroll + * to begin a new animation. + */ + @Deprecated + public void setFinalX(int newX) { + mScrollerX.setFinalPosition(newX); + } + + /** + * Sets the final position (Y) for this scroller. + * + * @param newY The new Y offset as an absolute distance from the origin. + * @see #extendDuration(int) + * @see #setFinalX(int) + * + * @hide Pending removal once nothing depends on it + * @deprecated OverScroller's final position may change during an animation. + * Instead of setting a new final position and extending + * the duration of an existing scroll, use startScroll + * to begin a new animation. + */ + @Deprecated + public void setFinalY(int newY) { + mScrollerY.setFinalPosition(newY); + } + + /** + * Call this when you want to know the new location. If it returns true, the + * animation is not yet finished. + */ + public boolean computeScrollOffset() { + if (isFinished()) { + return false; + } + + switch (mMode) { + case SCROLL_MODE: + long time = AnimationUtils.currentAnimationTimeMillis(); + // Any scroller can be used for time, since they were started + // together in scroll mode. We use X here. + final long elapsedTime = time - mScrollerX.mStartTime; + + final int duration = mScrollerX.mDuration; + if (elapsedTime < duration) { + float q = (float) (elapsedTime) / duration; + + if (mInterpolator == null) { + q = Scroller.viscousFluid(q); + } else { + q = mInterpolator.getInterpolation(q); + } + + mScrollerX.updateScroll(q); + mScrollerY.updateScroll(q); + } else { + abortAnimation(); + } + break; + + case FLING_MODE: + if (!mScrollerX.mFinished) { + if (!mScrollerX.update()) { + if (!mScrollerX.continueWhenFinished()) { + mScrollerX.finish(); + } + } + } + + if (!mScrollerY.mFinished) { + if (!mScrollerY.update()) { + if (!mScrollerY.continueWhenFinished()) { + mScrollerY.finish(); + } + } + } + + break; + } + + return true; + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * The scroll will use the default value of 250 milliseconds for the + * duration. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + */ + public void startScroll(int startX, int startY, int dx, int dy) { + startScroll(startX, startY, dx, dy, DEFAULT_DURATION); + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + * @param duration Duration of the scroll in milliseconds. + */ + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + mMode = SCROLL_MODE; + mScrollerX.startScroll(startX, dx, duration); + mScrollerY.startScroll(startY, dy, duration); + } + + /** + * Call this when you want to 'spring back' into a valid coordinate range. + * + * @param startX Starting X coordinate + * @param startY Starting Y coordinate + * @param minX Minimum valid X value + * @param maxX Maximum valid X value + * @param minY Minimum valid Y value + * @param maxY Minimum valid Y value + * @return true if a springback was initiated, false if startX and startY were + * already within the valid range. + */ + public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) { + mMode = FLING_MODE; + + // Make sure both methods are called. + final boolean spingbackX = mScrollerX.springback(startX, minX, maxX); + final boolean spingbackY = mScrollerY.springback(startY, minY, maxY); + return spingbackX || spingbackY; + } + + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY) { + fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0); + } + + /** + * Start scrolling based on a fling gesture. The distance traveled will + * depend on the initial velocity of the fling. + * + * @param startX Starting point of the scroll (X) + * @param startY Starting point of the scroll (Y) + * @param velocityX Initial velocity of the fling (X) measured in pixels per + * second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per + * second + * @param minX Minimum X value. The scroller will not scroll past this point + * unless overX > 0. If overfling is allowed, it will use minX as + * a springback boundary. + * @param maxX Maximum X value. The scroller will not scroll past this point + * unless overX > 0. If overfling is allowed, it will use maxX as + * a springback boundary. + * @param minY Minimum Y value. The scroller will not scroll past this point + * unless overY > 0. If overfling is allowed, it will use minY as + * a springback boundary. + * @param maxY Maximum Y value. The scroller will not scroll past this point + * unless overY > 0. If overfling is allowed, it will use maxY as + * a springback boundary. + * @param overX Overfling range. If > 0, horizontal overfling in either + * direction will be possible. + * @param overY Overfling range. If > 0, vertical overfling in either + * direction will be possible. + */ + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY, int overX, int overY) { + // Continue a scroll or fling in progress + if (mFlywheel && !isFinished()) { + float oldVelocityX = mScrollerX.mCurrVelocity; + float oldVelocityY = mScrollerY.mCurrVelocity; + if (Math.signum(velocityX) == Math.signum(oldVelocityX) && + Math.signum(velocityY) == Math.signum(oldVelocityY)) { + velocityX += oldVelocityX; + velocityY += oldVelocityY; + } + } + + mMode = FLING_MODE; + mScrollerX.fling(startX, velocityX, minX, maxX, overX); + mScrollerY.fling(startY, velocityY, minY, maxY, overY); + } + + /** + * Notify the scroller that we've reached a horizontal boundary. + * Normally the information to handle this will already be known + * when the animation is started, such as in a call to one of the + * fling functions. However there are cases where this cannot be known + * in advance. This function will transition the current motion and + * animate from startX to finalX as appropriate. + * + * @param startX Starting/current X position + * @param finalX Desired final X position + * @param overX Magnitude of overscroll allowed. This should be the maximum + * desired distance from finalX. Absolute value - must be positive. + */ + public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) { + mScrollerX.notifyEdgeReached(startX, finalX, overX); + } + + /** + * Notify the scroller that we've reached a vertical boundary. + * Normally the information to handle this will already be known + * when the animation is started, such as in a call to one of the + * fling functions. However there are cases where this cannot be known + * in advance. This function will animate a parabolic motion from + * startY to finalY. + * + * @param startY Starting/current Y position + * @param finalY Desired final Y position + * @param overY Magnitude of overscroll allowed. This should be the maximum + * desired distance from finalY. Absolute value - must be positive. + */ + public void notifyVerticalEdgeReached(int startY, int finalY, int overY) { + mScrollerY.notifyEdgeReached(startY, finalY, overY); + } + + /** + * Returns whether the current Scroller is currently returning to a valid position. + * Valid bounds were provided by the + * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method. + * + * One should check this value before calling + * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress + * to restore a valid position will then be stopped. The caller has to take into account + * the fact that the started scroll will start from an overscrolled position. + * + * @return true when the current position is overscrolled and in the process of + * interpolating back to a valid value. + */ + public boolean isOverScrolled() { + return ((!mScrollerX.mFinished && + mScrollerX.mState != SplineOverScroller.SPLINE) || + (!mScrollerY.mFinished && + mScrollerY.mState != SplineOverScroller.SPLINE)); + } + + /** + * Stops the animation. Contrary to {@link #forceFinished(boolean)}, + * aborting the animating causes the scroller to move to the final x and y + * positions. + * + * @see #forceFinished(boolean) + */ + public void abortAnimation() { + mScrollerX.finish(); + mScrollerY.finish(); + } + + /** + * Returns the time elapsed since the beginning of the scrolling. + * + * @return The elapsed time in milliseconds. + * + * @hide + */ + public int timePassed() { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime); + return (int) (time - startTime); + } + + /** + * @hide + */ + public boolean isScrollingInDirection(float xvel, float yvel) { + final int dx = mScrollerX.mFinal - mScrollerX.mStart; + final int dy = mScrollerY.mFinal - mScrollerY.mStart; + return !isFinished() && Math.signum(xvel) == Math.signum(dx) && + Math.signum(yvel) == Math.signum(dy); + } + + static class SplineOverScroller { + // Initial position + private int mStart; + + // Current position + private int mCurrentPosition; + + // Final position + private int mFinal; + + // Initial velocity + private int mVelocity; + + // Current velocity + private float mCurrVelocity; + + // Constant current deceleration + private float mDeceleration; + + // Animation starting time, in system milliseconds + private long mStartTime; + + // Animation duration, in milliseconds + private int mDuration; + + // Duration to complete spline component of animation + private int mSplineDuration; + + // Distance to travel along spline animation + private int mSplineDistance; + + // Whether the animation is currently in progress + private boolean mFinished; + + // The allowed overshot distance before boundary is reached. + private int mOver; + + // Fling friction + private float mFlingFriction = ViewConfiguration.getScrollFriction(); + + // Current state of the animation. + private int mState = SPLINE; + + // Constant gravity value, used in the deceleration phase. + private static final float GRAVITY = 2000.0f; + + // A device specific coefficient adjusted to physical values. + private static float PHYSICAL_COEF; + + private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); + private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) + private static final float START_TENSION = 0.5f; + private static final float END_TENSION = 1.0f; + private static final float P1 = START_TENSION * INFLEXION; + private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION); + + private static final int NB_SAMPLES = 100; + private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1]; + private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1]; + + private static final int SPLINE = 0; + private static final int CUBIC = 1; + private static final int BALLISTIC = 2; + + static { + float x_min = 0.0f; + float y_min = 0.0f; + for (int i = 0; i < NB_SAMPLES; i++) { + final float alpha = (float) i / NB_SAMPLES; + + float x_max = 1.0f; + float x, tx, coef; + while (true) { + x = x_min + (x_max - x_min) / 2.0f; + coef = 3.0f * x * (1.0f - x); + tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x; + if (Math.abs(tx - alpha) < 1E-5) break; + if (tx > alpha) x_max = x; + else x_min = x; + } + SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x; + + float y_max = 1.0f; + float y, dy; + while (true) { + y = y_min + (y_max - y_min) / 2.0f; + coef = 3.0f * y * (1.0f - y); + dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y; + if (Math.abs(dy - alpha) < 1E-5) break; + if (dy > alpha) y_max = y; + else y_min = y; + } + SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; + } + SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; + } + + static void initFromContext(Context context) { + final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; + PHYSICAL_COEF = SensorManager.GRAVITY_EARTH // g (m/s^2) + * 39.37f // inch/meter + * ppi + * 0.84f; // look and feel tuning + } + + void setFriction(float friction) { + mFlingFriction = friction; + } + + SplineOverScroller() { + mFinished = true; + } + + void updateScroll(float q) { + mCurrentPosition = mStart + Math.round(q * (mFinal - mStart)); + } + + /* + * Get a signed deceleration that will reduce the velocity. + */ + static private float getDeceleration(int velocity) { + return velocity > 0 ? -GRAVITY : GRAVITY; + } + + /* + * Modifies mDuration to the duration it takes to get from start to newFinal using the + * spline interpolation. The previous duration was needed to get to oldFinal. + */ + private void adjustDuration(int start, int oldFinal, int newFinal) { + final int oldDistance = oldFinal - start; + final int newDistance = newFinal - start; + final float x = Math.abs((float) newDistance / oldDistance); + final int index = (int) (NB_SAMPLES * x); + if (index < NB_SAMPLES) { + final float x_inf = (float) index / NB_SAMPLES; + final float x_sup = (float) (index + 1) / NB_SAMPLES; + final float t_inf = SPLINE_TIME[index]; + final float t_sup = SPLINE_TIME[index + 1]; + final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf); + mDuration *= timeCoef; + } + } + + void startScroll(int start, int distance, int duration) { + mFinished = false; + + mStart = start; + mFinal = start + distance; + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = duration; + + // Unused + mDeceleration = 0.0f; + mVelocity = 0; + } + + void finish() { + mCurrentPosition = mFinal; + // Not reset since WebView relies on this value for fast fling. + // TODO: restore when WebView uses the fast fling implemented in this class. + // mCurrVelocity = 0.0f; + mFinished = true; + } + + void setFinalPosition(int position) { + mFinal = position; + mFinished = false; + } + + void extendDuration(int extend) { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final int elapsedTime = (int) (time - mStartTime); + mDuration = elapsedTime + extend; + mFinished = false; + } + + boolean springback(int start, int min, int max) { + mFinished = true; + + mStart = mFinal = start; + mVelocity = 0; + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = 0; + + if (start < min) { + startSpringback(start, min, 0); + } else if (start > max) { + startSpringback(start, max, 0); + } + + return !mFinished; + } + + private void startSpringback(int start, int end, int velocity) { + // mStartTime has been set + mFinished = false; + mState = CUBIC; + mStart = start; + mFinal = end; + final int delta = start - end; + mDeceleration = getDeceleration(delta); + // TODO take velocity into account + mVelocity = -delta; // only sign is used + mOver = Math.abs(delta); + mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration)); + } + + void fling(int start, int velocity, int min, int max, int over) { + mOver = over; + mFinished = false; + mCurrVelocity = mVelocity = velocity; + mDuration = mSplineDuration = 0; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mCurrentPosition = mStart = start; + + if (start > max || start < min) { + startAfterEdge(start, min, max, velocity); + return; + } + + mState = SPLINE; + double totalDistance = 0.0; + + if (velocity != 0) { + mDuration = mSplineDuration = getSplineFlingDuration(velocity); + totalDistance = getSplineFlingDistance(velocity); + } + + mSplineDistance = (int) (totalDistance * Math.signum(velocity)); + mFinal = start + mSplineDistance; + + // Clamp to a valid final position + if (mFinal < min) { + adjustDuration(mStart, mFinal, min); + mFinal = min; + } + + if (mFinal > max) { + adjustDuration(mStart, mFinal, max); + mFinal = max; + } + } + + private double getSplineDeceleration(int velocity) { + return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * PHYSICAL_COEF)); + } + + private double getSplineFlingDistance(int velocity) { + final double l = getSplineDeceleration(velocity); + final double decelMinusOne = DECELERATION_RATE - 1.0; + return mFlingFriction * PHYSICAL_COEF * Math.exp(DECELERATION_RATE / decelMinusOne * l); + } + + /* Returns the duration, expressed in milliseconds */ + private int getSplineFlingDuration(int velocity) { + final double l = getSplineDeceleration(velocity); + final double decelMinusOne = DECELERATION_RATE - 1.0; + return (int) (1000.0 * Math.exp(l / decelMinusOne)); + } + + private void fitOnBounceCurve(int start, int end, int velocity) { + // Simulate a bounce that started from edge + final float durationToApex = - velocity / mDeceleration; + final float distanceToApex = velocity * velocity / 2.0f / Math.abs(mDeceleration); + final float distanceToEdge = Math.abs(end - start); + final float totalDuration = (float) Math.sqrt( + 2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration)); + mStartTime -= (int) (1000.0f * (totalDuration - durationToApex)); + mStart = end; + mVelocity = (int) (- mDeceleration * totalDuration); + } + + private void startBounceAfterEdge(int start, int end, int velocity) { + mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity); + fitOnBounceCurve(start, end, velocity); + onEdgeReached(); + } + + private void startAfterEdge(int start, int min, int max, int velocity) { + if (start > min && start < max) { + Log.e("OverScroller", "startAfterEdge called from a valid position"); + mFinished = true; + return; + } + final boolean positive = start > max; + final int edge = positive ? max : min; + final int overDistance = start - edge; + boolean keepIncreasing = overDistance * velocity >= 0; + if (keepIncreasing) { + // Will result in a bounce or a to_boundary depending on velocity. + startBounceAfterEdge(start, edge, velocity); + } else { + final double totalDistance = getSplineFlingDistance(velocity); + if (totalDistance > Math.abs(overDistance)) { + fling(start, velocity, positive ? min : start, positive ? start : max, mOver); + } else { + startSpringback(start, edge, velocity); + } + } + } + + void notifyEdgeReached(int start, int end, int over) { + // mState is used to detect successive notifications + if (mState == SPLINE) { + mOver = over; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + // We were in fling/scroll mode before: current velocity is such that distance to + // edge is increasing. This ensures that startAfterEdge will not start a new fling. + startAfterEdge(start, end, end, (int) mCurrVelocity); + } + } + + private void onEdgeReached() { + // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached. + float distance = mVelocity * mVelocity / (2.0f * Math.abs(mDeceleration)); + final float sign = Math.signum(mVelocity); + + if (distance > mOver) { + // Default deceleration is not sufficient to slow us down before boundary + mDeceleration = - sign * mVelocity * mVelocity / (2.0f * mOver); + distance = mOver; + } + + mOver = (int) distance; + mState = BALLISTIC; + mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance); + mDuration = - (int) (1000.0f * mVelocity / mDeceleration); + } + + boolean continueWhenFinished() { + switch (mState) { + case SPLINE: + // Duration from start to null velocity + if (mDuration < mSplineDuration) { + // If the animation was clamped, we reached the edge + mStart = mFinal; + // TODO Better compute speed when edge was reached + mVelocity = (int) mCurrVelocity; + mDeceleration = getDeceleration(mVelocity); + mStartTime += mDuration; + onEdgeReached(); + } else { + // Normal stop, no need to continue + return false; + } + break; + case BALLISTIC: + mStartTime += mDuration; + startSpringback(mFinal, mStart, 0); + break; + case CUBIC: + return false; + } + + update(); + return true; + } + + /* + * Update the current position and velocity for current time. Returns + * true if update has been done and false if animation duration has been + * reached. + */ + boolean update() { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final long currentTime = time - mStartTime; + + if (currentTime > mDuration) { + return false; + } + + double distance = 0.0; + switch (mState) { + case SPLINE: { + final float t = (float) currentTime / mSplineDuration; + final int index = (int) (NB_SAMPLES * t); + float distanceCoef = 1.f; + float velocityCoef = 0.f; + if (index < NB_SAMPLES) { + final float t_inf = (float) index / NB_SAMPLES; + final float t_sup = (float) (index + 1) / NB_SAMPLES; + final float d_inf = SPLINE_POSITION[index]; + final float d_sup = SPLINE_POSITION[index + 1]; + velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); + distanceCoef = d_inf + (t - t_inf) * velocityCoef; + } + + distance = distanceCoef * mSplineDistance; + mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f; + break; + } + + case BALLISTIC: { + final float t = currentTime / 1000.0f; + mCurrVelocity = mVelocity + mDeceleration * t; + distance = mVelocity * t + mDeceleration * t * t / 2.0f; + break; + } + + case CUBIC: { + final float t = (float) (currentTime) / mDuration; + final float t2 = t * t; + final float sign = Math.signum(mVelocity); + distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2); + mCurrVelocity = sign * mOver * 6.0f * (- t + t2); + break; + } + } + + mCurrentPosition = mStart + (int) Math.round(distance); + + return true; + } + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/Scroller.java b/gallerycommon/src/com/android/gallery3d/common/Scroller.java new file mode 100644 index 000000000..6cefd6fb0 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/Scroller.java @@ -0,0 +1,507 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.hardware.SensorManager; +import android.os.Build; +import android.util.FloatMath; +import android.view.ViewConfiguration; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + + +/** + * This class encapsulates scrolling. The duration of the scroll + * can be passed in the constructor and specifies the maximum time that + * the scrolling animation should take. Past this time, the scrolling is + * automatically moved to its final stage and computeScrollOffset() + * will always return false to indicate that scrolling is over. + */ +public class Scroller { + private int mMode; + + private int mStartX; + private int mStartY; + private int mFinalX; + private int mFinalY; + + private int mMinX; + private int mMaxX; + private int mMinY; + private int mMaxY; + + private int mCurrX; + private int mCurrY; + private long mStartTime; + private int mDuration; + private float mDurationReciprocal; + private float mDeltaX; + private float mDeltaY; + private boolean mFinished; + private Interpolator mInterpolator; + private boolean mFlywheel; + + private float mVelocity; + + private static final int DEFAULT_DURATION = 250; + private static final int SCROLL_MODE = 0; + private static final int FLING_MODE = 1; + + private static float DECELERATION_RATE = (float) (Math.log(0.75) / Math.log(0.9)); + private static float ALPHA = 800; // pixels / seconds + private static float START_TENSION = 0.4f; // Tension at start: (0.4 * total T, 1.0 * Distance) + private static float END_TENSION = 1.0f - START_TENSION; + private static final int NB_SAMPLES = 100; + private static final float[] SPLINE = new float[NB_SAMPLES + 1]; + + private float mDeceleration; + private final float mPpi; + + static { + float x_min = 0.0f; + for (int i = 0; i <= NB_SAMPLES; i++) { + final float t = (float) i / NB_SAMPLES; + float x_max = 1.0f; + float x, tx, coef; + while (true) { + x = x_min + (x_max - x_min) / 2.0f; + coef = 3.0f * x * (1.0f - x); + tx = coef * ((1.0f - x) * START_TENSION + x * END_TENSION) + x * x * x; + if (Math.abs(tx - t) < 1E-5) break; + if (tx > t) x_max = x; + else x_min = x; + } + final float d = coef + x * x * x; + SPLINE[i] = d; + } + SPLINE[NB_SAMPLES] = 1.0f; + + // This controls the viscous fluid effect (how much of it) + sViscousFluidScale = 8.0f; + // must be set to 1.0 (used in viscousFluid()) + sViscousFluidNormalize = 1.0f; + sViscousFluidNormalize = 1.0f / viscousFluid(1.0f); + } + + private static float sViscousFluidScale; + private static float sViscousFluidNormalize; + + /** + * Create a Scroller with the default duration and interpolator. + */ + public Scroller(Context context) { + this(context, null); + } + + /** + * Create a Scroller with the specified interpolator. If the interpolator is + * null, the default (viscous) interpolator will be used. "Flywheel" behavior will + * be in effect for apps targeting Honeycomb or newer. + */ + public Scroller(Context context, Interpolator interpolator) { + this(context, interpolator, + context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB); + } + + /** + * Create a Scroller with the specified interpolator. If the interpolator is + * null, the default (viscous) interpolator will be used. Specify whether or + * not to support progressive "flywheel" behavior in flinging. + */ + public Scroller(Context context, Interpolator interpolator, boolean flywheel) { + mFinished = true; + mInterpolator = interpolator; + mPpi = context.getResources().getDisplayMetrics().density * 160.0f; + mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); + mFlywheel = flywheel; + } + + /** + * The amount of friction applied to flings. The default value + * is {@link ViewConfiguration#getScrollFriction}. + * + * @param friction A scalar dimension-less value representing the coefficient of + * friction. + */ + public final void setFriction(float friction) { + mDeceleration = computeDeceleration(friction); + } + + private float computeDeceleration(float friction) { + return SensorManager.GRAVITY_EARTH // g (m/s^2) + * 39.37f // inch/meter + * mPpi // pixels per inch + * friction; + } + + /** + * + * Returns whether the scroller has finished scrolling. + * + * @return True if the scroller has finished scrolling, false otherwise. + */ + public final boolean isFinished() { + return mFinished; + } + + /** + * Force the finished field to a particular value. + * + * @param finished The new finished value. + */ + public final void forceFinished(boolean finished) { + mFinished = finished; + } + + /** + * Returns how long the scroll event will take, in milliseconds. + * + * @return The duration of the scroll in milliseconds. + */ + public final int getDuration() { + return mDuration; + } + + /** + * Returns the current X offset in the scroll. + * + * @return The new X offset as an absolute distance from the origin. + */ + public final int getCurrX() { + return mCurrX; + } + + /** + * Returns the current Y offset in the scroll. + * + * @return The new Y offset as an absolute distance from the origin. + */ + public final int getCurrY() { + return mCurrY; + } + + /** + * Returns the current velocity. + * + * @return The original velocity less the deceleration. Result may be + * negative. + */ + public float getCurrVelocity() { + return mVelocity - mDeceleration * timePassed() / 2000.0f; + } + + /** + * Returns the start X offset in the scroll. + * + * @return The start X offset as an absolute distance from the origin. + */ + public final int getStartX() { + return mStartX; + } + + /** + * Returns the start Y offset in the scroll. + * + * @return The start Y offset as an absolute distance from the origin. + */ + public final int getStartY() { + return mStartY; + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final X offset as an absolute distance from the origin. + */ + public final int getFinalX() { + return mFinalX; + } + + /** + * Returns where the scroll will end. Valid only for "fling" scrolls. + * + * @return The final Y offset as an absolute distance from the origin. + */ + public final int getFinalY() { + return mFinalY; + } + + /** + * Call this when you want to know the new location. If it returns true, + * the animation is not yet finished. loc will be altered to provide the + * new location. + */ + public boolean computeScrollOffset() { + if (mFinished) { + return false; + } + + int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); + + if (timePassed < mDuration) { + switch (mMode) { + case SCROLL_MODE: + float x = timePassed * mDurationReciprocal; + + if (mInterpolator == null) + x = viscousFluid(x); + else + x = mInterpolator.getInterpolation(x); + + mCurrX = mStartX + Math.round(x * mDeltaX); + mCurrY = mStartY + Math.round(x * mDeltaY); + break; + case FLING_MODE: + final float t = (float) timePassed / mDuration; + final int index = (int) (NB_SAMPLES * t); + final float t_inf = (float) index / NB_SAMPLES; + final float t_sup = (float) (index + 1) / NB_SAMPLES; + final float d_inf = SPLINE[index]; + final float d_sup = SPLINE[index + 1]; + final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf); + + mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX)); + // Pin to mMinX <= mCurrX <= mMaxX + mCurrX = Math.min(mCurrX, mMaxX); + mCurrX = Math.max(mCurrX, mMinX); + + mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY)); + // Pin to mMinY <= mCurrY <= mMaxY + mCurrY = Math.min(mCurrY, mMaxY); + mCurrY = Math.max(mCurrY, mMinY); + + if (mCurrX == mFinalX && mCurrY == mFinalY) { + mFinished = true; + } + + break; + } + } + else { + mCurrX = mFinalX; + mCurrY = mFinalY; + mFinished = true; + } + return true; + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * The scroll will use the default value of 250 milliseconds for the + * duration. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + */ + public void startScroll(int startX, int startY, int dx, int dy) { + startScroll(startX, startY, dx, dy, DEFAULT_DURATION); + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + * @param duration Duration of the scroll in milliseconds. + */ + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + mMode = SCROLL_MODE; + mFinished = false; + mDuration = duration; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStartX = startX; + mStartY = startY; + mFinalX = startX + dx; + mFinalY = startY + dy; + mDeltaX = dx; + mDeltaY = dy; + mDurationReciprocal = 1.0f / mDuration; + } + + /** + * Start scrolling based on a fling gesture. The distance travelled will + * depend on the initial velocity of the fling. + * + * @param startX Starting point of the scroll (X) + * @param startY Starting point of the scroll (Y) + * @param velocityX Initial velocity of the fling (X) measured in pixels per + * second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per + * second + * @param minX Minimum X value. The scroller will not scroll past this + * point. + * @param maxX Maximum X value. The scroller will not scroll past this + * point. + * @param minY Minimum Y value. The scroller will not scroll past this + * point. + * @param maxY Maximum Y value. The scroller will not scroll past this + * point. + */ + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY) { + // Continue a scroll or fling in progress + if (mFlywheel && !mFinished) { + float oldVel = getCurrVelocity(); + + float dx = mFinalX - mStartX; + float dy = mFinalY - mStartY; + float hyp = FloatMath.sqrt(dx * dx + dy * dy); + + float ndx = dx / hyp; + float ndy = dy / hyp; + + float oldVelocityX = ndx * oldVel; + float oldVelocityY = ndy * oldVel; + if (Math.signum(velocityX) == Math.signum(oldVelocityX) && + Math.signum(velocityY) == Math.signum(oldVelocityY)) { + velocityX += oldVelocityX; + velocityY += oldVelocityY; + } + } + + mMode = FLING_MODE; + mFinished = false; + + float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY); + + mVelocity = velocity; + final double l = Math.log(START_TENSION * velocity / ALPHA); + mDuration = (int) (1000.0 * Math.exp(l / (DECELERATION_RATE - 1.0))); + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStartX = startX; + mStartY = startY; + + float coeffX = velocity == 0 ? 1.0f : velocityX / velocity; + float coeffY = velocity == 0 ? 1.0f : velocityY / velocity; + + int totalDistance = + (int) (ALPHA * Math.exp(DECELERATION_RATE / (DECELERATION_RATE - 1.0) * l)); + + mMinX = minX; + mMaxX = maxX; + mMinY = minY; + mMaxY = maxY; + + mFinalX = startX + Math.round(totalDistance * coeffX); + // Pin to mMinX <= mFinalX <= mMaxX + mFinalX = Math.min(mFinalX, mMaxX); + mFinalX = Math.max(mFinalX, mMinX); + + mFinalY = startY + Math.round(totalDistance * coeffY); + // Pin to mMinY <= mFinalY <= mMaxY + mFinalY = Math.min(mFinalY, mMaxY); + mFinalY = Math.max(mFinalY, mMinY); + } + + static float viscousFluid(float x) + { + x *= sViscousFluidScale; + if (x < 1.0f) { + x -= (1.0f - (float)Math.exp(-x)); + } else { + float start = 0.36787944117f; // 1/e == exp(-1) + x = 1.0f - (float)Math.exp(1.0f - x); + x = start + x * (1.0f - start); + } + x *= sViscousFluidNormalize; + return x; + } + + /** + * Stops the animation. Contrary to {@link #forceFinished(boolean)}, + * aborting the animating cause the scroller to move to the final x and y + * position + * + * @see #forceFinished(boolean) + */ + public void abortAnimation() { + mCurrX = mFinalX; + mCurrY = mFinalY; + mFinished = true; + } + + /** + * Extend the scroll animation. This allows a running animation to scroll + * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}. + * + * @param extend Additional time to scroll in milliseconds. + * @see #setFinalX(int) + * @see #setFinalY(int) + */ + public void extendDuration(int extend) { + int passed = timePassed(); + mDuration = passed + extend; + mDurationReciprocal = 1.0f / mDuration; + mFinished = false; + } + + /** + * Returns the time elapsed since the beginning of the scrolling. + * + * @return The elapsed time in milliseconds. + */ + public int timePassed() { + return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); + } + + /** + * Sets the final position (X) for this scroller. + * + * @param newX The new X offset as an absolute distance from the origin. + * @see #extendDuration(int) + * @see #setFinalY(int) + */ + public void setFinalX(int newX) { + mFinalX = newX; + mDeltaX = mFinalX - mStartX; + mFinished = false; + } + + /** + * Sets the final position (Y) for this scroller. + * + * @param newY The new Y offset as an absolute distance from the origin. + * @see #extendDuration(int) + * @see #setFinalX(int) + */ + public void setFinalY(int newY) { + mFinalY = newY; + mDeltaY = mFinalY - mStartY; + mFinished = false; + } + + /** + * @hide + */ + public boolean isScrollingInDirection(float xvel, float yvel) { + return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && + Math.signum(yvel) == Math.signum(mFinalY - mStartY); + } +} diff --git a/gallerycommon/src/com/android/gallery3d/common/Utils.java b/gallerycommon/src/com/android/gallery3d/common/Utils.java new file mode 100644 index 000000000..614a081c8 --- /dev/null +++ b/gallerycommon/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> 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 <code>info.toString()</code> 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/gallerycommon/src/com/android/gallery3d/exif/ByteBufferInputStream.java b/gallerycommon/src/com/android/gallery3d/exif/ByteBufferInputStream.java new file mode 100644 index 000000000..7fb9f22cc --- /dev/null +++ b/gallerycommon/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/gallerycommon/src/com/android/gallery3d/exif/CountedDataInputStream.java b/gallerycommon/src/com/android/gallery3d/exif/CountedDataInputStream.java new file mode 100644 index 000000000..dfd4a1a10 --- /dev/null +++ b/gallerycommon/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/gallerycommon/src/com/android/gallery3d/exif/ExifData.java b/gallerycommon/src/com/android/gallery3d/exif/ExifData.java new file mode 100644 index 000000000..8422382bb --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/ExifData.java @@ -0,0 +1,348 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.exif; + +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * This class stores the EXIF header in IFDs according to the JPEG + * specification. It is the result produced by {@link ExifReader}. + * + * @see ExifReader + * @see IfdData + */ +class ExifData { + private static final String TAG = "ExifData"; + private static final byte[] USER_COMMENT_ASCII = { + 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00 + }; + private static final byte[] USER_COMMENT_JIS = { + 0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + private static final byte[] USER_COMMENT_UNICODE = { + 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00 + }; + + private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT]; + private byte[] mThumbnail; + private ArrayList<byte[]> mStripBytes = new ArrayList<byte[]>(); + private final ByteOrder mByteOrder; + + ExifData(ByteOrder order) { + mByteOrder = order; + } + + /** + * Gets the compressed thumbnail. Returns null if there is no compressed + * thumbnail. + * + * @see #hasCompressedThumbnail() + */ + protected byte[] getCompressedThumbnail() { + return mThumbnail; + } + + /** + * Sets the compressed thumbnail. + */ + protected void setCompressedThumbnail(byte[] thumbnail) { + mThumbnail = thumbnail; + } + + /** + * Returns true it this header contains a compressed thumbnail. + */ + protected boolean hasCompressedThumbnail() { + return mThumbnail != null; + } + + /** + * Adds an uncompressed strip. + */ + protected void setStripBytes(int index, byte[] strip) { + if (index < mStripBytes.size()) { + mStripBytes.set(index, strip); + } else { + for (int i = mStripBytes.size(); i < index; i++) { + mStripBytes.add(null); + } + mStripBytes.add(strip); + } + } + + /** + * Gets the strip count. + */ + protected int getStripCount() { + return mStripBytes.size(); + } + + /** + * Gets the strip at the specified index. + * + * @exceptions #IndexOutOfBoundException + */ + protected byte[] getStrip(int index) { + return mStripBytes.get(index); + } + + /** + * Returns true if this header contains uncompressed strip. + */ + protected boolean hasUncompressedStrip() { + return mStripBytes.size() != 0; + } + + /** + * Gets the byte order. + */ + protected ByteOrder getByteOrder() { + return mByteOrder; + } + + /** + * Returns the {@link IfdData} object corresponding to a given IFD if it + * exists or null. + */ + protected IfdData getIfdData(int ifdId) { + if (ExifTag.isValidIfd(ifdId)) { + return mIfdDatas[ifdId]; + } + return null; + } + + /** + * Adds IFD data. If IFD data of the same type already exists, it will be + * replaced by the new data. + */ + protected void addIfdData(IfdData data) { + mIfdDatas[data.getId()] = data; + } + + /** + * Returns the {@link IfdData} object corresponding to a given IFD or + * generates one if none exist. + */ + protected IfdData getOrCreateIfdData(int ifdId) { + IfdData ifdData = mIfdDatas[ifdId]; + if (ifdData == null) { + ifdData = new IfdData(ifdId); + mIfdDatas[ifdId] = ifdData; + } + return ifdData; + } + + /** + * Returns the tag with a given TID in the given IFD if the tag exists. + * Otherwise returns null. + */ + protected ExifTag getTag(short tag, int ifd) { + IfdData ifdData = mIfdDatas[ifd]; + return (ifdData == null) ? null : ifdData.getTag(tag); + } + + /** + * Adds the given ExifTag to its default IFD and returns an existing ExifTag + * with the same TID or null if none exist. + */ + protected ExifTag addTag(ExifTag tag) { + if (tag != null) { + int ifd = tag.getIfd(); + return addTag(tag, ifd); + } + return null; + } + + /** + * Adds the given ExifTag to the given IFD and returns an existing ExifTag + * with the same TID or null if none exist. + */ + protected ExifTag addTag(ExifTag tag, int ifdId) { + if (tag != null && ExifTag.isValidIfd(ifdId)) { + IfdData ifdData = getOrCreateIfdData(ifdId); + return ifdData.setTag(tag); + } + return null; + } + + protected void clearThumbnailAndStrips() { + mThumbnail = null; + mStripBytes.clear(); + } + + /** + * Removes the thumbnail and its related tags. IFD1 will be removed. + */ + protected void removeThumbnailData() { + clearThumbnailAndStrips(); + mIfdDatas[IfdId.TYPE_IFD_1] = null; + } + + /** + * Removes the tag with a given TID and IFD. + */ + protected void removeTag(short tagId, int ifdId) { + IfdData ifdData = mIfdDatas[ifdId]; + if (ifdData == null) { + return; + } + ifdData.removeTag(tagId); + } + + /** + * Decodes the user comment tag into string as specified in the EXIF + * standard. Returns null if decoding failed. + */ + protected String getUserComment() { + IfdData ifdData = mIfdDatas[IfdId.TYPE_IFD_0]; + if (ifdData == null) { + return null; + } + ExifTag tag = ifdData.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_USER_COMMENT)); + if (tag == null) { + return null; + } + if (tag.getComponentCount() < 8) { + return null; + } + + byte[] buf = new byte[tag.getComponentCount()]; + tag.getBytes(buf); + + byte[] code = new byte[8]; + System.arraycopy(buf, 0, code, 0, 8); + + try { + if (Arrays.equals(code, USER_COMMENT_ASCII)) { + return new String(buf, 8, buf.length - 8, "US-ASCII"); + } else if (Arrays.equals(code, USER_COMMENT_JIS)) { + return new String(buf, 8, buf.length - 8, "EUC-JP"); + } else if (Arrays.equals(code, USER_COMMENT_UNICODE)) { + return new String(buf, 8, buf.length - 8, "UTF-16"); + } else { + return null; + } + } catch (UnsupportedEncodingException e) { + Log.w(TAG, "Failed to decode the user comment"); + return null; + } + } + + /** + * Returns a list of all {@link ExifTag}s in the ExifData or null if there + * are none. + */ + protected List<ExifTag> getAllTags() { + ArrayList<ExifTag> ret = new ArrayList<ExifTag>(); + for (IfdData d : mIfdDatas) { + if (d != null) { + ExifTag[] tags = d.getAllTags(); + if (tags != null) { + for (ExifTag t : tags) { + ret.add(t); + } + } + } + } + if (ret.size() == 0) { + return null; + } + return ret; + } + + /** + * Returns a list of all {@link ExifTag}s in a given IFD or null if there + * are none. + */ + protected List<ExifTag> getAllTagsForIfd(int ifd) { + IfdData d = mIfdDatas[ifd]; + if (d == null) { + return null; + } + ExifTag[] tags = d.getAllTags(); + if (tags == null) { + return null; + } + ArrayList<ExifTag> ret = new ArrayList<ExifTag>(tags.length); + for (ExifTag t : tags) { + ret.add(t); + } + if (ret.size() == 0) { + return null; + } + return ret; + } + + /** + * Returns a list of all {@link ExifTag}s with a given TID or null if there + * are none. + */ + protected List<ExifTag> getAllTagsForTagId(short tag) { + ArrayList<ExifTag> ret = new ArrayList<ExifTag>(); + for (IfdData d : mIfdDatas) { + if (d != null) { + ExifTag t = d.getTag(tag); + if (t != null) { + ret.add(t); + } + } + } + if (ret.size() == 0) { + return null; + } + return ret; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (obj instanceof ExifData) { + ExifData data = (ExifData) obj; + if (data.mByteOrder != mByteOrder || + data.mStripBytes.size() != mStripBytes.size() || + !Arrays.equals(data.mThumbnail, mThumbnail)) { + return false; + } + for (int i = 0; i < mStripBytes.size(); i++) { + if (!Arrays.equals(data.mStripBytes.get(i), mStripBytes.get(i))) { + return false; + } + } + for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) { + IfdData ifd1 = data.getIfdData(i); + IfdData ifd2 = getIfdData(i); + if (ifd1 != ifd2 && ifd1 != null && !ifd1.equals(ifd2)) { + return false; + } + } + return true; + } + return false; + } + +} diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifInterface.java b/gallerycommon/src/com/android/gallery3d/exif/ExifInterface.java new file mode 100644 index 000000000..a1cf0fc85 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/ExifInterface.java @@ -0,0 +1,2407 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.exif; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.SparseIntArray; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel.MapMode; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.TimeZone; + +/** + * This class provides methods and constants for reading and writing jpeg file + * metadata. It contains a collection of ExifTags, and a collection of + * definitions for creating valid ExifTags. The collection of ExifTags can be + * updated by: reading new ones from a file, deleting or adding existing ones, + * or building new ExifTags from a tag definition. These ExifTags can be written + * to a valid jpeg image as exif metadata. + * <p> + * Each ExifTag has a tag ID (TID) and is stored in a specific image file + * directory (IFD) as specified by the exif standard. A tag definition can be + * looked up with a constant that is a combination of TID and IFD. This + * definition has information about the type, number of components, and valid + * IFDs for a tag. + * + * @see ExifTag + */ +public class ExifInterface { + public static final int TAG_NULL = -1; + public static final int IFD_NULL = -1; + public static final int DEFINITION_NULL = 0; + + /** + * Tag constants for Jeita EXIF 2.2 + */ + + // IFD 0 + public static final int TAG_IMAGE_WIDTH = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0100); + public static final int TAG_IMAGE_LENGTH = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0101); // Image height + public static final int TAG_BITS_PER_SAMPLE = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0102); + public static final int TAG_COMPRESSION = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0103); + public static final int TAG_PHOTOMETRIC_INTERPRETATION = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0106); + public static final int TAG_IMAGE_DESCRIPTION = + defineTag(IfdId.TYPE_IFD_0, (short) 0x010E); + public static final int TAG_MAKE = + defineTag(IfdId.TYPE_IFD_0, (short) 0x010F); + public static final int TAG_MODEL = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0110); + public static final int TAG_STRIP_OFFSETS = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0111); + public static final int TAG_ORIENTATION = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0112); + public static final int TAG_SAMPLES_PER_PIXEL = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0115); + public static final int TAG_ROWS_PER_STRIP = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0116); + public static final int TAG_STRIP_BYTE_COUNTS = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0117); + public static final int TAG_X_RESOLUTION = + defineTag(IfdId.TYPE_IFD_0, (short) 0x011A); + public static final int TAG_Y_RESOLUTION = + defineTag(IfdId.TYPE_IFD_0, (short) 0x011B); + public static final int TAG_PLANAR_CONFIGURATION = + defineTag(IfdId.TYPE_IFD_0, (short) 0x011C); + public static final int TAG_RESOLUTION_UNIT = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0128); + public static final int TAG_TRANSFER_FUNCTION = + defineTag(IfdId.TYPE_IFD_0, (short) 0x012D); + public static final int TAG_SOFTWARE = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0131); + public static final int TAG_DATE_TIME = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0132); + public static final int TAG_ARTIST = + defineTag(IfdId.TYPE_IFD_0, (short) 0x013B); + public static final int TAG_WHITE_POINT = + defineTag(IfdId.TYPE_IFD_0, (short) 0x013E); + public static final int TAG_PRIMARY_CHROMATICITIES = + defineTag(IfdId.TYPE_IFD_0, (short) 0x013F); + public static final int TAG_Y_CB_CR_COEFFICIENTS = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0211); + public static final int TAG_Y_CB_CR_SUB_SAMPLING = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0212); + public static final int TAG_Y_CB_CR_POSITIONING = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0213); + public static final int TAG_REFERENCE_BLACK_WHITE = + defineTag(IfdId.TYPE_IFD_0, (short) 0x0214); + public static final int TAG_COPYRIGHT = + defineTag(IfdId.TYPE_IFD_0, (short) 0x8298); + public static final int TAG_EXIF_IFD = + defineTag(IfdId.TYPE_IFD_0, (short) 0x8769); + public static final int TAG_GPS_IFD = + defineTag(IfdId.TYPE_IFD_0, (short) 0x8825); + // IFD 1 + public static final int TAG_JPEG_INTERCHANGE_FORMAT = + defineTag(IfdId.TYPE_IFD_1, (short) 0x0201); + public static final int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = + defineTag(IfdId.TYPE_IFD_1, (short) 0x0202); + // IFD Exif Tags + public static final int TAG_EXPOSURE_TIME = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x829A); + public static final int TAG_F_NUMBER = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x829D); + public static final int TAG_EXPOSURE_PROGRAM = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8822); + public static final int TAG_SPECTRAL_SENSITIVITY = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8824); + public static final int TAG_ISO_SPEED_RATINGS = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8827); + public static final int TAG_OECF = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x8828); + public static final int TAG_EXIF_VERSION = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9000); + public static final int TAG_DATE_TIME_ORIGINAL = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9003); + public static final int TAG_DATE_TIME_DIGITIZED = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9004); + public static final int TAG_COMPONENTS_CONFIGURATION = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9101); + public static final int TAG_COMPRESSED_BITS_PER_PIXEL = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9102); + public static final int TAG_SHUTTER_SPEED_VALUE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9201); + public static final int TAG_APERTURE_VALUE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9202); + public static final int TAG_BRIGHTNESS_VALUE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9203); + public static final int TAG_EXPOSURE_BIAS_VALUE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9204); + public static final int TAG_MAX_APERTURE_VALUE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9205); + public static final int TAG_SUBJECT_DISTANCE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9206); + public static final int TAG_METERING_MODE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9207); + public static final int TAG_LIGHT_SOURCE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9208); + public static final int TAG_FLASH = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9209); + public static final int TAG_FOCAL_LENGTH = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x920A); + public static final int TAG_SUBJECT_AREA = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9214); + public static final int TAG_MAKER_NOTE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x927C); + public static final int TAG_USER_COMMENT = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9286); + public static final int TAG_SUB_SEC_TIME = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9290); + public static final int TAG_SUB_SEC_TIME_ORIGINAL = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9291); + public static final int TAG_SUB_SEC_TIME_DIGITIZED = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0x9292); + public static final int TAG_FLASHPIX_VERSION = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA000); + public static final int TAG_COLOR_SPACE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA001); + public static final int TAG_PIXEL_X_DIMENSION = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA002); + public static final int TAG_PIXEL_Y_DIMENSION = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA003); + public static final int TAG_RELATED_SOUND_FILE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA004); + public static final int TAG_INTEROPERABILITY_IFD = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA005); + public static final int TAG_FLASH_ENERGY = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20B); + public static final int TAG_SPATIAL_FREQUENCY_RESPONSE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20C); + public static final int TAG_FOCAL_PLANE_X_RESOLUTION = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20E); + public static final int TAG_FOCAL_PLANE_Y_RESOLUTION = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA20F); + public static final int TAG_FOCAL_PLANE_RESOLUTION_UNIT = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA210); + public static final int TAG_SUBJECT_LOCATION = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA214); + public static final int TAG_EXPOSURE_INDEX = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA215); + public static final int TAG_SENSING_METHOD = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA217); + public static final int TAG_FILE_SOURCE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA300); + public static final int TAG_SCENE_TYPE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA301); + public static final int TAG_CFA_PATTERN = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA302); + public static final int TAG_CUSTOM_RENDERED = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA401); + public static final int TAG_EXPOSURE_MODE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA402); + public static final int TAG_WHITE_BALANCE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA403); + public static final int TAG_DIGITAL_ZOOM_RATIO = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA404); + public static final int TAG_FOCAL_LENGTH_IN_35_MM_FILE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA405); + public static final int TAG_SCENE_CAPTURE_TYPE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA406); + public static final int TAG_GAIN_CONTROL = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA407); + public static final int TAG_CONTRAST = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA408); + public static final int TAG_SATURATION = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA409); + public static final int TAG_SHARPNESS = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40A); + public static final int TAG_DEVICE_SETTING_DESCRIPTION = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40B); + public static final int TAG_SUBJECT_DISTANCE_RANGE = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA40C); + public static final int TAG_IMAGE_UNIQUE_ID = + defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA420); + // IFD GPS tags + public static final int TAG_GPS_VERSION_ID = + defineTag(IfdId.TYPE_IFD_GPS, (short) 0); + public static final int TAG_GPS_LATITUDE_REF = + defineTag(IfdId.TYPE_IFD_GPS, (short) 1); + public static final int TAG_GPS_LATITUDE = + defineTag(IfdId.TYPE_IFD_GPS, (short) 2); + public static final int TAG_GPS_LONGITUDE_REF = + defineTag(IfdId.TYPE_IFD_GPS, (short) 3); + public static final int TAG_GPS_LONGITUDE = + defineTag(IfdId.TYPE_IFD_GPS, (short) 4); + public static final int TAG_GPS_ALTITUDE_REF = + defineTag(IfdId.TYPE_IFD_GPS, (short) 5); + public static final int TAG_GPS_ALTITUDE = + defineTag(IfdId.TYPE_IFD_GPS, (short) 6); + public static final int TAG_GPS_TIME_STAMP = + defineTag(IfdId.TYPE_IFD_GPS, (short) 7); + public static final int TAG_GPS_SATTELLITES = + defineTag(IfdId.TYPE_IFD_GPS, (short) 8); + public static final int TAG_GPS_STATUS = + defineTag(IfdId.TYPE_IFD_GPS, (short) 9); + public static final int TAG_GPS_MEASURE_MODE = + defineTag(IfdId.TYPE_IFD_GPS, (short) 10); + public static final int TAG_GPS_DOP = + defineTag(IfdId.TYPE_IFD_GPS, (short) 11); + public static final int TAG_GPS_SPEED_REF = + defineTag(IfdId.TYPE_IFD_GPS, (short) 12); + public static final int TAG_GPS_SPEED = + defineTag(IfdId.TYPE_IFD_GPS, (short) 13); + public static final int TAG_GPS_TRACK_REF = + defineTag(IfdId.TYPE_IFD_GPS, (short) 14); + public static final int TAG_GPS_TRACK = + defineTag(IfdId.TYPE_IFD_GPS, (short) 15); + public static final int TAG_GPS_IMG_DIRECTION_REF = + defineTag(IfdId.TYPE_IFD_GPS, (short) 16); + public static final int TAG_GPS_IMG_DIRECTION = + defineTag(IfdId.TYPE_IFD_GPS, (short) 17); + public static final int TAG_GPS_MAP_DATUM = + defineTag(IfdId.TYPE_IFD_GPS, (short) 18); + public static final int TAG_GPS_DEST_LATITUDE_REF = + defineTag(IfdId.TYPE_IFD_GPS, (short) 19); + public static final int TAG_GPS_DEST_LATITUDE = + defineTag(IfdId.TYPE_IFD_GPS, (short) 20); + public static final int TAG_GPS_DEST_LONGITUDE_REF = + defineTag(IfdId.TYPE_IFD_GPS, (short) 21); + public static final int TAG_GPS_DEST_LONGITUDE = + defineTag(IfdId.TYPE_IFD_GPS, (short) 22); + public static final int TAG_GPS_DEST_BEARING_REF = + defineTag(IfdId.TYPE_IFD_GPS, (short) 23); + public static final int TAG_GPS_DEST_BEARING = + defineTag(IfdId.TYPE_IFD_GPS, (short) 24); + public static final int TAG_GPS_DEST_DISTANCE_REF = + defineTag(IfdId.TYPE_IFD_GPS, (short) 25); + public static final int TAG_GPS_DEST_DISTANCE = + defineTag(IfdId.TYPE_IFD_GPS, (short) 26); + public static final int TAG_GPS_PROCESSING_METHOD = + defineTag(IfdId.TYPE_IFD_GPS, (short) 27); + public static final int TAG_GPS_AREA_INFORMATION = + defineTag(IfdId.TYPE_IFD_GPS, (short) 28); + public static final int TAG_GPS_DATE_STAMP = + defineTag(IfdId.TYPE_IFD_GPS, (short) 29); + public static final int TAG_GPS_DIFFERENTIAL = + defineTag(IfdId.TYPE_IFD_GPS, (short) 30); + // IFD Interoperability tags + public static final int TAG_INTEROPERABILITY_INDEX = + defineTag(IfdId.TYPE_IFD_INTEROPERABILITY, (short) 1); + + /** + * Tags that contain offset markers. These are included in the banned + * defines. + */ + private static HashSet<Short> sOffsetTags = new HashSet<Short>(); + static { + sOffsetTags.add(getTrueTagKey(TAG_GPS_IFD)); + sOffsetTags.add(getTrueTagKey(TAG_EXIF_IFD)); + sOffsetTags.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT)); + sOffsetTags.add(getTrueTagKey(TAG_INTEROPERABILITY_IFD)); + sOffsetTags.add(getTrueTagKey(TAG_STRIP_OFFSETS)); + } + + /** + * Tags with definitions that cannot be overridden (banned defines). + */ + protected static HashSet<Short> sBannedDefines = new HashSet<Short>(sOffsetTags); + static { + sBannedDefines.add(getTrueTagKey(TAG_NULL)); + sBannedDefines.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)); + sBannedDefines.add(getTrueTagKey(TAG_STRIP_BYTE_COUNTS)); + } + + /** + * Returns the constant representing a tag with a given TID and default IFD. + */ + public static int defineTag(int ifdId, short tagId) { + return (tagId & 0x0000ffff) | (ifdId << 16); + } + + /** + * Returns the TID for a tag constant. + */ + public static short getTrueTagKey(int tag) { + // Truncate + return (short) tag; + } + + /** + * Returns the default IFD for a tag constant. + */ + public static int getTrueIfd(int tag) { + return tag >>> 16; + } + + /** + * Constants for {@link TAG_ORIENTATION}. They can be interpreted as + * follows: + * <ul> + * <li>TOP_LEFT is the normal orientation.</li> + * <li>TOP_RIGHT is a left-right mirror.</li> + * <li>BOTTOM_LEFT is a 180 degree rotation.</li> + * <li>BOTTOM_RIGHT is a top-bottom mirror.</li> + * <li>LEFT_TOP is mirrored about the top-left<->bottom-right axis.</li> + * <li>RIGHT_TOP is a 90 degree clockwise rotation.</li> + * <li>LEFT_BOTTOM is mirrored about the top-right<->bottom-left axis.</li> + * <li>RIGHT_BOTTOM is a 270 degree clockwise rotation.</li> + * </ul> + */ + public static interface Orientation { + public static final short TOP_LEFT = 1; + public static final short TOP_RIGHT = 2; + public static final short BOTTOM_LEFT = 3; + public static final short BOTTOM_RIGHT = 4; + public static final short LEFT_TOP = 5; + public static final short RIGHT_TOP = 6; + public static final short LEFT_BOTTOM = 7; + public static final short RIGHT_BOTTOM = 8; + } + + /** + * Constants for {@link TAG_Y_CB_CR_POSITIONING} + */ + public static interface YCbCrPositioning { + public static final short CENTERED = 1; + public static final short CO_SITED = 2; + } + + /** + * Constants for {@link TAG_COMPRESSION} + */ + public static interface Compression { + public static final short UNCOMPRESSION = 1; + public static final short JPEG = 6; + } + + /** + * Constants for {@link TAG_RESOLUTION_UNIT} + */ + public static interface ResolutionUnit { + public static final short INCHES = 2; + public static final short CENTIMETERS = 3; + } + + /** + * Constants for {@link TAG_PHOTOMETRIC_INTERPRETATION} + */ + public static interface PhotometricInterpretation { + public static final short RGB = 2; + public static final short YCBCR = 6; + } + + /** + * Constants for {@link TAG_PLANAR_CONFIGURATION} + */ + public static interface PlanarConfiguration { + public static final short CHUNKY = 1; + public static final short PLANAR = 2; + } + + /** + * Constants for {@link TAG_EXPOSURE_PROGRAM} + */ + public static interface ExposureProgram { + public static final short NOT_DEFINED = 0; + public static final short MANUAL = 1; + public static final short NORMAL_PROGRAM = 2; + public static final short APERTURE_PRIORITY = 3; + public static final short SHUTTER_PRIORITY = 4; + public static final short CREATIVE_PROGRAM = 5; + public static final short ACTION_PROGRAM = 6; + public static final short PROTRAIT_MODE = 7; + public static final short LANDSCAPE_MODE = 8; + } + + /** + * Constants for {@link TAG_METERING_MODE} + */ + public static interface MeteringMode { + public static final short UNKNOWN = 0; + public static final short AVERAGE = 1; + public static final short CENTER_WEIGHTED_AVERAGE = 2; + public static final short SPOT = 3; + public static final short MULTISPOT = 4; + public static final short PATTERN = 5; + public static final short PARTAIL = 6; + public static final short OTHER = 255; + } + + /** + * Constants for {@link TAG_FLASH} As the definition in Jeita EXIF 2.2 + * standard, we can treat this constant as bitwise flag. + * <p> + * e.g. + * <p> + * short flash = FIRED | RETURN_STROBE_RETURN_LIGHT_DETECTED | + * MODE_AUTO_MODE + */ + public static interface Flash { + // LSB + public static final short DID_NOT_FIRED = 0; + public static final short FIRED = 1; + // 1st~2nd bits + public static final short RETURN_NO_STROBE_RETURN_DETECTION_FUNCTION = 0 << 1; + public static final short RETURN_STROBE_RETURN_LIGHT_NOT_DETECTED = 2 << 1; + public static final short RETURN_STROBE_RETURN_LIGHT_DETECTED = 3 << 1; + // 3rd~4th bits + public static final short MODE_UNKNOWN = 0 << 3; + public static final short MODE_COMPULSORY_FLASH_FIRING = 1 << 3; + public static final short MODE_COMPULSORY_FLASH_SUPPRESSION = 2 << 3; + public static final short MODE_AUTO_MODE = 3 << 3; + // 5th bit + public static final short FUNCTION_PRESENT = 0 << 5; + public static final short FUNCTION_NO_FUNCTION = 1 << 5; + // 6th bit + public static final short RED_EYE_REDUCTION_NO_OR_UNKNOWN = 0 << 6; + public static final short RED_EYE_REDUCTION_SUPPORT = 1 << 6; + } + + /** + * Constants for {@link TAG_COLOR_SPACE} + */ + public static interface ColorSpace { + public static final short SRGB = 1; + public static final short UNCALIBRATED = (short) 0xFFFF; + } + + /** + * Constants for {@link TAG_EXPOSURE_MODE} + */ + public static interface ExposureMode { + public static final short AUTO_EXPOSURE = 0; + public static final short MANUAL_EXPOSURE = 1; + public static final short AUTO_BRACKET = 2; + } + + /** + * Constants for {@link TAG_WHITE_BALANCE} + */ + public static interface WhiteBalance { + public static final short AUTO = 0; + public static final short MANUAL = 1; + } + + /** + * Constants for {@link TAG_SCENE_CAPTURE_TYPE} + */ + public static interface SceneCapture { + public static final short STANDARD = 0; + public static final short LANDSCAPE = 1; + public static final short PROTRAIT = 2; + public static final short NIGHT_SCENE = 3; + } + + /** + * Constants for {@link TAG_COMPONENTS_CONFIGURATION} + */ + public static interface ComponentsConfiguration { + public static final short NOT_EXIST = 0; + public static final short Y = 1; + public static final short CB = 2; + public static final short CR = 3; + public static final short R = 4; + public static final short G = 5; + public static final short B = 6; + } + + /** + * Constants for {@link TAG_LIGHT_SOURCE} + */ + public static interface LightSource { + public static final short UNKNOWN = 0; + public static final short DAYLIGHT = 1; + public static final short FLUORESCENT = 2; + public static final short TUNGSTEN = 3; + public static final short FLASH = 4; + public static final short FINE_WEATHER = 9; + public static final short CLOUDY_WEATHER = 10; + public static final short SHADE = 11; + public static final short DAYLIGHT_FLUORESCENT = 12; + public static final short DAY_WHITE_FLUORESCENT = 13; + public static final short COOL_WHITE_FLUORESCENT = 14; + public static final short WHITE_FLUORESCENT = 15; + public static final short STANDARD_LIGHT_A = 17; + public static final short STANDARD_LIGHT_B = 18; + public static final short STANDARD_LIGHT_C = 19; + public static final short D55 = 20; + public static final short D65 = 21; + public static final short D75 = 22; + public static final short D50 = 23; + public static final short ISO_STUDIO_TUNGSTEN = 24; + public static final short OTHER = 255; + } + + /** + * Constants for {@link TAG_SENSING_METHOD} + */ + public static interface SensingMethod { + public static final short NOT_DEFINED = 1; + public static final short ONE_CHIP_COLOR = 2; + public static final short TWO_CHIP_COLOR = 3; + public static final short THREE_CHIP_COLOR = 4; + public static final short COLOR_SEQUENTIAL_AREA = 5; + public static final short TRILINEAR = 7; + public static final short COLOR_SEQUENTIAL_LINEAR = 8; + } + + /** + * Constants for {@link TAG_FILE_SOURCE} + */ + public static interface FileSource { + public static final short DSC = 3; + } + + /** + * Constants for {@link TAG_SCENE_TYPE} + */ + public static interface SceneType { + public static final short DIRECT_PHOTOGRAPHED = 1; + } + + /** + * Constants for {@link TAG_GAIN_CONTROL} + */ + public static interface GainControl { + public static final short NONE = 0; + public static final short LOW_UP = 1; + public static final short HIGH_UP = 2; + public static final short LOW_DOWN = 3; + public static final short HIGH_DOWN = 4; + } + + /** + * Constants for {@link TAG_CONTRAST} + */ + public static interface Contrast { + public static final short NORMAL = 0; + public static final short SOFT = 1; + public static final short HARD = 2; + } + + /** + * Constants for {@link TAG_SATURATION} + */ + public static interface Saturation { + public static final short NORMAL = 0; + public static final short LOW = 1; + public static final short HIGH = 2; + } + + /** + * Constants for {@link TAG_SHARPNESS} + */ + public static interface Sharpness { + public static final short NORMAL = 0; + public static final short SOFT = 1; + public static final short HARD = 2; + } + + /** + * Constants for {@link TAG_SUBJECT_DISTANCE} + */ + public static interface SubjectDistance { + public static final short UNKNOWN = 0; + public static final short MACRO = 1; + public static final short CLOSE_VIEW = 2; + public static final short DISTANT_VIEW = 3; + } + + /** + * Constants for {@link TAG_GPS_LATITUDE_REF}, + * {@link TAG_GPS_DEST_LATITUDE_REF} + */ + public static interface GpsLatitudeRef { + public static final String NORTH = "N"; + public static final String SOUTH = "S"; + } + + /** + * Constants for {@link TAG_GPS_LONGITUDE_REF}, + * {@link TAG_GPS_DEST_LONGITUDE_REF} + */ + public static interface GpsLongitudeRef { + public static final String EAST = "E"; + public static final String WEST = "W"; + } + + /** + * Constants for {@link TAG_GPS_ALTITUDE_REF} + */ + public static interface GpsAltitudeRef { + public static final short SEA_LEVEL = 0; + public static final short SEA_LEVEL_NEGATIVE = 1; + } + + /** + * Constants for {@link TAG_GPS_STATUS} + */ + public static interface GpsStatus { + public static final String IN_PROGRESS = "A"; + public static final String INTEROPERABILITY = "V"; + } + + /** + * Constants for {@link TAG_GPS_MEASURE_MODE} + */ + public static interface GpsMeasureMode { + public static final String MODE_2_DIMENSIONAL = "2"; + public static final String MODE_3_DIMENSIONAL = "3"; + } + + /** + * Constants for {@link TAG_GPS_SPEED_REF}, + * {@link TAG_GPS_DEST_DISTANCE_REF} + */ + public static interface GpsSpeedRef { + public static final String KILOMETERS = "K"; + public static final String MILES = "M"; + public static final String KNOTS = "N"; + } + + /** + * Constants for {@link TAG_GPS_TRACK_REF}, + * {@link TAG_GPS_IMG_DIRECTION_REF}, {@link TAG_GPS_DEST_BEARING_REF} + */ + public static interface GpsTrackRef { + public static final String TRUE_DIRECTION = "T"; + public static final String MAGNETIC_DIRECTION = "M"; + } + + /** + * Constants for {@link TAG_GPS_DIFFERENTIAL} + */ + public static interface GpsDifferential { + public static final short WITHOUT_DIFFERENTIAL_CORRECTION = 0; + public static final short DIFFERENTIAL_CORRECTION_APPLIED = 1; + } + + private static final String NULL_ARGUMENT_STRING = "Argument is null"; + private ExifData mData = new ExifData(DEFAULT_BYTE_ORDER); + public static final ByteOrder DEFAULT_BYTE_ORDER = ByteOrder.BIG_ENDIAN; + + public ExifInterface() { + mGPSDateStampFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + /** + * Reads the exif tags from a byte array, clearing this ExifInterface + * object's existing exif tags. + * + * @param jpeg a byte array containing a jpeg compressed image. + * @throws IOException + */ + public void readExif(byte[] jpeg) throws IOException { + readExif(new ByteArrayInputStream(jpeg)); + } + + /** + * Reads the exif tags from an InputStream, clearing this ExifInterface + * object's existing exif tags. + * + * @param inStream an InputStream containing a jpeg compressed image. + * @throws IOException + */ + public void readExif(InputStream inStream) throws IOException { + if (inStream == null) { + throw new IllegalArgumentException(NULL_ARGUMENT_STRING); + } + ExifData d = null; + try { + d = new ExifReader(this).read(inStream); + } catch (ExifInvalidFormatException e) { + throw new IOException("Invalid exif format : " + e); + } + mData = d; + } + + /** + * Reads the exif tags from a file, clearing this ExifInterface object's + * existing exif tags. + * + * @param inFileName a string representing the filepath to jpeg file. + * @throws FileNotFoundException + * @throws IOException + */ + public void readExif(String inFileName) throws FileNotFoundException, IOException { + if (inFileName == null) { + throw new IllegalArgumentException(NULL_ARGUMENT_STRING); + } + InputStream is = null; + try { + is = (InputStream) new BufferedInputStream(new FileInputStream(inFileName)); + readExif(is); + } catch (IOException e) { + closeSilently(is); + throw e; + } + is.close(); + } + + /** + * Sets the exif tags, clearing this ExifInterface object's existing exif + * tags. + * + * @param tags a collection of exif tags to set. + */ + public void setExif(Collection<ExifTag> tags) { + clearExif(); + setTags(tags); + } + + /** + * Clears this ExifInterface object's existing exif tags. + */ + public void clearExif() { + mData = new ExifData(DEFAULT_BYTE_ORDER); + } + + /** + * Writes the tags from this ExifInterface object into a jpeg image, + * removing prior exif tags. + * + * @param jpeg a byte array containing a jpeg compressed image. + * @param exifOutStream an OutputStream to which the jpeg image with added + * exif tags will be written. + * @throws IOException + */ + public void writeExif(byte[] jpeg, OutputStream exifOutStream) throws IOException { + if (jpeg == null || exifOutStream == null) { + throw new IllegalArgumentException(NULL_ARGUMENT_STRING); + } + OutputStream s = getExifWriterStream(exifOutStream); + s.write(jpeg, 0, jpeg.length); + s.flush(); + } + + /** + * Writes the tags from this ExifInterface object into a jpeg compressed + * bitmap, removing prior exif tags. + * + * @param bmap a bitmap to compress and write exif into. + * @param exifOutStream the OutputStream to which the jpeg image with added + * exif tags will be written. + * @throws IOException + */ + public void writeExif(Bitmap bmap, OutputStream exifOutStream) throws IOException { + if (bmap == null || exifOutStream == null) { + throw new IllegalArgumentException(NULL_ARGUMENT_STRING); + } + OutputStream s = getExifWriterStream(exifOutStream); + bmap.compress(Bitmap.CompressFormat.JPEG, 90, s); + s.flush(); + } + + /** + * Writes the tags from this ExifInterface object into a jpeg stream, + * removing prior exif tags. + * + * @param jpegStream an InputStream containing a jpeg compressed image. + * @param exifOutStream an OutputStream to which the jpeg image with added + * exif tags will be written. + * @throws IOException + */ + public void writeExif(InputStream jpegStream, OutputStream exifOutStream) throws IOException { + if (jpegStream == null || exifOutStream == null) { + throw new IllegalArgumentException(NULL_ARGUMENT_STRING); + } + OutputStream s = getExifWriterStream(exifOutStream); + doExifStreamIO(jpegStream, s); + s.flush(); + } + + /** + * Writes the tags from this ExifInterface object into a jpeg image, + * removing prior exif tags. + * + * @param jpeg a byte array containing a jpeg compressed image. + * @param exifOutFileName a String containing the filepath to which the jpeg + * image with added exif tags will be written. + * @throws FileNotFoundException + * @throws IOException + */ + public void writeExif(byte[] jpeg, String exifOutFileName) throws FileNotFoundException, + IOException { + if (jpeg == null || exifOutFileName == null) { + throw new IllegalArgumentException(NULL_ARGUMENT_STRING); + } + OutputStream s = null; + try { + s = getExifWriterStream(exifOutFileName); + s.write(jpeg, 0, jpeg.length); + s.flush(); + } catch (IOException e) { + closeSilently(s); + throw e; + } + s.close(); + } + + /** + * Writes the tags from this ExifInterface object into a jpeg compressed + * bitmap, removing prior exif tags. + * + * @param bmap a bitmap to compress and write exif into. + * @param exifOutFileName a String containing the filepath to which the jpeg + * image with added exif tags will be written. + * @throws FileNotFoundException + * @throws IOException + */ + public void writeExif(Bitmap bmap, String exifOutFileName) throws FileNotFoundException, + IOException { + if (bmap == null || exifOutFileName == null) { + throw new IllegalArgumentException(NULL_ARGUMENT_STRING); + } + OutputStream s = null; + try { + s = getExifWriterStream(exifOutFileName); + bmap.compress(Bitmap.CompressFormat.JPEG, 90, s); + s.flush(); + } catch (IOException e) { + closeSilently(s); + throw e; + } + s.close(); + } + + /** + * Writes the tags from this ExifInterface object into a jpeg stream, + * removing prior exif tags. + * + * @param jpegStream an InputStream containing a jpeg compressed image. + * @param exifOutFileName a String containing the filepath to which the jpeg + * image with added exif tags will be written. + * @throws FileNotFoundException + * @throws IOException + */ + public void writeExif(InputStream jpegStream, String exifOutFileName) + throws FileNotFoundException, IOException { + if (jpegStream == null || exifOutFileName == null) { + throw new IllegalArgumentException(NULL_ARGUMENT_STRING); + } + OutputStream s = null; + try { + s = getExifWriterStream(exifOutFileName); + doExifStreamIO(jpegStream, s); + s.flush(); + } catch (IOException e) { + closeSilently(s); + throw e; + } + s.close(); + } + + /** + * Writes the tags from this ExifInterface object into a jpeg file, removing + * prior exif tags. + * + * @param jpegFileName a String containing the filepath for a jpeg file. + * @param exifOutFileName a String containing the filepath to which the jpeg + * image with added exif tags will be written. + * @throws FileNotFoundException + * @throws IOException + */ + public void writeExif(String jpegFileName, String exifOutFileName) + throws FileNotFoundException, IOException { + if (jpegFileName == null || exifOutFileName == null) { + throw new IllegalArgumentException(NULL_ARGUMENT_STRING); + } + InputStream is = null; + try { + is = new FileInputStream(jpegFileName); + writeExif(is, exifOutFileName); + } catch (IOException e) { + closeSilently(is); + throw e; + } + is.close(); + } + + /** + * Wraps an OutputStream object with an ExifOutputStream. Exif tags in this + * ExifInterface object will be added to a jpeg image written to this + * stream, removing prior exif tags. Other methods of this ExifInterface + * object should not be called until the returned OutputStream has been + * closed. + * + * @param outStream an OutputStream to wrap. + * @return an OutputStream that wraps the outStream parameter, and adds exif + * metadata. A jpeg image should be written to this stream. + */ + public OutputStream getExifWriterStream(OutputStream outStream) { + if (outStream == null) { + throw new IllegalArgumentException(NULL_ARGUMENT_STRING); + } + ExifOutputStream eos = new ExifOutputStream(outStream, this); + eos.setExifData(mData); + return eos; + } + + /** + * Returns an OutputStream object that writes to a file. Exif tags in this + * ExifInterface object will be added to a jpeg image written to this + * stream, removing prior exif tags. Other methods of this ExifInterface + * object should not be called until the returned OutputStream has been + * closed. + * + * @param exifOutFileName an String containing a filepath for a jpeg file. + * @return an OutputStream that writes to the exifOutFileName file, and adds + * exif metadata. A jpeg image should be written to this stream. + * @throws FileNotFoundException + */ + public OutputStream getExifWriterStream(String exifOutFileName) throws FileNotFoundException { + if (exifOutFileName == null) { + throw new IllegalArgumentException(NULL_ARGUMENT_STRING); + } + OutputStream out = null; + try { + out = (OutputStream) new FileOutputStream(exifOutFileName); + } catch (FileNotFoundException e) { + closeSilently(out); + throw e; + } + return getExifWriterStream(out); + } + + /** + * Attempts to do an in-place rewrite the exif metadata in a file for the + * given tags. If tags do not exist or do not have the same size as the + * existing exif tags, this method will fail. + * + * @param filename a String containing a filepath for a jpeg file with exif + * tags to rewrite. + * @param tags tags that will be written into the jpeg file over existing + * tags if possible. + * @return true if success, false if could not overwrite. If false, no + * changes are made to the file. + * @throws FileNotFoundException + * @throws IOException + */ + public boolean rewriteExif(String filename, Collection<ExifTag> tags) + throws FileNotFoundException, IOException { + RandomAccessFile file = null; + InputStream is = null; + boolean ret; + try { + File temp = new File(filename); + is = new BufferedInputStream(new FileInputStream(temp)); + + // Parse beginning of APP1 in exif to find size of exif header. + ExifParser parser = null; + try { + parser = ExifParser.parse(is, this); + } catch (ExifInvalidFormatException e) { + throw new IOException("Invalid exif format : ", e); + } + long exifSize = parser.getOffsetToExifEndFromSOF(); + + // Free up resources + is.close(); + is = null; + + // Open file for memory mapping. + file = new RandomAccessFile(temp, "rw"); + long fileLength = file.length(); + if (fileLength < exifSize) { + throw new IOException("Filesize changed during operation"); + } + + // Map only exif header into memory. + ByteBuffer buf = file.getChannel().map(MapMode.READ_WRITE, 0, exifSize); + + // Attempt to overwrite tag values without changing lengths (avoids + // file copy). + ret = rewriteExif(buf, tags); + } catch (IOException e) { + closeSilently(file); + throw e; + } finally { + closeSilently(is); + } + file.close(); + return ret; + } + + /** + * Attempts to do an in-place rewrite the exif metadata in a ByteBuffer for + * the given tags. If tags do not exist or do not have the same size as the + * existing exif tags, this method will fail. + * + * @param buf a ByteBuffer containing a jpeg file with existing exif tags to + * rewrite. + * @param tags tags that will be written into the jpeg ByteBuffer over + * existing tags if possible. + * @return true if success, false if could not overwrite. If false, no + * changes are made to the ByteBuffer. + * @throws IOException + */ + public boolean rewriteExif(ByteBuffer buf, Collection<ExifTag> tags) throws IOException { + ExifModifier mod = null; + try { + mod = new ExifModifier(buf, this); + for (ExifTag t : tags) { + mod.modifyTag(t); + } + return mod.commit(); + } catch (ExifInvalidFormatException e) { + throw new IOException("Invalid exif format : " + e); + } + } + + /** + * Attempts to do an in-place rewrite of the exif metadata. If this fails, + * fall back to overwriting file. This preserves tags that are not being + * rewritten. + * + * @param filename a String containing a filepath for a jpeg file. + * @param tags tags that will be written into the jpeg file over existing + * tags if possible. + * @throws FileNotFoundException + * @throws IOException + * @see #rewriteExif + */ + public void forceRewriteExif(String filename, Collection<ExifTag> tags) + throws FileNotFoundException, + IOException { + // Attempt in-place write + if (!rewriteExif(filename, tags)) { + // Fall back to doing a copy + ExifData tempData = mData; + mData = new ExifData(DEFAULT_BYTE_ORDER); + FileInputStream is = null; + ByteArrayOutputStream bytes = null; + try { + is = new FileInputStream(filename); + bytes = new ByteArrayOutputStream(); + doExifStreamIO(is, bytes); + byte[] imageBytes = bytes.toByteArray(); + readExif(imageBytes); + setTags(tags); + writeExif(imageBytes, filename); + } catch (IOException e) { + closeSilently(is); + throw e; + } finally { + is.close(); + // Prevent clobbering of mData + mData = tempData; + } + } + } + + /** + * Attempts to do an in-place rewrite of the exif metadata using the tags in + * this ExifInterface object. If this fails, fall back to overwriting file. + * This preserves tags that are not being rewritten. + * + * @param filename a String containing a filepath for a jpeg file. + * @throws FileNotFoundException + * @throws IOException + * @see #rewriteExif + */ + public void forceRewriteExif(String filename) throws FileNotFoundException, IOException { + forceRewriteExif(filename, getAllTags()); + } + + /** + * Get the exif tags in this ExifInterface object or null if none exist. + * + * @return a List of {@link ExifTag}s. + */ + public List<ExifTag> getAllTags() { + return mData.getAllTags(); + } + + /** + * Returns a list of ExifTags that share a TID (which can be obtained by + * calling {@link #getTrueTagKey} on a defined tag constant) or null if none + * exist. + * + * @param tagId a TID as defined in the exif standard (or with + * {@link #defineTag}). + * @return a List of {@link ExifTag}s. + */ + public List<ExifTag> getTagsForTagId(short tagId) { + return mData.getAllTagsForTagId(tagId); + } + + /** + * Returns a list of ExifTags that share an IFD (which can be obtained by + * calling {@link #getTrueIFD} on a defined tag constant) or null if none + * exist. + * + * @param ifdId an IFD as defined in the exif standard (or with + * {@link #defineTag}). + * @return a List of {@link ExifTag}s. + */ + public List<ExifTag> getTagsForIfdId(int ifdId) { + return mData.getAllTagsForIfd(ifdId); + } + + /** + * Gets an ExifTag for an IFD other than the tag's default. + * + * @see #getTag + */ + public ExifTag getTag(int tagId, int ifdId) { + if (!ExifTag.isValidIfd(ifdId)) { + return null; + } + return mData.getTag(getTrueTagKey(tagId), ifdId); + } + + /** + * Returns the ExifTag in that tag's default IFD for a defined tag constant + * or null if none exists. + * + * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. + * @return an {@link ExifTag} or null if none exists. + */ + public ExifTag getTag(int tagId) { + int ifdId = getDefinedTagDefaultIfd(tagId); + return getTag(tagId, ifdId); + } + + /** + * Gets a tag value for an IFD other than the tag's default. + * + * @see #getTagValue + */ + public Object getTagValue(int tagId, int ifdId) { + ExifTag t = getTag(tagId, ifdId); + return (t == null) ? null : t.getValue(); + } + + /** + * Returns the value of the ExifTag in that tag's default IFD for a defined + * tag constant or null if none exists or the value could not be cast into + * the return type. + * + * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. + * @return the value of the ExifTag or null if none exists. + */ + public Object getTagValue(int tagId) { + int ifdId = getDefinedTagDefaultIfd(tagId); + return getTagValue(tagId, ifdId); + } + + /* + * Getter methods that are similar to getTagValue. Null is returned if the + * tag value cannot be cast into the return type. + */ + + /** + * @see #getTagValue + */ + public String getTagStringValue(int tagId, int ifdId) { + ExifTag t = getTag(tagId, ifdId); + if (t == null) { + return null; + } + return t.getValueAsString(); + } + + /** + * @see #getTagValue + */ + public String getTagStringValue(int tagId) { + int ifdId = getDefinedTagDefaultIfd(tagId); + return getTagStringValue(tagId, ifdId); + } + + /** + * @see #getTagValue + */ + public Long getTagLongValue(int tagId, int ifdId) { + long[] l = getTagLongValues(tagId, ifdId); + if (l == null || l.length <= 0) { + return null; + } + return 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} + * <p> + * Note: defining tags with these TID's is disallowed. + * + * @param tag a tag's TID (can be obtained from a defined tag constant with + * {@link #getTrueTagKey}). + * @return true if the TID is that of an offset tag. + */ + protected static boolean isOffsetTag(short tag) { + return sOffsetTags.contains(tag); + } + + /** + * Creates a tag for a defined tag constant in a given IFD if that IFD is + * allowed for the tag. This method will fail anytime the appropriate + * {@link ExifTag#setValue} for this tag's datatype would fail. + * + * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. + * @param ifdId the IFD that the tag should be in. + * @param val the value of the tag to set. + * @return an ExifTag object or null if one could not be constructed. + * @see #buildTag + */ + public ExifTag buildTag(int tagId, int ifdId, Object val) { + int info = getTagInfo().get(tagId); + if (info == 0 || val == null) { + return null; + } + short type = getTypeFromInfo(info); + int definedCount = getComponentCountFromInfo(info); + boolean hasDefinedCount = (definedCount != ExifTag.SIZE_UNDEFINED); + if (!ExifInterface.isIfdAllowed(info, ifdId)) { + return null; + } + ExifTag t = new ExifTag(getTrueTagKey(tagId), type, definedCount, ifdId, hasDefinedCount); + if (!t.setValue(val)) { + return null; + } + return t; + } + + /** + * Creates a tag for a defined tag constant in the tag's default IFD. + * + * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. + * @param val the tag's value. + * @return an ExifTag object. + */ + public ExifTag buildTag(int tagId, Object val) { + int ifdId = getTrueIfd(tagId); + return buildTag(tagId, ifdId, val); + } + + protected ExifTag buildUninitializedTag(int tagId) { + int info = getTagInfo().get(tagId); + if (info == 0) { + return null; + } + short type = getTypeFromInfo(info); + int definedCount = getComponentCountFromInfo(info); + boolean hasDefinedCount = (definedCount != ExifTag.SIZE_UNDEFINED); + int ifdId = getTrueIfd(tagId); + ExifTag t = new ExifTag(getTrueTagKey(tagId), type, definedCount, ifdId, hasDefinedCount); + return t; + } + + /** + * Sets the value of an ExifTag if it exists in the given IFD. The value + * must be the correct type and length for that ExifTag. + * + * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. + * @param ifdId the IFD that the ExifTag is in. + * @param val the value to set. + * @return true if success, false if the ExifTag doesn't exist or the value + * is the wrong type/length. + * @see #setTagValue + */ + public boolean setTagValue(int tagId, int ifdId, Object val) { + ExifTag t = getTag(tagId, ifdId); + if (t == null) { + return false; + } + return t.setValue(val); + } + + /** + * Sets the value of an ExifTag if it exists it's default IFD. The value + * must be the correct type and length for that ExifTag. + * + * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. + * @param val the value to set. + * @return true if success, false if the ExifTag doesn't exist or the value + * is the wrong type/length. + */ + public boolean setTagValue(int tagId, Object val) { + int ifdId = getDefinedTagDefaultIfd(tagId); + return setTagValue(tagId, ifdId, val); + } + + /** + * Puts an ExifTag into this ExifInterface object's tags, removing a + * previous ExifTag with the same TID and IFD. The IFD it is put into will + * be the one the tag was created with in {@link #buildTag}. + * + * @param tag an ExifTag to put into this ExifInterface's tags. + * @return the previous ExifTag with the same TID and IFD or null if none + * exists. + */ + public ExifTag setTag(ExifTag tag) { + return mData.addTag(tag); + } + + /** + * Puts a collection of ExifTags into this ExifInterface objects's tags. Any + * previous ExifTags with the same TID and IFDs will be removed. + * + * @param tags a Collection of ExifTags. + * @see #setTag + */ + public void setTags(Collection<ExifTag> tags) { + for (ExifTag t : tags) { + setTag(t); + } + } + + /** + * Removes the ExifTag for a tag constant from the given IFD. + * + * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. + * @param ifdId the IFD of the ExifTag to remove. + */ + public void deleteTag(int tagId, int ifdId) { + mData.removeTag(getTrueTagKey(tagId), ifdId); + } + + /** + * Removes the ExifTag for a tag constant from that tag's default IFD. + * + * @param tagId a tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. + */ + public void deleteTag(int tagId) { + int ifdId = getDefinedTagDefaultIfd(tagId); + deleteTag(tagId, ifdId); + } + + /** + * Creates a new tag definition in this ExifInterface object for a given TID + * and default IFD. Creating a definition with the same TID and default IFD + * as a previous definition will override it. + * + * @param tagId the TID for the tag. + * @param defaultIfd the default IFD for the tag. + * @param tagType the type of the tag (see {@link ExifTag#getDataType()}). + * @param defaultComponentCount the number of elements of this tag's type in + * the tags value. + * @param allowedIfds the IFD's this tag is allowed to be put in. + * @return the defined tag constant (e.g. {@link #TAG_IMAGE_WIDTH}) or + * {@link #TAG_NULL} if the definition could not be made. + */ + public int setTagDefinition(short tagId, int defaultIfd, short tagType, + short defaultComponentCount, int[] allowedIfds) { + if (sBannedDefines.contains(tagId)) { + return TAG_NULL; + } + if (ExifTag.isValidType(tagType) && ExifTag.isValidIfd(defaultIfd)) { + int tagDef = defineTag(defaultIfd, tagId); + if (tagDef == TAG_NULL) { + return TAG_NULL; + } + int[] otherDefs = getTagDefinitionsForTagId(tagId); + SparseIntArray infos = getTagInfo(); + // Make sure defaultIfd is in allowedIfds + boolean defaultCheck = false; + for (int i : allowedIfds) { + if (defaultIfd == i) { + defaultCheck = true; + } + if (!ExifTag.isValidIfd(i)) { + return TAG_NULL; + } + } + if (!defaultCheck) { + return TAG_NULL; + } + + int ifdFlags = getFlagsFromAllowedIfds(allowedIfds); + // Make sure no identical tags can exist in allowedIfds + if (otherDefs != null) { + for (int def : otherDefs) { + int tagInfo = infos.get(def); + int allowedFlags = getAllowedIfdFlagsFromInfo(tagInfo); + if ((ifdFlags & allowedFlags) != 0) { + return TAG_NULL; + } + } + } + getTagInfo().put(tagDef, ifdFlags << 24 | (tagType << 16) | defaultComponentCount); + return tagDef; + } + return TAG_NULL; + } + + protected int getTagDefinition(short tagId, int defaultIfd) { + return getTagInfo().get(defineTag(defaultIfd, tagId)); + } + + protected int[] getTagDefinitionsForTagId(short tagId) { + int[] ifds = IfdData.getIfds(); + int[] defs = new int[ifds.length]; + int counter = 0; + SparseIntArray infos = getTagInfo(); + for (int i : ifds) { + int def = defineTag(i, tagId); + if (infos.get(def) != DEFINITION_NULL) { + defs[counter++] = def; + } + } + if (counter == 0) { + return null; + } + + return Arrays.copyOfRange(defs, 0, counter); + } + + protected int getTagDefinitionForTag(ExifTag tag) { + short type = tag.getDataType(); + int count = tag.getComponentCount(); + int ifd = tag.getIfd(); + return getTagDefinitionForTag(tag.getTagId(), type, count, ifd); + } + + protected int getTagDefinitionForTag(short tagId, short type, int count, int ifd) { + int[] defs = getTagDefinitionsForTagId(tagId); + if (defs == null) { + return TAG_NULL; + } + SparseIntArray infos = getTagInfo(); + int ret = TAG_NULL; + for (int i : defs) { + int info = infos.get(i); + short def_type = getTypeFromInfo(info); + int def_count = getComponentCountFromInfo(info); + int[] def_ifds = getAllowedIfdsFromInfo(info); + boolean valid_ifd = false; + for (int j : def_ifds) { + if (j == ifd) { + valid_ifd = true; + break; + } + } + if (valid_ifd && type == def_type + && (count == def_count || def_count == ExifTag.SIZE_UNDEFINED)) { + ret = i; + break; + } + } + return ret; + } + + /** + * Removes a tag definition for given defined tag constant. + * + * @param tagId a defined tag constant, e.g. {@link #TAG_IMAGE_WIDTH}. + */ + public void removeTagDefinition(int tagId) { + getTagInfo().delete(tagId); + } + + /** + * Resets tag definitions to the default ones. + */ + public void resetTagDefinitions() { + mTagInfo = null; + } + + /** + * Returns the thumbnail from IFD1 as a bitmap, or null if none exists. + * + * @return the thumbnail as a bitmap. + */ + public Bitmap getThumbnailBitmap() { + if (mData.hasCompressedThumbnail()) { + byte[] thumb = mData.getCompressedThumbnail(); + return BitmapFactory.decodeByteArray(thumb, 0, thumb.length); + } else if (mData.hasUncompressedStrip()) { + // TODO: implement uncompressed + } + return null; + } + + /** + * Returns the thumbnail from IFD1 as a byte array, or null if none exists. + * The bytes may either be an uncompressed strip as specified in the exif + * standard or a jpeg compressed image. + * + * @return the thumbnail as a byte array. + */ + public byte[] getThumbnailBytes() { + if (mData.hasCompressedThumbnail()) { + return mData.getCompressedThumbnail(); + } else if (mData.hasUncompressedStrip()) { + // TODO: implement this + } + return null; + } + + /** + * Returns the thumbnail if it is jpeg compressed, or null if none exists. + * + * @return the thumbnail as a byte array. + */ + public byte[] getThumbnail() { + return mData.getCompressedThumbnail(); + } + + /** + * Check if thumbnail is compressed. + * + * @return true if the thumbnail is compressed. + */ + public boolean isThumbnailCompressed() { + return mData.hasCompressedThumbnail(); + } + + /** + * Check if thumbnail exists. + * + * @return true if a compressed thumbnail exists. + */ + public boolean hasThumbnail() { + // TODO: add back in uncompressed strip + return mData.hasCompressedThumbnail(); + } + + // TODO: uncompressed thumbnail setters + + /** + * Sets the thumbnail to be a jpeg compressed image. Clears any prior + * thumbnail. + * + * @param thumb a byte array containing a jpeg compressed image. + * @return true if the thumbnail was set. + */ + public boolean setCompressedThumbnail(byte[] thumb) { + mData.clearThumbnailAndStrips(); + mData.setCompressedThumbnail(thumb); + return true; + } + + /** + * Sets the thumbnail to be a jpeg compressed bitmap. Clears any prior + * thumbnail. + * + * @param thumb a bitmap to compress to a jpeg thumbnail. + * @return true if the thumbnail was set. + */ + public boolean setCompressedThumbnail(Bitmap thumb) { + ByteArrayOutputStream thumbnail = new ByteArrayOutputStream(); + if (!thumb.compress(Bitmap.CompressFormat.JPEG, 90, thumbnail)) { + return false; + } + return setCompressedThumbnail(thumbnail.toByteArray()); + } + + /** + * Clears the compressed thumbnail if it exists. + */ + public void removeCompressedThumbnail() { + mData.setCompressedThumbnail(null); + } + + // Convenience methods: + + /** + * Decodes the user comment tag into string as specified in the EXIF + * standard. Returns null if decoding failed. + */ + public String getUserComment() { + return mData.getUserComment(); + } + + /** + * Returns the Orientation ExifTag value for a given number of degrees. + * + * @param degrees the amount an image is rotated in degrees. + */ + public static short getOrientationValueForRotation(int degrees) { + degrees %= 360; + if (degrees < 0) { + degrees += 360; + } + if (degrees < 90) { + return Orientation.TOP_LEFT; // 0 degrees + } else if (degrees < 180) { + return Orientation.RIGHT_TOP; // 90 degrees cw + } else if (degrees < 270) { + return Orientation.BOTTOM_LEFT; // 180 degrees + } else { + return Orientation.RIGHT_BOTTOM; // 270 degrees cw + } + } + + /** + * Returns the rotation degrees corresponding to an ExifTag Orientation + * value. + * + * @param orientation the ExifTag Orientation value. + */ + public static int getRotationForOrientationValue(short orientation) { + switch (orientation) { + case Orientation.TOP_LEFT: + return 0; + case Orientation.RIGHT_TOP: + return 90; + case Orientation.BOTTOM_LEFT: + return 180; + case Orientation.RIGHT_BOTTOM: + return 270; + default: + return 0; + } + } + + /** + * Gets the double representation of the GPS latitude or longitude + * coordinate. + * + * @param coordinate an array of 3 Rationals representing the degrees, + * minutes, and seconds of the GPS location as defined in the + * exif specification. + * @param reference a GPS reference reperesented by a String containing "N", + * "S", "E", or "W". + * @return the GPS coordinate represented as degrees + minutes/60 + + * seconds/3600 + */ + public static double convertLatOrLongToDouble(Rational[] coordinate, String reference) { + try { + double degrees = coordinate[0].toDouble(); + double minutes = coordinate[1].toDouble(); + double seconds = coordinate[2].toDouble(); + double result = degrees + minutes / 60.0 + seconds / 3600.0; + if ((reference.equals("S") || reference.equals("W"))) { + return -result; + } + return result; + } catch (ArrayIndexOutOfBoundsException e) { + throw new IllegalArgumentException(); + } + } + + /** + * Gets the GPS latitude and longitude as a pair of doubles from this + * ExifInterface object's tags, or null if the necessary tags do not exist. + * + * @return an array of 2 doubles containing the latitude, and longitude + * respectively. + * @see #convertLatOrLongToDouble + */ + public double[] getLatLongAsDoubles() { + Rational[] latitude = getTagRationalValues(TAG_GPS_LATITUDE); + String latitudeRef = getTagStringValue(TAG_GPS_LATITUDE_REF); + Rational[] longitude = getTagRationalValues(TAG_GPS_LONGITUDE); + String longitudeRef = getTagStringValue(TAG_GPS_LONGITUDE_REF); + if (latitude == null || longitude == null || latitudeRef == null || longitudeRef == null + || latitude.length < 3 || longitude.length < 3) { + return null; + } + double[] latLon = new double[2]; + latLon[0] = convertLatOrLongToDouble(latitude, latitudeRef); + latLon[1] = convertLatOrLongToDouble(longitude, longitudeRef); + return latLon; + } + + private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd"; + private static final String DATETIME_FORMAT_STR = "yyyy:MM:dd kk:mm:ss"; + private final DateFormat mDateTimeStampFormat = new SimpleDateFormat(DATETIME_FORMAT_STR); + private final DateFormat mGPSDateStampFormat = new SimpleDateFormat(GPS_DATE_FORMAT_STR); + private final Calendar mGPSTimeStampCalendar = Calendar + .getInstance(TimeZone.getTimeZone("UTC")); + + /** + * Creates, formats, and sets the DateTimeStamp tag for one of: + * {@link #TAG_DATE_TIME}, {@link #TAG_DATE_TIME_DIGITIZED}, + * {@link #TAG_DATE_TIME_ORIGINAL}. + * + * @param tagId one of the DateTimeStamp tags. + * @param timestamp a timestamp to format. + * @param timezone a TimeZone object. + * @return true if success, false if the tag could not be set. + */ + public boolean addDateTimeStampTag(int tagId, long timestamp, TimeZone timezone) { + if (tagId == TAG_DATE_TIME || tagId == TAG_DATE_TIME_DIGITIZED + || tagId == TAG_DATE_TIME_ORIGINAL) { + mDateTimeStampFormat.setTimeZone(timezone); + ExifTag t = buildTag(tagId, mDateTimeStampFormat.format(timestamp)); + if (t == null) { + return false; + } + setTag(t); + } else { + return false; + } + return true; + } + + /** + * Creates and sets all to the GPS tags for a give latitude and longitude. + * + * @param latitude a GPS latitude coordinate. + * @param longitude a GPS longitude coordinate. + * @return true if success, false if they could not be created or set. + */ + public boolean addGpsTags(double latitude, double longitude) { + ExifTag latTag = buildTag(TAG_GPS_LATITUDE, toExifLatLong(latitude)); + ExifTag longTag = buildTag(TAG_GPS_LONGITUDE, toExifLatLong(longitude)); + ExifTag latRefTag = buildTag(TAG_GPS_LATITUDE_REF, + latitude >= 0 ? ExifInterface.GpsLatitudeRef.NORTH + : ExifInterface.GpsLatitudeRef.SOUTH); + ExifTag longRefTag = buildTag(TAG_GPS_LONGITUDE_REF, + longitude >= 0 ? ExifInterface.GpsLongitudeRef.EAST + : ExifInterface.GpsLongitudeRef.WEST); + if (latTag == null || longTag == null || latRefTag == null || longRefTag == null) { + return false; + } + setTag(latTag); + setTag(longTag); + setTag(latRefTag); + setTag(longRefTag); + return true; + } + + /** + * Creates and sets the GPS timestamp tag. + * + * @param timestamp a GPS timestamp. + * @return true if success, false if could not be created or set. + */ + public boolean addGpsDateTimeStampTag(long timestamp) { + ExifTag t = buildTag(TAG_GPS_DATE_STAMP, mGPSDateStampFormat.format(timestamp)); + if (t == null) { + return false; + } + setTag(t); + mGPSTimeStampCalendar.setTimeInMillis(timestamp); + t = buildTag(TAG_GPS_TIME_STAMP, new Rational[] { + new Rational(mGPSTimeStampCalendar.get(Calendar.HOUR_OF_DAY), 1), + new Rational(mGPSTimeStampCalendar.get(Calendar.MINUTE), 1), + new Rational(mGPSTimeStampCalendar.get(Calendar.SECOND), 1) + }); + if (t == null) { + return false; + } + setTag(t); + return true; + } + + private static Rational[] toExifLatLong(double value) { + // convert to the format dd/1 mm/1 ssss/100 + value = Math.abs(value); + int degrees = (int) value; + value = (value - degrees) * 60; + int minutes = (int) value; + value = (value - minutes) * 6000; + int seconds = (int) value; + return new Rational[] { + new Rational(degrees, 1), new Rational(minutes, 1), new Rational(seconds, 100) + }; + } + + private void doExifStreamIO(InputStream is, OutputStream os) throws IOException { + byte[] buf = new byte[1024]; + int ret = is.read(buf, 0, 1024); + while (ret != -1) { + os.write(buf, 0, ret); + ret = is.read(buf, 0, 1024); + } + } + + protected static void closeSilently(Closeable c) { + if (c != null) { + try { + c.close(); + } catch (Throwable e) { + // ignored + } + } + } + + private SparseIntArray mTagInfo = null; + + protected SparseIntArray getTagInfo() { + if (mTagInfo == null) { + mTagInfo = new SparseIntArray(); + initTagInfo(); + } + return mTagInfo; + } + + private void initTagInfo() { + /** + * We put tag information in a 4-bytes integer. The first byte a bitmask + * representing the allowed IFDs of the tag, the second byte is the data + * type, and the last two byte are a short value indicating the default + * component count of this tag. + */ + // IFD0 tags + int[] ifdAllowedIfds = { + IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1 + }; + int ifdFlags = getFlagsFromAllowedIfds(ifdAllowedIfds) << 24; + mTagInfo.put(ExifInterface.TAG_MAKE, + ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_IMAGE_WIDTH, + ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); + mTagInfo.put(ExifInterface.TAG_IMAGE_LENGTH, + ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); + mTagInfo.put(ExifInterface.TAG_BITS_PER_SAMPLE, + ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 3); + mTagInfo.put(ExifInterface.TAG_COMPRESSION, + ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION, + ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_ORIENTATION, ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 + | 1); + mTagInfo.put(ExifInterface.TAG_SAMPLES_PER_PIXEL, + ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_PLANAR_CONFIGURATION, + ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING, + ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 2); + mTagInfo.put(ExifInterface.TAG_Y_CB_CR_POSITIONING, + ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_X_RESOLUTION, + ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_Y_RESOLUTION, + ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_RESOLUTION_UNIT, + ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_STRIP_OFFSETS, + ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_ROWS_PER_STRIP, + ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); + mTagInfo.put(ExifInterface.TAG_STRIP_BYTE_COUNTS, + ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_TRANSFER_FUNCTION, + ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 3 * 256); + mTagInfo.put(ExifInterface.TAG_WHITE_POINT, + ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 2); + mTagInfo.put(ExifInterface.TAG_PRIMARY_CHROMATICITIES, + ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 6); + mTagInfo.put(ExifInterface.TAG_Y_CB_CR_COEFFICIENTS, + ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 3); + mTagInfo.put(ExifInterface.TAG_REFERENCE_BLACK_WHITE, + ifdFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 6); + mTagInfo.put(ExifInterface.TAG_DATE_TIME, + ifdFlags | ExifTag.TYPE_ASCII << 16 | 20); + mTagInfo.put(ExifInterface.TAG_IMAGE_DESCRIPTION, + ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_MAKE, + ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_MODEL, + ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_SOFTWARE, + ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_ARTIST, + ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_COPYRIGHT, + ifdFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_EXIF_IFD, + ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); + mTagInfo.put(ExifInterface.TAG_GPS_IFD, + ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); + // IFD1 tags + int[] ifd1AllowedIfds = { + IfdId.TYPE_IFD_1 + }; + int ifdFlags1 = getFlagsFromAllowedIfds(ifd1AllowedIfds) << 24; + mTagInfo.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, + ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); + mTagInfo.put(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, + ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); + // Exif tags + int[] exifAllowedIfds = { + IfdId.TYPE_IFD_EXIF + }; + int exifFlags = getFlagsFromAllowedIfds(exifAllowedIfds) << 24; + mTagInfo.put(ExifInterface.TAG_EXIF_VERSION, + exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4); + mTagInfo.put(ExifInterface.TAG_FLASHPIX_VERSION, + exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4); + mTagInfo.put(ExifInterface.TAG_COLOR_SPACE, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_COMPONENTS_CONFIGURATION, + exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 4); + mTagInfo.put(ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_PIXEL_X_DIMENSION, + exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); + mTagInfo.put(ExifInterface.TAG_PIXEL_Y_DIMENSION, + exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); + mTagInfo.put(ExifInterface.TAG_MAKER_NOTE, + exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_USER_COMMENT, + exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_RELATED_SOUND_FILE, + exifFlags | ExifTag.TYPE_ASCII << 16 | 13); + mTagInfo.put(ExifInterface.TAG_DATE_TIME_ORIGINAL, + exifFlags | ExifTag.TYPE_ASCII << 16 | 20); + mTagInfo.put(ExifInterface.TAG_DATE_TIME_DIGITIZED, + exifFlags | ExifTag.TYPE_ASCII << 16 | 20); + mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME, + exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME_ORIGINAL, + exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_SUB_SEC_TIME_DIGITIZED, + exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_IMAGE_UNIQUE_ID, + exifFlags | ExifTag.TYPE_ASCII << 16 | 33); + mTagInfo.put(ExifInterface.TAG_EXPOSURE_TIME, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_F_NUMBER, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_EXPOSURE_PROGRAM, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_SPECTRAL_SENSITIVITY, + exifFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_ISO_SPEED_RATINGS, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_OECF, + exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_SHUTTER_SPEED_VALUE, + exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_APERTURE_VALUE, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_BRIGHTNESS_VALUE, + exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_EXPOSURE_BIAS_VALUE, + exifFlags | ExifTag.TYPE_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_MAX_APERTURE_VALUE, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_SUBJECT_DISTANCE, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_METERING_MODE, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_LIGHT_SOURCE, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_FLASH, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_FOCAL_LENGTH, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_SUBJECT_AREA, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_FLASH_ENERGY, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE, + exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_SUBJECT_LOCATION, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 2); + mTagInfo.put(ExifInterface.TAG_EXPOSURE_INDEX, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_SENSING_METHOD, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_FILE_SOURCE, + exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 1); + mTagInfo.put(ExifInterface.TAG_SCENE_TYPE, + exifFlags | ExifTag.TYPE_UNDEFINED << 16 | 1); + mTagInfo.put(ExifInterface.TAG_CFA_PATTERN, + exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_CUSTOM_RENDERED, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_EXPOSURE_MODE, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_WHITE_BALANCE, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_DIGITAL_ZOOM_RATIO, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_FOCAL_LENGTH_IN_35_MM_FILE, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_SCENE_CAPTURE_TYPE, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_GAIN_CONTROL, + exifFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_CONTRAST, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_SATURATION, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_SHARPNESS, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION, + exifFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_SUBJECT_DISTANCE_RANGE, + exifFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1); + mTagInfo.put(ExifInterface.TAG_INTEROPERABILITY_IFD, exifFlags + | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1); + // GPS tag + int[] gpsAllowedIfds = { + IfdId.TYPE_IFD_GPS + }; + int gpsFlags = getFlagsFromAllowedIfds(gpsAllowedIfds) << 24; + mTagInfo.put(ExifInterface.TAG_GPS_VERSION_ID, + gpsFlags | ExifTag.TYPE_UNSIGNED_BYTE << 16 | 4); + mTagInfo.put(ExifInterface.TAG_GPS_LATITUDE_REF, + gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); + mTagInfo.put(ExifInterface.TAG_GPS_LONGITUDE_REF, + gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); + mTagInfo.put(ExifInterface.TAG_GPS_LATITUDE, + gpsFlags | ExifTag.TYPE_RATIONAL << 16 | 3); + mTagInfo.put(ExifInterface.TAG_GPS_LONGITUDE, + gpsFlags | ExifTag.TYPE_RATIONAL << 16 | 3); + mTagInfo.put(ExifInterface.TAG_GPS_ALTITUDE_REF, + gpsFlags | ExifTag.TYPE_UNSIGNED_BYTE << 16 | 1); + mTagInfo.put(ExifInterface.TAG_GPS_ALTITUDE, + gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_GPS_TIME_STAMP, + gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 3); + mTagInfo.put(ExifInterface.TAG_GPS_SATTELLITES, + gpsFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_GPS_STATUS, + gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); + mTagInfo.put(ExifInterface.TAG_GPS_MEASURE_MODE, + gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); + mTagInfo.put(ExifInterface.TAG_GPS_DOP, + gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_GPS_SPEED_REF, + gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); + mTagInfo.put(ExifInterface.TAG_GPS_SPEED, + gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_GPS_TRACK_REF, + gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); + mTagInfo.put(ExifInterface.TAG_GPS_TRACK, + gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_GPS_IMG_DIRECTION_REF, + gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); + mTagInfo.put(ExifInterface.TAG_GPS_IMG_DIRECTION, + gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_GPS_MAP_DATUM, + gpsFlags | ExifTag.TYPE_ASCII << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_GPS_DEST_LATITUDE_REF, + gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); + mTagInfo.put(ExifInterface.TAG_GPS_DEST_LATITUDE, + gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_GPS_DEST_BEARING_REF, + gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); + mTagInfo.put(ExifInterface.TAG_GPS_DEST_BEARING, + gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_GPS_DEST_DISTANCE_REF, + gpsFlags | ExifTag.TYPE_ASCII << 16 | 2); + mTagInfo.put(ExifInterface.TAG_GPS_DEST_DISTANCE, + gpsFlags | ExifTag.TYPE_UNSIGNED_RATIONAL << 16 | 1); + mTagInfo.put(ExifInterface.TAG_GPS_PROCESSING_METHOD, + gpsFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_GPS_AREA_INFORMATION, + gpsFlags | ExifTag.TYPE_UNDEFINED << 16 | ExifTag.SIZE_UNDEFINED); + mTagInfo.put(ExifInterface.TAG_GPS_DATE_STAMP, + gpsFlags | ExifTag.TYPE_ASCII << 16 | 11); + mTagInfo.put(ExifInterface.TAG_GPS_DIFFERENTIAL, + gpsFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 11); + // Interoperability tag + int[] interopAllowedIfds = { + IfdId.TYPE_IFD_INTEROPERABILITY + }; + int interopFlags = getFlagsFromAllowedIfds(interopAllowedIfds) << 24; + mTagInfo.put(TAG_INTEROPERABILITY_INDEX, interopFlags | ExifTag.TYPE_ASCII << 16 + | ExifTag.SIZE_UNDEFINED); + } + + protected static int getAllowedIfdFlagsFromInfo(int info) { + return info >>> 24; + } + + protected static int[] getAllowedIfdsFromInfo(int info) { + int ifdFlags = getAllowedIfdFlagsFromInfo(info); + int[] ifds = IfdData.getIfds(); + ArrayList<Integer> l = new ArrayList<Integer>(); + for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) { + int flag = (ifdFlags >> i) & 1; + if (flag == 1) { + l.add(ifds[i]); + } + } + if (l.size() <= 0) { + return null; + } + int[] ret = new int[l.size()]; + int j = 0; + for (int i : l) { + ret[j++] = i; + } + return ret; + } + + protected static boolean isIfdAllowed(int info, int ifd) { + int[] ifds = IfdData.getIfds(); + int ifdFlags = getAllowedIfdFlagsFromInfo(info); + for (int i = 0; i < ifds.length; i++) { + if (ifd == ifds[i] && ((ifdFlags >> i) & 1) == 1) { + return true; + } + } + return false; + } + + protected static int getFlagsFromAllowedIfds(int[] allowedIfds) { + if (allowedIfds == null || allowedIfds.length == 0) { + return 0; + } + int flags = 0; + int[] ifds = IfdData.getIfds(); + for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) { + for (int j : allowedIfds) { + if (ifds[i] == j) { + flags |= 1 << i; + break; + } + } + } + return flags; + } + + protected static short getTypeFromInfo(int info) { + return (short) ((info >> 16) & 0x0ff); + } + + protected static int getComponentCountFromInfo(int info) { + return info & 0x0ffff; + } + +} diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifInvalidFormatException.java b/gallerycommon/src/com/android/gallery3d/exif/ExifInvalidFormatException.java new file mode 100644 index 000000000..bf923ec26 --- /dev/null +++ b/gallerycommon/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/gallerycommon/src/com/android/gallery3d/exif/ExifModifier.java b/gallerycommon/src/com/android/gallery3d/exif/ExifModifier.java new file mode 100644 index 000000000..f00362b6b --- /dev/null +++ b/gallerycommon/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<TagOffset> mTagOffsets = new ArrayList<TagOffset>(); + private final ExifInterface mInterface; + private int mOffsetBase; + + private static class TagOffset { + final int mOffset; + final ExifTag mTag; + + TagOffset(ExifTag tag, int offset) { + mTag = tag; + mOffset = offset; + } + } + + protected ExifModifier(ByteBuffer byteBuffer, ExifInterface iRef) throws IOException, + ExifInvalidFormatException { + mByteBuffer = byteBuffer; + mOffsetBase = byteBuffer.position(); + mInterface = iRef; + InputStream is = null; + try { + is = new ByteBufferInputStream(byteBuffer); + // Do not require any IFD; + ExifParser parser = ExifParser.parse(is, mInterface); + mTagToModified = new ExifData(parser.getByteOrder()); + mOffsetBase += parser.getTiffStartPosition(); + mByteBuffer.position(0); + } finally { + ExifInterface.closeSilently(is); + } + } + + protected ByteOrder getByteOrder() { + return mTagToModified.getByteOrder(); + } + + protected boolean commit() throws IOException, ExifInvalidFormatException { + InputStream is = null; + try { + is = new ByteBufferInputStream(mByteBuffer); + int flag = 0; + IfdData[] ifdDatas = new IfdData[] { + mTagToModified.getIfdData(IfdId.TYPE_IFD_0), + mTagToModified.getIfdData(IfdId.TYPE_IFD_1), + mTagToModified.getIfdData(IfdId.TYPE_IFD_EXIF), + mTagToModified.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY), + mTagToModified.getIfdData(IfdId.TYPE_IFD_GPS) + }; + + if (ifdDatas[IfdId.TYPE_IFD_0] != null) { + flag |= ExifParser.OPTION_IFD_0; + } + if (ifdDatas[IfdId.TYPE_IFD_1] != null) { + flag |= ExifParser.OPTION_IFD_1; + } + if (ifdDatas[IfdId.TYPE_IFD_EXIF] != null) { + flag |= ExifParser.OPTION_IFD_EXIF; + } + if (ifdDatas[IfdId.TYPE_IFD_GPS] != null) { + flag |= ExifParser.OPTION_IFD_GPS; + } + if (ifdDatas[IfdId.TYPE_IFD_INTEROPERABILITY] != null) { + flag |= ExifParser.OPTION_IFD_INTEROPERABILITY; + } + + ExifParser parser = ExifParser.parse(is, flag, mInterface); + int event = parser.next(); + IfdData currIfd = null; + while (event != ExifParser.EVENT_END) { + switch (event) { + case ExifParser.EVENT_START_OF_IFD: + currIfd = ifdDatas[parser.getCurrentIfd()]; + if (currIfd == null) { + parser.skipRemainingTagsInCurrentIfd(); + } + break; + case ExifParser.EVENT_NEW_TAG: + ExifTag oldTag = parser.getTag(); + ExifTag newTag = currIfd.getTag(oldTag.getTagId()); + if (newTag != null) { + if (newTag.getComponentCount() != oldTag.getComponentCount() + || newTag.getDataType() != oldTag.getDataType()) { + return false; + } else { + mTagOffsets.add(new TagOffset(newTag, oldTag.getOffset())); + currIfd.removeTag(oldTag.getTagId()); + if (currIfd.getTagCount() == 0) { + parser.skipRemainingTagsInCurrentIfd(); + } + } + } + break; + } + event = parser.next(); + } + for (IfdData ifd : ifdDatas) { + if (ifd != null && ifd.getTagCount() > 0) { + return false; + } + } + modify(); + } finally { + ExifInterface.closeSilently(is); + } + return true; + } + + private void modify() { + mByteBuffer.order(getByteOrder()); + for (TagOffset tagOffset : mTagOffsets) { + writeTagValue(tagOffset.mTag, tagOffset.mOffset); + } + } + + private void writeTagValue(ExifTag tag, int offset) { + if (DEBUG) { + Log.v(TAG, "modifying tag to: \n" + tag.toString()); + Log.v(TAG, "at offset: " + offset); + } + mByteBuffer.position(offset + mOffsetBase); + switch (tag.getDataType()) { + case ExifTag.TYPE_ASCII: + byte buf[] = tag.getStringByte(); + if (buf.length == tag.getComponentCount()) { + buf[buf.length - 1] = 0; + mByteBuffer.put(buf); + } else { + mByteBuffer.put(buf); + mByteBuffer.put((byte) 0); + } + break; + case ExifTag.TYPE_LONG: + case ExifTag.TYPE_UNSIGNED_LONG: + for (int i = 0, n = tag.getComponentCount(); i < n; i++) { + mByteBuffer.putInt((int) tag.getValueAt(i)); + } + break; + case ExifTag.TYPE_RATIONAL: + case ExifTag.TYPE_UNSIGNED_RATIONAL: + for (int i = 0, n = tag.getComponentCount(); i < n; i++) { + Rational v = tag.getRational(i); + mByteBuffer.putInt((int) v.getNumerator()); + mByteBuffer.putInt((int) v.getDenominator()); + } + break; + case ExifTag.TYPE_UNDEFINED: + case ExifTag.TYPE_UNSIGNED_BYTE: + buf = new byte[tag.getComponentCount()]; + tag.getBytes(buf); + mByteBuffer.put(buf); + break; + case ExifTag.TYPE_UNSIGNED_SHORT: + for (int i = 0, n = tag.getComponentCount(); i < n; i++) { + mByteBuffer.putShort((short) tag.getValueAt(i)); + } + break; + } + } + + public void modifyTag(ExifTag tag) { + mTagToModified.addTag(tag); + } +} diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifOutputStream.java b/gallerycommon/src/com/android/gallery3d/exif/ExifOutputStream.java new file mode 100644 index 000000000..7ca05f2e0 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/ExifOutputStream.java @@ -0,0 +1,518 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.exif; + +import android.util.Log; + +import java.io.BufferedOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; + +/** + * This class provides a way to replace the Exif header of a JPEG image. + * <p> + * Below is an example of writing EXIF data into a file + * + * <pre> + * public static void writeExif(byte[] jpeg, ExifData exif, String path) { + * OutputStream os = null; + * try { + * os = new FileOutputStream(path); + * ExifOutputStream eos = new ExifOutputStream(os); + * // Set the exif header + * eos.setExifData(exif); + * // Write the original jpeg out, the header will be add into the file. + * eos.write(jpeg); + * } catch (FileNotFoundException e) { + * e.printStackTrace(); + * } catch (IOException e) { + * e.printStackTrace(); + * } finally { + * if (os != null) { + * try { + * os.close(); + * } catch (IOException e) { + * e.printStackTrace(); + * } + * } + * } + * } + * </pre> + */ +class ExifOutputStream extends FilterOutputStream { + private static final String TAG = "ExifOutputStream"; + private static final boolean DEBUG = false; + private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb + + private static final int STATE_SOI = 0; + private static final int STATE_FRAME_HEADER = 1; + private static final int STATE_JPEG_DATA = 2; + + private static final int EXIF_HEADER = 0x45786966; + private static final short TIFF_HEADER = 0x002A; + private static final short TIFF_BIG_ENDIAN = 0x4d4d; + private static final short TIFF_LITTLE_ENDIAN = 0x4949; + private static final short TAG_SIZE = 12; + private static final short TIFF_HEADER_SIZE = 8; + private static final int MAX_EXIF_SIZE = 65535; + + private ExifData mExifData; + private int mState = STATE_SOI; + private int mByteToSkip; + private int mByteToCopy; + private byte[] mSingleByteArray = new byte[1]; + private ByteBuffer mBuffer = ByteBuffer.allocate(4); + private final ExifInterface mInterface; + + protected ExifOutputStream(OutputStream ou, ExifInterface iRef) { + super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE)); + mInterface = iRef; + } + + /** + * Sets the ExifData to be written into the JPEG file. Should be called + * before writing image data. + */ + protected void setExifData(ExifData exifData) { + mExifData = exifData; + } + + /** + * Gets the Exif header to be written into the JPEF file. + */ + protected ExifData getExifData() { + return mExifData; + } + + private int requestByteToBuffer(int requestByteCount, byte[] buffer + , int offset, int length) { + int byteNeeded = requestByteCount - mBuffer.position(); + int byteToRead = length > byteNeeded ? byteNeeded : length; + mBuffer.put(buffer, offset, byteToRead); + return byteToRead; + } + + /** + * Writes the image out. The input data should be a valid JPEG format. After + * writing, it's Exif header will be replaced by the given header. + */ + @Override + public void write(byte[] buffer, int offset, int length) throws IOException { + while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA) + && length > 0) { + if (mByteToSkip > 0) { + int byteToProcess = length > mByteToSkip ? mByteToSkip : length; + length -= byteToProcess; + mByteToSkip -= byteToProcess; + offset += byteToProcess; + } + if (mByteToCopy > 0) { + int byteToProcess = length > mByteToCopy ? mByteToCopy : length; + out.write(buffer, offset, byteToProcess); + length -= byteToProcess; + mByteToCopy -= byteToProcess; + offset += byteToProcess; + } + if (length == 0) { + return; + } + switch (mState) { + case STATE_SOI: + int byteRead = requestByteToBuffer(2, buffer, offset, length); + offset += byteRead; + length -= byteRead; + if (mBuffer.position() < 2) { + return; + } + mBuffer.rewind(); + if (mBuffer.getShort() != JpegHeader.SOI) { + throw new IOException("Not a valid jpeg image, cannot write exif"); + } + out.write(mBuffer.array(), 0, 2); + mState = STATE_FRAME_HEADER; + mBuffer.rewind(); + writeExifData(); + break; + case STATE_FRAME_HEADER: + // We ignore the APP1 segment and copy all other segments + // until SOF tag. + byteRead = requestByteToBuffer(4, buffer, offset, length); + offset += byteRead; + length -= byteRead; + // Check if this image data doesn't contain SOF. + if (mBuffer.position() == 2) { + short tag = mBuffer.getShort(); + if (tag == JpegHeader.EOI) { + out.write(mBuffer.array(), 0, 2); + mBuffer.rewind(); + } + } + if (mBuffer.position() < 4) { + return; + } + mBuffer.rewind(); + short marker = mBuffer.getShort(); + if (marker == JpegHeader.APP1) { + mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2; + mState = STATE_JPEG_DATA; + } else if (!JpegHeader.isSofMarker(marker)) { + out.write(mBuffer.array(), 0, 4); + mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2; + } else { + out.write(mBuffer.array(), 0, 4); + mState = STATE_JPEG_DATA; + } + mBuffer.rewind(); + } + } + if (length > 0) { + out.write(buffer, offset, length); + } + } + + /** + * Writes the one bytes out. The input data should be a valid JPEG format. + * After writing, it's Exif header will be replaced by the given header. + */ + @Override + public void write(int oneByte) throws IOException { + mSingleByteArray[0] = (byte) (0xff & oneByte); + write(mSingleByteArray); + } + + /** + * Equivalent to calling write(buffer, 0, buffer.length). + */ + @Override + public void write(byte[] buffer) throws IOException { + write(buffer, 0, buffer.length); + } + + private void writeExifData() throws IOException { + if (mExifData == null) { + return; + } + if (DEBUG) { + Log.v(TAG, "Writing exif data..."); + } + ArrayList<ExifTag> nullTags = stripNullValueTags(mExifData); + createRequiredIfdAndTag(); + int exifSize = calculateAllOffset(); + if (exifSize + 8 > MAX_EXIF_SIZE) { + throw new IOException("Exif header is too large (>64Kb)"); + } + OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out); + dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN); + dataOutputStream.writeShort(JpegHeader.APP1); + dataOutputStream.writeShort((short) (exifSize + 8)); + dataOutputStream.writeInt(EXIF_HEADER); + dataOutputStream.writeShort((short) 0x0000); + if (mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN) { + dataOutputStream.writeShort(TIFF_BIG_ENDIAN); + } else { + dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN); + } + dataOutputStream.setByteOrder(mExifData.getByteOrder()); + dataOutputStream.writeShort(TIFF_HEADER); + dataOutputStream.writeInt(8); + writeAllTags(dataOutputStream); + writeThumbnail(dataOutputStream); + for (ExifTag t : nullTags) { + mExifData.addTag(t); + } + } + + private ArrayList<ExifTag> stripNullValueTags(ExifData data) { + ArrayList<ExifTag> nullTags = new ArrayList<ExifTag>(); + for(ExifTag t : data.getAllTags()) { + if (t.getValue() == null && !ExifInterface.isOffsetTag(t.getTagId())) { + data.removeTag(t.getTagId(), t.getIfd()); + nullTags.add(t); + } + } + return nullTags; + } + + private void writeThumbnail(OrderedDataOutputStream dataOutputStream) throws IOException { + if (mExifData.hasCompressedThumbnail()) { + dataOutputStream.write(mExifData.getCompressedThumbnail()); + } else if (mExifData.hasUncompressedStrip()) { + for (int i = 0; i < mExifData.getStripCount(); i++) { + dataOutputStream.write(mExifData.getStrip(i)); + } + } + } + + private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException { + writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_0), dataOutputStream); + writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_EXIF), dataOutputStream); + IfdData interoperabilityIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY); + if (interoperabilityIfd != null) { + writeIfd(interoperabilityIfd, dataOutputStream); + } + IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS); + if (gpsIfd != null) { + writeIfd(gpsIfd, dataOutputStream); + } + IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1); + if (ifd1 != null) { + writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream); + } + } + + private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream) + throws IOException { + ExifTag[] tags = ifd.getAllTags(); + dataOutputStream.writeShort((short) tags.length); + for (ExifTag tag : tags) { + dataOutputStream.writeShort(tag.getTagId()); + dataOutputStream.writeShort(tag.getDataType()); + dataOutputStream.writeInt(tag.getComponentCount()); + if (DEBUG) { + Log.v(TAG, "\n" + tag.toString()); + } + if (tag.getDataSize() > 4) { + dataOutputStream.writeInt(tag.getOffset()); + } else { + ExifOutputStream.writeTagValue(tag, dataOutputStream); + for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) { + dataOutputStream.write(0); + } + } + } + dataOutputStream.writeInt(ifd.getOffsetToNextIfd()); + for (ExifTag tag : tags) { + if (tag.getDataSize() > 4) { + ExifOutputStream.writeTagValue(tag, dataOutputStream); + } + } + } + + private int calculateOffsetOfIfd(IfdData ifd, int offset) { + offset += 2 + ifd.getTagCount() * TAG_SIZE + 4; + ExifTag[] tags = ifd.getAllTags(); + for (ExifTag tag : tags) { + if (tag.getDataSize() > 4) { + tag.setOffset(offset); + offset += tag.getDataSize(); + } + } + return offset; + } + + private void createRequiredIfdAndTag() throws IOException { + // IFD0 is required for all file + IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0); + if (ifd0 == null) { + ifd0 = new IfdData(IfdId.TYPE_IFD_0); + mExifData.addIfdData(ifd0); + } + ExifTag exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD); + if (exifOffsetTag == null) { + throw new IOException("No definition for crucial exif tag: " + + ExifInterface.TAG_EXIF_IFD); + } + ifd0.setTag(exifOffsetTag); + + // Exif IFD is required for all files. + IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF); + if (exifIfd == null) { + exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF); + mExifData.addIfdData(exifIfd); + } + + // GPS IFD + IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS); + if (gpsIfd != null) { + ExifTag gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD); + if (gpsOffsetTag == null) { + throw new IOException("No definition for crucial exif tag: " + + ExifInterface.TAG_GPS_IFD); + } + ifd0.setTag(gpsOffsetTag); + } + + // Interoperability IFD + IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY); + if (interIfd != null) { + ExifTag interOffsetTag = mInterface + .buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD); + if (interOffsetTag == null) { + throw new IOException("No definition for crucial exif tag: " + + ExifInterface.TAG_INTEROPERABILITY_IFD); + } + exifIfd.setTag(interOffsetTag); + } + + IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1); + + // thumbnail + if (mExifData.hasCompressedThumbnail()) { + + if (ifd1 == null) { + ifd1 = new IfdData(IfdId.TYPE_IFD_1); + mExifData.addIfdData(ifd1); + } + + ExifTag offsetTag = mInterface + .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT); + if (offsetTag == null) { + throw new IOException("No definition for crucial exif tag: " + + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT); + } + + ifd1.setTag(offsetTag); + ExifTag lengthTag = mInterface + .buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + if (lengthTag == null) { + throw new IOException("No definition for crucial exif tag: " + + ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + } + + lengthTag.setValue(mExifData.getCompressedThumbnail().length); + ifd1.setTag(lengthTag); + + // Get rid of tags for uncompressed if they exist. + ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)); + ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS)); + } else if (mExifData.hasUncompressedStrip()) { + if (ifd1 == null) { + ifd1 = new IfdData(IfdId.TYPE_IFD_1); + mExifData.addIfdData(ifd1); + } + int stripCount = mExifData.getStripCount(); + ExifTag offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS); + if (offsetTag == null) { + throw new IOException("No definition for crucial exif tag: " + + ExifInterface.TAG_STRIP_OFFSETS); + } + ExifTag lengthTag = mInterface + .buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS); + if (lengthTag == null) { + throw new IOException("No definition for crucial exif tag: " + + ExifInterface.TAG_STRIP_BYTE_COUNTS); + } + long[] lengths = new long[stripCount]; + for (int i = 0; i < mExifData.getStripCount(); i++) { + lengths[i] = mExifData.getStrip(i).length; + } + lengthTag.setValue(lengths); + ifd1.setTag(offsetTag); + ifd1.setTag(lengthTag); + // Get rid of tags for compressed if they exist. + ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)); + ifd1.removeTag(ExifInterface + .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)); + } else if (ifd1 != null) { + // Get rid of offset and length tags if there is no thumbnail. + ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)); + ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS)); + ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)); + ifd1.removeTag(ExifInterface + .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)); + } + } + + private int calculateAllOffset() { + int offset = TIFF_HEADER_SIZE; + IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0); + offset = calculateOffsetOfIfd(ifd0, offset); + ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD)).setValue(offset); + + IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF); + offset = calculateOffsetOfIfd(exifIfd, offset); + + IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY); + if (interIfd != null) { + exifIfd.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD)) + .setValue(offset); + offset = calculateOffsetOfIfd(interIfd, offset); + } + + IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS); + if (gpsIfd != null) { + ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)).setValue(offset); + offset = calculateOffsetOfIfd(gpsIfd, offset); + } + + IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1); + if (ifd1 != null) { + ifd0.setOffsetToNextIfd(offset); + offset = calculateOffsetOfIfd(ifd1, offset); + } + + // thumbnail + if (mExifData.hasCompressedThumbnail()) { + ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) + .setValue(offset); + offset += mExifData.getCompressedThumbnail().length; + } else if (mExifData.hasUncompressedStrip()) { + int stripCount = mExifData.getStripCount(); + long[] offsets = new long[stripCount]; + for (int i = 0; i < mExifData.getStripCount(); i++) { + offsets[i] = offset; + offset += mExifData.getStrip(i).length; + } + ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)).setValue( + offsets); + } + return offset; + } + + static void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream) + throws IOException { + switch (tag.getDataType()) { + case ExifTag.TYPE_ASCII: + byte buf[] = tag.getStringByte(); + if (buf.length == tag.getComponentCount()) { + buf[buf.length - 1] = 0; + dataOutputStream.write(buf); + } else { + dataOutputStream.write(buf); + dataOutputStream.write(0); + } + break; + case ExifTag.TYPE_LONG: + case ExifTag.TYPE_UNSIGNED_LONG: + for (int i = 0, n = tag.getComponentCount(); i < n; i++) { + dataOutputStream.writeInt((int) tag.getValueAt(i)); + } + break; + case ExifTag.TYPE_RATIONAL: + case ExifTag.TYPE_UNSIGNED_RATIONAL: + for (int i = 0, n = tag.getComponentCount(); i < n; i++) { + dataOutputStream.writeRational(tag.getRational(i)); + } + break; + case ExifTag.TYPE_UNDEFINED: + case ExifTag.TYPE_UNSIGNED_BYTE: + buf = new byte[tag.getComponentCount()]; + tag.getBytes(buf); + dataOutputStream.write(buf); + break; + case ExifTag.TYPE_UNSIGNED_SHORT: + for (int i = 0, n = tag.getComponentCount(); i < n; i++) { + dataOutputStream.writeShort((short) tag.getValueAt(i)); + } + break; + } + } +} diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifParser.java b/gallerycommon/src/com/android/gallery3d/exif/ExifParser.java new file mode 100644 index 000000000..5467d423d --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/ExifParser.java @@ -0,0 +1,916 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.exif; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.util.Map.Entry; +import java.util.TreeMap; + +/** + * This class provides a low-level EXIF parsing API. Given a JPEG format + * InputStream, the caller can request which IFD's to read via + * {@link #parse(InputStream, int)} with given options. + * <p> + * Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the + * parser. + * + * <pre> + * void parse() { + * ExifParser parser = ExifParser.parse(mImageInputStream, + * ExifParser.OPTION_IFD_0 | ExifParser.OPTIONS_IFD_EXIF); + * int event = parser.next(); + * while (event != ExifParser.EVENT_END) { + * switch (event) { + * case ExifParser.EVENT_START_OF_IFD: + * break; + * case ExifParser.EVENT_NEW_TAG: + * ExifTag tag = parser.getTag(); + * if (!tag.hasValue()) { + * parser.registerForTagValue(tag); + * } else { + * processTag(tag); + * } + * break; + * case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG: + * tag = parser.getTag(); + * if (tag.getDataType() != ExifTag.TYPE_UNDEFINED) { + * processTag(tag); + * } + * break; + * } + * event = parser.next(); + * } + * } + * + * void processTag(ExifTag tag) { + * // process the tag as you like. + * } + * </pre> + */ +class ExifParser { + private static final boolean LOGV = false; + private static final String TAG = "ExifParser"; + /** + * When the parser reaches a new IFD area. Call {@link #getCurrentIfd()} to + * know which IFD we are in. + */ + public static final int EVENT_START_OF_IFD = 0; + /** + * When the parser reaches a new tag. Call {@link #getTag()}to get the + * corresponding tag. + */ + public static final int EVENT_NEW_TAG = 1; + /** + * When the parser reaches the value area of tag that is registered by + * {@link #registerForTagValue(ExifTag)} previously. Call {@link #getTag()} + * to get the corresponding tag. + */ + public static final int EVENT_VALUE_OF_REGISTERED_TAG = 2; + + /** + * When the parser reaches the compressed image area. + */ + public static final int EVENT_COMPRESSED_IMAGE = 3; + /** + * When the parser reaches the uncompressed image strip. Call + * {@link #getStripIndex()} to get the index of the strip. + * + * @see #getStripIndex() + * @see #getStripCount() + */ + public static final int EVENT_UNCOMPRESSED_STRIP = 4; + /** + * When there is nothing more to parse. + */ + public static final int EVENT_END = 5; + + /** + * Option bit to request to parse IFD0. + */ + public static final int OPTION_IFD_0 = 1 << 0; + /** + * Option bit to request to parse IFD1. + */ + public static final int OPTION_IFD_1 = 1 << 1; + /** + * Option bit to request to parse Exif-IFD. + */ + public static final int OPTION_IFD_EXIF = 1 << 2; + /** + * Option bit to request to parse GPS-IFD. + */ + public static final int OPTION_IFD_GPS = 1 << 3; + /** + * Option bit to request to parse Interoperability-IFD. + */ + public static final int OPTION_IFD_INTEROPERABILITY = 1 << 4; + /** + * Option bit to request to parse thumbnail. + */ + public static final int OPTION_THUMBNAIL = 1 << 5; + + protected static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif" + protected static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in APP1 + + // TIFF header + protected static final short LITTLE_ENDIAN_TAG = (short) 0x4949; // "II" + protected static final short BIG_ENDIAN_TAG = (short) 0x4d4d; // "MM" + protected static final short TIFF_HEADER_TAIL = 0x002A; + + protected static final int TAG_SIZE = 12; + protected static final int OFFSET_SIZE = 2; + + private static final Charset US_ASCII = Charset.forName("US-ASCII"); + + protected static final int DEFAULT_IFD0_OFFSET = 8; + + private final CountedDataInputStream mTiffStream; + private final int mOptions; + private int mIfdStartOffset = 0; + private int mNumOfTagInIfd = 0; + private int mIfdType; + private ExifTag mTag; + private ImageEvent mImageEvent; + private int mStripCount; + private ExifTag mStripSizeTag; + private ExifTag mJpegSizeTag; + private boolean mNeedToParseOffsetsInCurrentIfd; + private boolean mContainExifData = false; + private int mApp1End; + private int mOffsetToApp1EndFromSOF = 0; + private byte[] mDataAboveIfd0; + private int mIfd0Position; + private int mTiffStartPosition; + private final ExifInterface mInterface; + + private static final short TAG_EXIF_IFD = ExifInterface + .getTrueTagKey(ExifInterface.TAG_EXIF_IFD); + private static final short TAG_GPS_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD); + private static final short TAG_INTEROPERABILITY_IFD = ExifInterface + .getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD); + private static final short TAG_JPEG_INTERCHANGE_FORMAT = ExifInterface + .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT); + private static final short TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = ExifInterface + .getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); + private static final short TAG_STRIP_OFFSETS = ExifInterface + .getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS); + private static final short TAG_STRIP_BYTE_COUNTS = ExifInterface + .getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS); + + private final TreeMap<Integer, Object> mCorrespondingEvent = new TreeMap<Integer, Object>(); + + private boolean isIfdRequested(int ifdType) { + switch (ifdType) { + case IfdId.TYPE_IFD_0: + return (mOptions & OPTION_IFD_0) != 0; + case IfdId.TYPE_IFD_1: + return (mOptions & OPTION_IFD_1) != 0; + case IfdId.TYPE_IFD_EXIF: + return (mOptions & OPTION_IFD_EXIF) != 0; + case IfdId.TYPE_IFD_GPS: + return (mOptions & OPTION_IFD_GPS) != 0; + case IfdId.TYPE_IFD_INTEROPERABILITY: + return (mOptions & OPTION_IFD_INTEROPERABILITY) != 0; + } + return false; + } + + private boolean isThumbnailRequested() { + return (mOptions & OPTION_THUMBNAIL) != 0; + } + + private ExifParser(InputStream inputStream, int options, ExifInterface iRef) + throws IOException, ExifInvalidFormatException { + if (inputStream == null) { + throw new IOException("Null argument inputStream to ExifParser"); + } + if (LOGV) { + Log.v(TAG, "Reading exif..."); + } + mInterface = iRef; + mContainExifData = seekTiffData(inputStream); + mTiffStream = new CountedDataInputStream(inputStream); + mOptions = options; + if (!mContainExifData) { + return; + } + + parseTiffHeader(); + long offset = mTiffStream.readUnsignedInt(); + if (offset > Integer.MAX_VALUE) { + throw new ExifInvalidFormatException("Invalid offset " + offset); + } + mIfd0Position = (int) offset; + mIfdType = IfdId.TYPE_IFD_0; + if (isIfdRequested(IfdId.TYPE_IFD_0) || needToParseOffsetsInCurrentIfd()) { + registerIfd(IfdId.TYPE_IFD_0, offset); + if (offset != DEFAULT_IFD0_OFFSET) { + mDataAboveIfd0 = new byte[(int) offset - DEFAULT_IFD0_OFFSET]; + read(mDataAboveIfd0); + } + } + } + + /** + * Parses the the given InputStream with the given options + * + * @exception IOException + * @exception ExifInvalidFormatException + */ + protected static ExifParser parse(InputStream inputStream, int options, ExifInterface iRef) + throws IOException, ExifInvalidFormatException { + return new ExifParser(inputStream, options, iRef); + } + + /** + * Parses the the given InputStream with default options; that is, every IFD + * and thumbnaill will be parsed. + * + * @exception IOException + * @exception ExifInvalidFormatException + * @see #parse(InputStream, int) + */ + protected static ExifParser parse(InputStream inputStream, ExifInterface iRef) + throws IOException, ExifInvalidFormatException { + return new ExifParser(inputStream, OPTION_IFD_0 | OPTION_IFD_1 + | OPTION_IFD_EXIF | OPTION_IFD_GPS | OPTION_IFD_INTEROPERABILITY + | OPTION_THUMBNAIL, iRef); + } + + /** + * Moves the parser forward and returns the next parsing event + * + * @exception IOException + * @exception ExifInvalidFormatException + * @see #EVENT_START_OF_IFD + * @see #EVENT_NEW_TAG + * @see #EVENT_VALUE_OF_REGISTERED_TAG + * @see #EVENT_COMPRESSED_IMAGE + * @see #EVENT_UNCOMPRESSED_STRIP + * @see #EVENT_END + */ + protected int next() throws IOException, ExifInvalidFormatException { + if (!mContainExifData) { + return EVENT_END; + } + int offset = mTiffStream.getReadByteCount(); + int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd; + if (offset < endOfTags) { + mTag = readTag(); + if (mTag == null) { + return next(); + } + if (mNeedToParseOffsetsInCurrentIfd) { + checkOffsetOrImageTag(mTag); + } + return EVENT_NEW_TAG; + } else if (offset == endOfTags) { + // There is a link to ifd1 at the end of ifd0 + if (mIfdType == IfdId.TYPE_IFD_0) { + long ifdOffset = readUnsignedLong(); + if (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested()) { + if (ifdOffset != 0) { + registerIfd(IfdId.TYPE_IFD_1, ifdOffset); + } + } + } else { + int offsetSize = 4; + // Some camera models use invalid length of the offset + if (mCorrespondingEvent.size() > 0) { + offsetSize = mCorrespondingEvent.firstEntry().getKey() - + mTiffStream.getReadByteCount(); + } + if (offsetSize < 4) { + Log.w(TAG, "Invalid size of link to next IFD: " + offsetSize); + } else { + long ifdOffset = readUnsignedLong(); + if (ifdOffset != 0) { + Log.w(TAG, "Invalid link to next IFD: " + ifdOffset); + } + } + } + } + while (mCorrespondingEvent.size() != 0) { + Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry(); + Object event = entry.getValue(); + try { + skipTo(entry.getKey()); + } catch (IOException e) { + Log.w(TAG, "Failed to skip to data at: " + entry.getKey() + + " for " + event.getClass().getName() + ", the file may be broken."); + continue; + } + if (event instanceof IfdEvent) { + mIfdType = ((IfdEvent) event).ifd; + mNumOfTagInIfd = mTiffStream.readUnsignedShort(); + mIfdStartOffset = entry.getKey(); + + if (mNumOfTagInIfd * TAG_SIZE + mIfdStartOffset + OFFSET_SIZE > mApp1End) { + Log.w(TAG, "Invalid size of IFD " + mIfdType); + return EVENT_END; + } + + mNeedToParseOffsetsInCurrentIfd = needToParseOffsetsInCurrentIfd(); + if (((IfdEvent) event).isRequested) { + return EVENT_START_OF_IFD; + } else { + skipRemainingTagsInCurrentIfd(); + } + } else if (event instanceof ImageEvent) { + mImageEvent = (ImageEvent) event; + return mImageEvent.type; + } else { + ExifTagEvent tagEvent = (ExifTagEvent) event; + mTag = tagEvent.tag; + if (mTag.getDataType() != ExifTag.TYPE_UNDEFINED) { + readFullTagValue(mTag); + checkOffsetOrImageTag(mTag); + } + if (tagEvent.isRequested) { + return EVENT_VALUE_OF_REGISTERED_TAG; + } + } + } + return EVENT_END; + } + + /** + * Skips the tags area of current IFD, if the parser is not in the tag area, + * nothing will happen. + * + * @throws IOException + * @throws ExifInvalidFormatException + */ + protected void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException { + int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd; + int offset = mTiffStream.getReadByteCount(); + if (offset > endOfTags) { + return; + } + if (mNeedToParseOffsetsInCurrentIfd) { + while (offset < endOfTags) { + mTag = readTag(); + offset += TAG_SIZE; + if (mTag == null) { + continue; + } + checkOffsetOrImageTag(mTag); + } + } else { + skipTo(endOfTags); + } + long ifdOffset = readUnsignedLong(); + // For ifd0, there is a link to ifd1 in the end of all tags + if (mIfdType == IfdId.TYPE_IFD_0 + && (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested())) { + if (ifdOffset > 0) { + registerIfd(IfdId.TYPE_IFD_1, ifdOffset); + } + } + } + + private boolean needToParseOffsetsInCurrentIfd() { + switch (mIfdType) { + case IfdId.TYPE_IFD_0: + return isIfdRequested(IfdId.TYPE_IFD_EXIF) || isIfdRequested(IfdId.TYPE_IFD_GPS) + || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY) + || isIfdRequested(IfdId.TYPE_IFD_1); + case IfdId.TYPE_IFD_1: + return isThumbnailRequested(); + case IfdId.TYPE_IFD_EXIF: + // The offset to interoperability IFD is located in Exif IFD + return isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY); + default: + return false; + } + } + + /** + * If {@link #next()} return {@link #EVENT_NEW_TAG} or + * {@link #EVENT_VALUE_OF_REGISTERED_TAG}, call this function to get the + * corresponding tag. + * <p> + * For {@link #EVENT_NEW_TAG}, the tag may not contain the value if the size + * of the value is greater than 4 bytes. One should call + * {@link ExifTag#hasValue()} to check if the tag contains value. If there + * is no value,call {@link #registerForTagValue(ExifTag)} to have the parser + * emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area + * pointed by the offset. + * <p> + * When {@link #EVENT_VALUE_OF_REGISTERED_TAG} is emitted, the value of the + * tag will have already been read except for tags of undefined type. For + * tags of undefined type, call one of the read methods to get the value. + * + * @see #registerForTagValue(ExifTag) + * @see #read(byte[]) + * @see #read(byte[], int, int) + * @see #readLong() + * @see #readRational() + * @see #readString(int) + * @see #readString(int, Charset) + */ + protected ExifTag getTag() { + return mTag; + } + + /** + * Gets number of tags in the current IFD area. + */ + protected int getTagCountInCurrentIfd() { + return mNumOfTagInIfd; + } + + /** + * Gets the ID of current IFD. + * + * @see IfdId#TYPE_IFD_0 + * @see IfdId#TYPE_IFD_1 + * @see IfdId#TYPE_IFD_GPS + * @see IfdId#TYPE_IFD_INTEROPERABILITY + * @see IfdId#TYPE_IFD_EXIF + */ + protected int getCurrentIfd() { + return mIfdType; + } + + /** + * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to + * get the index of this strip. + * + * @see #getStripCount() + */ + protected int getStripIndex() { + return mImageEvent.stripIndex; + } + + /** + * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to + * get the number of strip data. + * + * @see #getStripIndex() + */ + protected int getStripCount() { + return mStripCount; + } + + /** + * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to + * get the strip size. + */ + protected int getStripSize() { + if (mStripSizeTag == null) + return 0; + return (int) mStripSizeTag.getValueAt(0); + } + + /** + * When receiving {@link #EVENT_COMPRESSED_IMAGE}, call this function to get + * the image data size. + */ + protected int getCompressedImageSize() { + if (mJpegSizeTag == null) { + return 0; + } + return (int) mJpegSizeTag.getValueAt(0); + } + + private void skipTo(int offset) throws IOException { + mTiffStream.skipTo(offset); + while (!mCorrespondingEvent.isEmpty() && mCorrespondingEvent.firstKey() < offset) { + mCorrespondingEvent.pollFirstEntry(); + } + } + + /** + * When getting {@link #EVENT_NEW_TAG} in the tag area of IFD, the tag may + * not contain the value if the size of the value is greater than 4 bytes. + * When the value is not available here, call this method so that the parser + * will emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area + * where the value is located. + * + * @see #EVENT_VALUE_OF_REGISTERED_TAG + */ + protected void registerForTagValue(ExifTag tag) { + if (tag.getOffset() >= mTiffStream.getReadByteCount()) { + mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, true)); + } + } + + private void registerIfd(int ifdType, long offset) { + // Cast unsigned int to int since the offset is always smaller + // than the size of APP1 (65536) + mCorrespondingEvent.put((int) offset, new IfdEvent(ifdType, isIfdRequested(ifdType))); + } + + private void registerCompressedImage(long offset) { + mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_COMPRESSED_IMAGE)); + } + + private void registerUncompressedStrip(int stripIndex, long offset) { + mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_UNCOMPRESSED_STRIP + , stripIndex)); + } + + private ExifTag readTag() throws IOException, ExifInvalidFormatException { + short tagId = mTiffStream.readShort(); + short dataFormat = mTiffStream.readShort(); + long numOfComp = mTiffStream.readUnsignedInt(); + if (numOfComp > Integer.MAX_VALUE) { + throw new ExifInvalidFormatException( + "Number of component is larger then Integer.MAX_VALUE"); + } + // Some invalid image file contains invalid data type. Ignore those tags + if (!ExifTag.isValidType(dataFormat)) { + Log.w(TAG, String.format("Tag %04x: Invalid data type %d", tagId, dataFormat)); + mTiffStream.skip(4); + return null; + } + // TODO: handle numOfComp overflow + ExifTag tag = new ExifTag(tagId, dataFormat, (int) numOfComp, mIfdType, + ((int) numOfComp) != ExifTag.SIZE_UNDEFINED); + int dataSize = tag.getDataSize(); + if (dataSize > 4) { + long offset = mTiffStream.readUnsignedInt(); + if (offset > Integer.MAX_VALUE) { + throw new ExifInvalidFormatException( + "offset is larger then Integer.MAX_VALUE"); + } + // Some invalid images put some undefined data before IFD0. + // Read the data here. + if ((offset < mIfd0Position) && (dataFormat == ExifTag.TYPE_UNDEFINED)) { + byte[] buf = new byte[(int) numOfComp]; + System.arraycopy(mDataAboveIfd0, (int) offset - DEFAULT_IFD0_OFFSET, + buf, 0, (int) numOfComp); + tag.setValue(buf); + } else { + tag.setOffset((int) offset); + } + } else { + boolean defCount = tag.hasDefinedCount(); + // Set defined count to 0 so we can add \0 to non-terminated strings + tag.setHasDefinedCount(false); + // Read value + readFullTagValue(tag); + tag.setHasDefinedCount(defCount); + mTiffStream.skip(4 - dataSize); + // Set the offset to the position of value. + tag.setOffset(mTiffStream.getReadByteCount() - 4); + } + return tag; + } + + /** + * Check the tag, if the tag is one of the offset tag that points to the IFD + * or image the caller is interested in, register the IFD or image. + */ + private void checkOffsetOrImageTag(ExifTag tag) { + // Some invalid formattd image contains tag with 0 size. + if (tag.getComponentCount() == 0) { + return; + } + short tid = tag.getTagId(); + int ifd = tag.getIfd(); + if (tid == TAG_EXIF_IFD && checkAllowed(ifd, ExifInterface.TAG_EXIF_IFD)) { + if (isIfdRequested(IfdId.TYPE_IFD_EXIF) + || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) { + registerIfd(IfdId.TYPE_IFD_EXIF, tag.getValueAt(0)); + } + } else if (tid == TAG_GPS_IFD && checkAllowed(ifd, ExifInterface.TAG_GPS_IFD)) { + if (isIfdRequested(IfdId.TYPE_IFD_GPS)) { + registerIfd(IfdId.TYPE_IFD_GPS, tag.getValueAt(0)); + } + } else if (tid == TAG_INTEROPERABILITY_IFD + && checkAllowed(ifd, ExifInterface.TAG_INTEROPERABILITY_IFD)) { + if (isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) { + registerIfd(IfdId.TYPE_IFD_INTEROPERABILITY, tag.getValueAt(0)); + } + } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT + && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) { + if (isThumbnailRequested()) { + registerCompressedImage(tag.getValueAt(0)); + } + } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT_LENGTH + && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)) { + if (isThumbnailRequested()) { + mJpegSizeTag = tag; + } + } else if (tid == TAG_STRIP_OFFSETS && checkAllowed(ifd, ExifInterface.TAG_STRIP_OFFSETS)) { + if (isThumbnailRequested()) { + if (tag.hasValue()) { + for (int i = 0; i < tag.getComponentCount(); i++) { + if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) { + registerUncompressedStrip(i, tag.getValueAt(i)); + } else { + registerUncompressedStrip(i, tag.getValueAt(i)); + } + } + } else { + mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, false)); + } + } + } else if (tid == TAG_STRIP_BYTE_COUNTS + && checkAllowed(ifd, ExifInterface.TAG_STRIP_BYTE_COUNTS) + &&isThumbnailRequested() && tag.hasValue()) { + mStripSizeTag = tag; + } + } + + private boolean checkAllowed(int ifd, int tagId) { + int info = mInterface.getTagInfo().get(tagId); + if (info == ExifInterface.DEFINITION_NULL) { + return false; + } + return ExifInterface.isIfdAllowed(info, ifd); + } + + protected void readFullTagValue(ExifTag tag) throws IOException { + // Some invalid images contains tags with wrong size, check it here + short type = tag.getDataType(); + if (type == ExifTag.TYPE_ASCII || type == ExifTag.TYPE_UNDEFINED || + type == ExifTag.TYPE_UNSIGNED_BYTE) { + int size = tag.getComponentCount(); + if (mCorrespondingEvent.size() > 0) { + if (mCorrespondingEvent.firstEntry().getKey() < mTiffStream.getReadByteCount() + + size) { + Object event = mCorrespondingEvent.firstEntry().getValue(); + if (event instanceof ImageEvent) { + // Tag value overlaps thumbnail, ignore thumbnail. + Log.w(TAG, "Thumbnail overlaps value for tag: \n" + tag.toString()); + Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry(); + Log.w(TAG, "Invalid thumbnail offset: " + entry.getKey()); + } else { + // Tag value overlaps another tag, shorten count + if (event instanceof IfdEvent) { + Log.w(TAG, "Ifd " + ((IfdEvent) event).ifd + + " overlaps value for tag: \n" + tag.toString()); + } else if (event instanceof ExifTagEvent) { + Log.w(TAG, "Tag value for tag: \n" + + ((ExifTagEvent) event).tag.toString() + + " overlaps value for tag: \n" + tag.toString()); + } + size = mCorrespondingEvent.firstEntry().getKey() + - mTiffStream.getReadByteCount(); + Log.w(TAG, "Invalid size of tag: \n" + tag.toString() + + " setting count to: " + size); + tag.forceSetComponentCount(size); + } + } + } + } + switch (tag.getDataType()) { + case ExifTag.TYPE_UNSIGNED_BYTE: + case ExifTag.TYPE_UNDEFINED: { + byte buf[] = new byte[tag.getComponentCount()]; + read(buf); + tag.setValue(buf); + } + break; + case ExifTag.TYPE_ASCII: + tag.setValue(readString(tag.getComponentCount())); + break; + case ExifTag.TYPE_UNSIGNED_LONG: { + long value[] = new long[tag.getComponentCount()]; + for (int i = 0, n = value.length; i < n; i++) { + value[i] = readUnsignedLong(); + } + tag.setValue(value); + } + break; + case ExifTag.TYPE_UNSIGNED_RATIONAL: { + Rational value[] = new Rational[tag.getComponentCount()]; + for (int i = 0, n = value.length; i < n; i++) { + value[i] = readUnsignedRational(); + } + tag.setValue(value); + } + break; + case ExifTag.TYPE_UNSIGNED_SHORT: { + int value[] = new int[tag.getComponentCount()]; + for (int i = 0, n = value.length; i < n; i++) { + value[i] = readUnsignedShort(); + } + tag.setValue(value); + } + break; + case ExifTag.TYPE_LONG: { + int value[] = new int[tag.getComponentCount()]; + for (int i = 0, n = value.length; i < n; i++) { + value[i] = readLong(); + } + tag.setValue(value); + } + break; + case ExifTag.TYPE_RATIONAL: { + Rational value[] = new Rational[tag.getComponentCount()]; + for (int i = 0, n = value.length; i < n; i++) { + value[i] = readRational(); + } + tag.setValue(value); + } + break; + } + if (LOGV) { + Log.v(TAG, "\n" + tag.toString()); + } + } + + private void parseTiffHeader() throws IOException, + ExifInvalidFormatException { + short byteOrder = mTiffStream.readShort(); + if (LITTLE_ENDIAN_TAG == byteOrder) { + mTiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN); + } else if (BIG_ENDIAN_TAG == byteOrder) { + mTiffStream.setByteOrder(ByteOrder.BIG_ENDIAN); + } else { + throw new ExifInvalidFormatException("Invalid TIFF header"); + } + + if (mTiffStream.readShort() != TIFF_HEADER_TAIL) { + throw new ExifInvalidFormatException("Invalid TIFF header"); + } + } + + private boolean seekTiffData(InputStream inputStream) throws IOException, + ExifInvalidFormatException { + CountedDataInputStream dataStream = new CountedDataInputStream(inputStream); + if (dataStream.readShort() != JpegHeader.SOI) { + throw new ExifInvalidFormatException("Invalid JPEG format"); + } + + short marker = dataStream.readShort(); + while (marker != JpegHeader.EOI + && !JpegHeader.isSofMarker(marker)) { + int length = dataStream.readUnsignedShort(); + // Some invalid formatted image contains multiple APP1, + // try to find the one with Exif data. + if (marker == JpegHeader.APP1) { + int header = 0; + short headerTail = 0; + if (length >= 8) { + header = dataStream.readInt(); + headerTail = dataStream.readShort(); + length -= 6; + if (header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL) { + mTiffStartPosition = dataStream.getReadByteCount(); + mApp1End = length; + mOffsetToApp1EndFromSOF = mTiffStartPosition + mApp1End; + return true; + } + } + } + if (length < 2 || (length - 2) != dataStream.skip(length - 2)) { + Log.w(TAG, "Invalid JPEG format."); + return false; + } + marker = dataStream.readShort(); + } + return false; + } + + protected int getOffsetToExifEndFromSOF() { + return mOffsetToApp1EndFromSOF; + } + + protected int getTiffStartPosition() { + return mTiffStartPosition; + } + + /** + * Reads bytes from the InputStream. + */ + protected int read(byte[] buffer, int offset, int length) throws IOException { + return mTiffStream.read(buffer, offset, length); + } + + /** + * Equivalent to read(buffer, 0, buffer.length). + */ + protected int read(byte[] buffer) throws IOException { + return mTiffStream.read(buffer); + } + + /** + * Reads a String from the InputStream with US-ASCII charset. The parser + * will read n bytes and convert it to ascii string. This is used for + * reading values of type {@link ExifTag#TYPE_ASCII}. + */ + protected String readString(int n) throws IOException { + return readString(n, US_ASCII); + } + + /** + * Reads a String from the InputStream with the given charset. The parser + * will read n bytes and convert it to string. This is used for reading + * values of type {@link ExifTag#TYPE_ASCII}. + */ + protected String readString(int n, Charset charset) throws IOException { + if (n > 0) { + return mTiffStream.readString(n, charset); + } else { + return ""; + } + } + + /** + * Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the + * InputStream. + */ + protected int readUnsignedShort() throws IOException { + return mTiffStream.readShort() & 0xffff; + } + + /** + * Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the + * InputStream. + */ + protected long readUnsignedLong() throws IOException { + return readLong() & 0xffffffffL; + } + + /** + * Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the + * InputStream. + */ + protected Rational readUnsignedRational() throws IOException { + long nomi = readUnsignedLong(); + long denomi = readUnsignedLong(); + return new Rational(nomi, denomi); + } + + /** + * Reads value of type {@link ExifTag#TYPE_LONG} from the InputStream. + */ + protected int readLong() throws IOException { + return mTiffStream.readInt(); + } + + /** + * Reads value of type {@link ExifTag#TYPE_RATIONAL} from the InputStream. + */ + protected Rational readRational() throws IOException { + int nomi = readLong(); + int denomi = readLong(); + return new Rational(nomi, denomi); + } + + private static class ImageEvent { + int stripIndex; + int type; + + ImageEvent(int type) { + this.stripIndex = 0; + this.type = type; + } + + ImageEvent(int type, int stripIndex) { + this.type = type; + this.stripIndex = stripIndex; + } + } + + private static class IfdEvent { + int ifd; + boolean isRequested; + + IfdEvent(int ifd, boolean isInterestedIfd) { + this.ifd = ifd; + this.isRequested = isInterestedIfd; + } + } + + private static class ExifTagEvent { + ExifTag tag; + boolean isRequested; + + ExifTagEvent(ExifTag tag, boolean isRequireByUser) { + this.tag = tag; + this.isRequested = isRequireByUser; + } + } + + /** + * Gets the byte order of the current InputStream. + */ + protected ByteOrder getByteOrder() { + return mTiffStream.getByteOrder(); + } +} diff --git a/gallerycommon/src/com/android/gallery3d/exif/ExifReader.java b/gallerycommon/src/com/android/gallery3d/exif/ExifReader.java new file mode 100644 index 000000000..68e972fb7 --- /dev/null +++ b/gallerycommon/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/gallerycommon/src/com/android/gallery3d/exif/ExifTag.java b/gallerycommon/src/com/android/gallery3d/exif/ExifTag.java new file mode 100644 index 000000000..b8b387201 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/ExifTag.java @@ -0,0 +1,1008 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.exif; + +import java.nio.charset.Charset; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; + +/** + * This class stores information of an EXIF tag. For more information about + * defined EXIF tags, please read the Jeita EXIF 2.2 standard. Tags should be + * instantiated using {@link ExifInterface#buildTag}. + * + * @see ExifInterface + */ +public class ExifTag { + /** + * The BYTE type in the EXIF standard. An 8-bit unsigned integer. + */ + public static final short TYPE_UNSIGNED_BYTE = 1; + /** + * The ASCII type in the EXIF standard. An 8-bit byte containing one 7-bit + * ASCII code. The final byte is terminated with NULL. + */ + public static final short TYPE_ASCII = 2; + /** + * The SHORT type in the EXIF standard. A 16-bit (2-byte) unsigned integer + */ + public static final short TYPE_UNSIGNED_SHORT = 3; + /** + * The LONG type in the EXIF standard. A 32-bit (4-byte) unsigned integer + */ + public static final short TYPE_UNSIGNED_LONG = 4; + /** + * The RATIONAL type of EXIF standard. It consists of two LONGs. The first + * one is the numerator and the second one expresses the denominator. + */ + public static final short TYPE_UNSIGNED_RATIONAL = 5; + /** + * The UNDEFINED type in the EXIF standard. An 8-bit byte that can take any + * value depending on the field definition. + */ + public static final short TYPE_UNDEFINED = 7; + /** + * The SLONG type in the EXIF standard. A 32-bit (4-byte) signed integer + * (2's complement notation). + */ + public static final short TYPE_LONG = 9; + /** + * The SRATIONAL type of EXIF standard. It consists of two SLONGs. The first + * one is the numerator and the second one is the denominator. + */ + public static final short TYPE_RATIONAL = 10; + + private static Charset US_ASCII = Charset.forName("US-ASCII"); + private static final int TYPE_TO_SIZE_MAP[] = new int[11]; + private static final int UNSIGNED_SHORT_MAX = 65535; + private static final long UNSIGNED_LONG_MAX = 4294967295L; + private static final long LONG_MAX = Integer.MAX_VALUE; + private static final long LONG_MIN = Integer.MIN_VALUE; + + static { + TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_BYTE] = 1; + TYPE_TO_SIZE_MAP[TYPE_ASCII] = 1; + TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_SHORT] = 2; + TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_LONG] = 4; + TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_RATIONAL] = 8; + TYPE_TO_SIZE_MAP[TYPE_UNDEFINED] = 1; + TYPE_TO_SIZE_MAP[TYPE_LONG] = 4; + TYPE_TO_SIZE_MAP[TYPE_RATIONAL] = 8; + } + + static final int SIZE_UNDEFINED = 0; + + // Exif TagId + private final short mTagId; + // Exif Tag Type + private final short mDataType; + // If tag has defined count + private boolean mHasDefinedDefaultComponentCount; + // Actual data count in tag (should be number of elements in value array) + private int mComponentCountActual; + // The ifd that this tag should be put in + private int mIfd; + // The value (array of elements of type Tag Type) + private Object mValue; + // Value offset in exif header. + private int mOffset; + + private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("yyyy:MM:dd kk:mm:ss"); + + /** + * Returns true if the given IFD is a valid IFD. + */ + public static boolean isValidIfd(int ifdId) { + return ifdId == IfdId.TYPE_IFD_0 || ifdId == IfdId.TYPE_IFD_1 + || ifdId == IfdId.TYPE_IFD_EXIF || ifdId == IfdId.TYPE_IFD_INTEROPERABILITY + || ifdId == IfdId.TYPE_IFD_GPS; + } + + /** + * Returns true if a given type is a valid tag type. + */ + public static boolean isValidType(short type) { + return type == TYPE_UNSIGNED_BYTE || type == TYPE_ASCII || + type == TYPE_UNSIGNED_SHORT || type == TYPE_UNSIGNED_LONG || + type == TYPE_UNSIGNED_RATIONAL || type == TYPE_UNDEFINED || + type == TYPE_LONG || type == TYPE_RATIONAL; + } + + // Use builtTag in ExifInterface instead of constructor. + ExifTag(short tagId, short type, int componentCount, int ifd, + boolean hasDefinedComponentCount) { + mTagId = tagId; + mDataType = type; + mComponentCountActual = componentCount; + mHasDefinedDefaultComponentCount = hasDefinedComponentCount; + mIfd = ifd; + mValue = null; + } + + /** + * Gets the element size of the given data type in bytes. + * + * @see #TYPE_ASCII + * @see #TYPE_LONG + * @see #TYPE_RATIONAL + * @see #TYPE_UNDEFINED + * @see #TYPE_UNSIGNED_BYTE + * @see #TYPE_UNSIGNED_LONG + * @see #TYPE_UNSIGNED_RATIONAL + * @see #TYPE_UNSIGNED_SHORT + */ + public static int getElementSize(short type) { + return TYPE_TO_SIZE_MAP[type]; + } + + /** + * Returns the ID of the IFD this tag belongs to. + * + * @see IfdId#TYPE_IFD_0 + * @see IfdId#TYPE_IFD_1 + * @see IfdId#TYPE_IFD_EXIF + * @see IfdId#TYPE_IFD_GPS + * @see IfdId#TYPE_IFD_INTEROPERABILITY + */ + public int getIfd() { + return mIfd; + } + + protected void setIfd(int ifdId) { + mIfd = ifdId; + } + + /** + * Gets the TID of this tag. + */ + public short getTagId() { + return mTagId; + } + + /** + * Gets the data type of this tag + * + * @see #TYPE_ASCII + * @see #TYPE_LONG + * @see #TYPE_RATIONAL + * @see #TYPE_UNDEFINED + * @see #TYPE_UNSIGNED_BYTE + * @see #TYPE_UNSIGNED_LONG + * @see #TYPE_UNSIGNED_RATIONAL + * @see #TYPE_UNSIGNED_SHORT + */ + public short getDataType() { + return mDataType; + } + + /** + * Gets the total data size in bytes of the value of this tag. + */ + public int getDataSize() { + return getComponentCount() * getElementSize(getDataType()); + } + + /** + * Gets the component count of this tag. + */ + + // TODO: fix integer overflows with this + public int getComponentCount() { + return mComponentCountActual; + } + + /** + * Sets the component count of this tag. Call this function before + * setValue() if the length of value does not match the component count. + */ + protected void forceSetComponentCount(int count) { + mComponentCountActual = count; + } + + /** + * Returns true if this ExifTag contains value; otherwise, this tag will + * contain an offset value that is determined when the tag is written. + */ + public boolean hasValue() { + return mValue != null; + } + + /** + * Sets integer values into this tag. This method should be used for tags of + * type {@link #TYPE_UNSIGNED_SHORT}. This method will fail if: + * <ul> + * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT}, + * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.</li> + * <li>The value overflows.</li> + * <li>The value.length does NOT match the component count in the definition + * for this tag.</li> + * </ul> + */ + public boolean setValue(int[] value) { + if (checkBadComponentCount(value.length)) { + return false; + } + if (mDataType != TYPE_UNSIGNED_SHORT && mDataType != TYPE_LONG && + mDataType != TYPE_UNSIGNED_LONG) { + return false; + } + if (mDataType == TYPE_UNSIGNED_SHORT && checkOverflowForUnsignedShort(value)) { + return false; + } else if (mDataType == TYPE_UNSIGNED_LONG && checkOverflowForUnsignedLong(value)) { + return false; + } + + long[] data = new long[value.length]; + for (int i = 0; i < value.length; i++) { + data[i] = value[i]; + } + mValue = data; + mComponentCountActual = value.length; + return true; + } + + /** + * Sets integer value into this tag. This method should be used for tags of + * type {@link #TYPE_UNSIGNED_SHORT}, or {@link #TYPE_LONG}. This method + * will fail if: + * <ul> + * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT}, + * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.</li> + * <li>The value overflows.</li> + * <li>The component count in the definition of this tag is not 1.</li> + * </ul> + */ + public boolean setValue(int value) { + return setValue(new int[] { + value + }); + } + + /** + * Sets long values into this tag. This method should be used for tags of + * type {@link #TYPE_UNSIGNED_LONG}. This method will fail if: + * <ul> + * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.</li> + * <li>The value overflows.</li> + * <li>The value.length does NOT match the component count in the definition + * for this tag.</li> + * </ul> + */ + public boolean setValue(long[] value) { + if (checkBadComponentCount(value.length) || mDataType != TYPE_UNSIGNED_LONG) { + return false; + } + if (checkOverflowForUnsignedLong(value)) { + return false; + } + mValue = value; + mComponentCountActual = value.length; + return true; + } + + /** + * Sets long values into this tag. This method should be used for tags of + * type {@link #TYPE_UNSIGNED_LONG}. This method will fail if: + * <ul> + * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.</li> + * <li>The value overflows.</li> + * <li>The component count in the definition for this tag is not 1.</li> + * </ul> + */ + public boolean setValue(long value) { + return setValue(new long[] { + value + }); + } + + /** + * Sets a string value into this tag. This method should be used for tags of + * type {@link #TYPE_ASCII}. The string is converted to an ASCII string. + * Characters that cannot be converted are replaced with '?'. The length of + * the string must be equal to either (component count -1) or (component + * count). The final byte will be set to the string null terminator '\0', + * overwriting the last character in the string if the value.length is equal + * to the component count. This method will fail if: + * <ul> + * <li>The data type is not {@link #TYPE_ASCII} or {@link #TYPE_UNDEFINED}.</li> + * <li>The length of the string is not equal to (component count -1) or + * (component count) in the definition for this tag.</li> + * </ul> + */ + public boolean setValue(String value) { + if (mDataType != TYPE_ASCII && mDataType != TYPE_UNDEFINED) { + return false; + } + + byte[] buf = value.getBytes(US_ASCII); + byte[] finalBuf = buf; + if (buf.length > 0) { + finalBuf = (buf[buf.length - 1] == 0 || mDataType == TYPE_UNDEFINED) ? buf : Arrays + .copyOf(buf, buf.length + 1); + } else if (mDataType == TYPE_ASCII && mComponentCountActual == 1) { + finalBuf = new byte[] { 0 }; + } + int count = finalBuf.length; + if (checkBadComponentCount(count)) { + return false; + } + mComponentCountActual = count; + mValue = finalBuf; + return true; + } + + /** + * Sets Rational values into this tag. This method should be used for tags + * of type {@link #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This + * method will fail if: + * <ul> + * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} + * or {@link #TYPE_RATIONAL}.</li> + * <li>The value overflows.</li> + * <li>The value.length does NOT match the component count in the definition + * for this tag.</li> + * </ul> + * + * @see Rational + */ + public boolean setValue(Rational[] value) { + if (checkBadComponentCount(value.length)) { + return false; + } + if (mDataType != TYPE_UNSIGNED_RATIONAL && mDataType != TYPE_RATIONAL) { + return false; + } + if (mDataType == TYPE_UNSIGNED_RATIONAL && checkOverflowForUnsignedRational(value)) { + return false; + } else if (mDataType == TYPE_RATIONAL && checkOverflowForRational(value)) { + return false; + } + + mValue = value; + mComponentCountActual = value.length; + return true; + } + + /** + * Sets a Rational value into this tag. This method should be used for tags + * of type {@link #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This + * method will fail if: + * <ul> + * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} + * or {@link #TYPE_RATIONAL}.</li> + * <li>The value overflows.</li> + * <li>The component count in the definition for this tag is not 1.</li> + * </ul> + * + * @see Rational + */ + public boolean setValue(Rational value) { + return setValue(new Rational[] { + value + }); + } + + /** + * Sets byte values into this tag. This method should be used for tags of + * type {@link #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method + * will fail if: + * <ul> + * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or + * {@link #TYPE_UNDEFINED} .</li> + * <li>The length does NOT match the component count in the definition for + * this tag.</li> + * </ul> + */ + public boolean setValue(byte[] value, int offset, int length) { + if (checkBadComponentCount(length)) { + return false; + } + if (mDataType != TYPE_UNSIGNED_BYTE && mDataType != TYPE_UNDEFINED) { + return false; + } + mValue = new byte[length]; + System.arraycopy(value, offset, mValue, 0, length); + mComponentCountActual = length; + return true; + } + + /** + * Equivalent to setValue(value, 0, value.length). + */ + public boolean setValue(byte[] value) { + return setValue(value, 0, value.length); + } + + /** + * Sets byte value into this tag. This method should be used for tags of + * type {@link #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method + * will fail if: + * <ul> + * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or + * {@link #TYPE_UNDEFINED} .</li> + * <li>The component count in the definition for this tag is not 1.</li> + * </ul> + */ + public boolean setValue(byte value) { + return setValue(new byte[] { + value + }); + } + + /** + * Sets the value for this tag using an appropriate setValue method for the + * given object. This method will fail if: + * <ul> + * <li>The corresponding setValue method for the class of the object passed + * in would fail.</li> + * <li>There is no obvious way to cast the object passed in into an EXIF tag + * type.</li> + * </ul> + */ + public boolean setValue(Object obj) { + if (obj == null) { + return false; + } else if (obj instanceof Short) { + return setValue(((Short) obj).shortValue() & 0x0ffff); + } else if (obj instanceof String) { + return setValue((String) obj); + } else if (obj instanceof int[]) { + return setValue((int[]) obj); + } else if (obj instanceof long[]) { + return setValue((long[]) obj); + } else if (obj instanceof Rational) { + return setValue((Rational) obj); + } else if (obj instanceof Rational[]) { + return setValue((Rational[]) obj); + } else if (obj instanceof byte[]) { + return setValue((byte[]) obj); + } else if (obj instanceof Integer) { + return setValue(((Integer) obj).intValue()); + } else if (obj instanceof Long) { + return setValue(((Long) obj).longValue()); + } else if (obj instanceof Byte) { + return setValue(((Byte) obj).byteValue()); + } else if (obj instanceof Short[]) { + // Nulls in this array are treated as zeroes. + Short[] arr = (Short[]) obj; + int[] fin = new int[arr.length]; + for (int i = 0; i < arr.length; i++) { + fin[i] = (arr[i] == null) ? 0 : arr[i].shortValue() & 0x0ffff; + } + return setValue(fin); + } else if (obj instanceof Integer[]) { + // Nulls in this array are treated as zeroes. + Integer[] arr = (Integer[]) obj; + int[] fin = new int[arr.length]; + for (int i = 0; i < arr.length; i++) { + fin[i] = (arr[i] == null) ? 0 : arr[i].intValue(); + } + return setValue(fin); + } else if (obj instanceof Long[]) { + // Nulls in this array are treated as zeroes. + Long[] arr = (Long[]) obj; + long[] fin = new long[arr.length]; + for (int i = 0; i < arr.length; i++) { + fin[i] = (arr[i] == null) ? 0 : arr[i].longValue(); + } + return setValue(fin); + } else if (obj instanceof Byte[]) { + // Nulls in this array are treated as zeroes. + Byte[] arr = (Byte[]) obj; + byte[] fin = new byte[arr.length]; + for (int i = 0; i < arr.length; i++) { + fin[i] = (arr[i] == null) ? 0 : arr[i].byteValue(); + } + return setValue(fin); + } else { + return false; + } + } + + /** + * Sets a timestamp to this tag. The method converts the timestamp with the + * format of "yyyy:MM:dd kk:mm:ss" and calls {@link #setValue(String)}. This + * method will fail if the data type is not {@link #TYPE_ASCII} or the + * component count of this tag is not 20 or undefined. + * + * @param time the number of milliseconds since Jan. 1, 1970 GMT + * @return true on success + */ + public boolean setTimeValue(long time) { + // synchronized on TIME_FORMAT as SimpleDateFormat is not thread safe + synchronized (TIME_FORMAT) { + return setValue(TIME_FORMAT.format(new Date(time))); + } + } + + /** + * Gets the value as a String. This method should be used for tags of type + * {@link #TYPE_ASCII}. + * + * @return the value as a String, or null if the tag's value does not exist + * or cannot be converted to a String. + */ + public String getValueAsString() { + if (mValue == null) { + return null; + } else if (mValue instanceof String) { + return (String) mValue; + } else if (mValue instanceof byte[]) { + return new String((byte[]) mValue, US_ASCII); + } + return null; + } + + /** + * Gets the value as a String. This method should be used for tags of type + * {@link #TYPE_ASCII}. + * + * @param defaultValue the String to return if the tag's value does not + * exist or cannot be converted to a String. + * @return the tag's value as a String, or the defaultValue. + */ + public String getValueAsString(String defaultValue) { + String s = getValueAsString(); + if (s == null) { + return defaultValue; + } + return s; + } + + /** + * Gets the value as a byte array. This method should be used for tags of + * type {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}. + * + * @return the value as a byte array, or null if the tag's value does not + * exist or cannot be converted to a byte array. + */ + public byte[] getValueAsBytes() { + if (mValue instanceof byte[]) { + return (byte[]) mValue; + } + return null; + } + + /** + * Gets the value as a byte. If there are more than 1 bytes in this value, + * gets the first byte. This method should be used for tags of type + * {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}. + * + * @param defaultValue the byte to return if tag's value does not exist or + * cannot be converted to a byte. + * @return the tag's value as a byte, or the defaultValue. + */ + public byte getValueAsByte(byte defaultValue) { + byte[] b = getValueAsBytes(); + if (b == null || b.length < 1) { + return defaultValue; + } + return b[0]; + } + + /** + * Gets the value as an array of Rationals. This method should be used for + * tags of type {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}. + * + * @return the value as as an array of Rationals, or null if the tag's value + * does not exist or cannot be converted to an array of Rationals. + */ + public Rational[] getValueAsRationals() { + if (mValue instanceof Rational[]) { + return (Rational[]) mValue; + } + return null; + } + + /** + * Gets the value as a Rational. If there are more than 1 Rationals in this + * value, gets the first one. This method should be used for tags of type + * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}. + * + * @param defaultValue the Rational to return if tag's value does not exist + * or cannot be converted to a Rational. + * @return the tag's value as a Rational, or the defaultValue. + */ + public Rational getValueAsRational(Rational defaultValue) { + Rational[] r = getValueAsRationals(); + if (r == null || r.length < 1) { + return defaultValue; + } + return r[0]; + } + + /** + * Gets the value as a Rational. If there are more than 1 Rationals in this + * value, gets the first one. This method should be used for tags of type + * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}. + * + * @param defaultValue the numerator of the Rational to return if tag's + * value does not exist or cannot be converted to a Rational (the + * denominator will be 1). + * @return the tag's value as a Rational, or the defaultValue. + */ + public Rational getValueAsRational(long defaultValue) { + Rational defaultVal = new Rational(defaultValue, 1); + return getValueAsRational(defaultVal); + } + + /** + * Gets the value as an array of ints. This method should be used for tags + * of type {@link #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}. + * + * @return the value as as an array of ints, or null if the tag's value does + * not exist or cannot be converted to an array of ints. + */ + public int[] getValueAsInts() { + if (mValue == null) { + return null; + } else if (mValue instanceof long[]) { + long[] val = (long[]) mValue; + int[] arr = new int[val.length]; + for (int i = 0; i < val.length; i++) { + arr[i] = (int) val[i]; // Truncates + } + return arr; + } + return null; + } + + /** + * Gets the value as an int. If there are more than 1 ints in this value, + * gets the first one. This method should be used for tags of type + * {@link #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}. + * + * @param defaultValue the int to return if tag's value does not exist or + * cannot be converted to an int. + * @return the tag's value as a int, or the defaultValue. + */ + public int getValueAsInt(int defaultValue) { + int[] i = getValueAsInts(); + if (i == null || i.length < 1) { + return defaultValue; + } + return i[0]; + } + + /** + * Gets the value as an array of longs. This method should be used for tags + * of type {@link #TYPE_UNSIGNED_LONG}. + * + * @return the value as as an array of longs, or null if the tag's value + * does not exist or cannot be converted to an array of longs. + */ + public long[] getValueAsLongs() { + if (mValue instanceof long[]) { + return (long[]) mValue; + } + return null; + } + + /** + * Gets the value or null if none exists. If there are more than 1 longs in + * this value, gets the first one. This method should be used for tags of + * type {@link #TYPE_UNSIGNED_LONG}. + * + * @param defaultValue the long to return if tag's value does not exist or + * cannot be converted to a long. + * @return the tag's value as a long, or the defaultValue. + */ + public long getValueAsLong(long defaultValue) { + long[] l = getValueAsLongs(); + if (l == null || l.length < 1) { + return defaultValue; + } + return l[0]; + } + + /** + * Gets the tag's value or null if none exists. + */ + public Object getValue() { + return mValue; + } + + /** + * Gets a long representation of the value. + * + * @param defaultValue value to return if there is no value or value is a + * rational with a denominator of 0. + * @return the tag's value as a long, or defaultValue if no representation + * exists. + */ + public long forceGetValueAsLong(long defaultValue) { + long[] l = getValueAsLongs(); + if (l != null && l.length >= 1) { + return l[0]; + } + byte[] b = getValueAsBytes(); + if (b != null && b.length >= 1) { + return b[0]; + } + Rational[] r = getValueAsRationals(); + if (r != null && r.length >= 1 && r[0].getDenominator() != 0) { + return (long) r[0].toDouble(); + } + return defaultValue; + } + + /** + * Gets a string representation of the value. + */ + public String forceGetValueAsString() { + if (mValue == null) { + return ""; + } else if (mValue instanceof byte[]) { + if (mDataType == TYPE_ASCII) { + return new String((byte[]) mValue, US_ASCII); + } else { + return Arrays.toString((byte[]) mValue); + } + } else if (mValue instanceof long[]) { + if (((long[]) mValue).length == 1) { + return String.valueOf(((long[]) mValue)[0]); + } else { + return Arrays.toString((long[]) mValue); + } + } else if (mValue instanceof Object[]) { + if (((Object[]) mValue).length == 1) { + Object val = ((Object[]) mValue)[0]; + if (val == null) { + return ""; + } else { + return val.toString(); + } + } else { + return Arrays.toString((Object[]) mValue); + } + } else { + return mValue.toString(); + } + } + + /** + * Gets the value for type {@link #TYPE_ASCII}, {@link #TYPE_LONG}, + * {@link #TYPE_UNDEFINED}, {@link #TYPE_UNSIGNED_BYTE}, + * {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_UNSIGNED_SHORT}. For + * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}, call + * {@link #getRational(int)} instead. + * + * @exception IllegalArgumentException if the data type is + * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}. + */ + protected long getValueAt(int index) { + if (mValue instanceof long[]) { + return ((long[]) mValue)[index]; + } else if (mValue instanceof byte[]) { + return ((byte[]) mValue)[index]; + } + throw new IllegalArgumentException("Cannot get integer value from " + + convertTypeToString(mDataType)); + } + + /** + * Gets the {@link #TYPE_ASCII} data. + * + * @exception IllegalArgumentException If the type is NOT + * {@link #TYPE_ASCII}. + */ + protected String getString() { + if (mDataType != TYPE_ASCII) { + throw new IllegalArgumentException("Cannot get ASCII value from " + + convertTypeToString(mDataType)); + } + return new String((byte[]) mValue, US_ASCII); + } + + /* + * Get the converted ascii byte. Used by ExifOutputStream. + */ + protected byte[] getStringByte() { + return (byte[]) mValue; + } + + /** + * Gets the {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL} data. + * + * @exception IllegalArgumentException If the type is NOT + * {@link #TYPE_RATIONAL} or {@link #TYPE_UNSIGNED_RATIONAL}. + */ + protected Rational getRational(int index) { + if ((mDataType != TYPE_RATIONAL) && (mDataType != TYPE_UNSIGNED_RATIONAL)) { + throw new IllegalArgumentException("Cannot get RATIONAL value from " + + convertTypeToString(mDataType)); + } + return ((Rational[]) mValue)[index]; + } + + /** + * Equivalent to getBytes(buffer, 0, buffer.length). + */ + protected void getBytes(byte[] buf) { + getBytes(buf, 0, buf.length); + } + + /** + * Gets the {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE} data. + * + * @param buf the byte array in which to store the bytes read. + * @param offset the initial position in buffer to store the bytes. + * @param length the maximum number of bytes to store in buffer. If length > + * component count, only the valid bytes will be stored. + * @exception IllegalArgumentException If the type is NOT + * {@link #TYPE_UNDEFINED} or {@link #TYPE_UNSIGNED_BYTE}. + */ + protected void getBytes(byte[] buf, int offset, int length) { + if ((mDataType != TYPE_UNDEFINED) && (mDataType != TYPE_UNSIGNED_BYTE)) { + throw new IllegalArgumentException("Cannot get BYTE value from " + + convertTypeToString(mDataType)); + } + System.arraycopy(mValue, 0, buf, offset, + (length > mComponentCountActual) ? mComponentCountActual : length); + } + + /** + * Gets the offset of this tag. This is only valid if this data size > 4 and + * contains an offset to the location of the actual value. + */ + protected int getOffset() { + return mOffset; + } + + /** + * Sets the offset of this tag. + */ + protected void setOffset(int offset) { + mOffset = offset; + } + + protected void setHasDefinedCount(boolean d) { + mHasDefinedDefaultComponentCount = d; + } + + protected boolean hasDefinedCount() { + return mHasDefinedDefaultComponentCount; + } + + private boolean checkBadComponentCount(int count) { + if (mHasDefinedDefaultComponentCount && (mComponentCountActual != count)) { + return true; + } + return false; + } + + private static String convertTypeToString(short type) { + switch (type) { + case TYPE_UNSIGNED_BYTE: + return "UNSIGNED_BYTE"; + case TYPE_ASCII: + return "ASCII"; + case TYPE_UNSIGNED_SHORT: + return "UNSIGNED_SHORT"; + case TYPE_UNSIGNED_LONG: + return "UNSIGNED_LONG"; + case TYPE_UNSIGNED_RATIONAL: + return "UNSIGNED_RATIONAL"; + case TYPE_UNDEFINED: + return "UNDEFINED"; + case TYPE_LONG: + return "LONG"; + case TYPE_RATIONAL: + return "RATIONAL"; + default: + return ""; + } + } + + private boolean checkOverflowForUnsignedShort(int[] value) { + for (int v : value) { + if (v > UNSIGNED_SHORT_MAX || v < 0) { + return true; + } + } + return false; + } + + private boolean checkOverflowForUnsignedLong(long[] value) { + for (long v : value) { + if (v < 0 || v > UNSIGNED_LONG_MAX) { + return true; + } + } + return false; + } + + private boolean checkOverflowForUnsignedLong(int[] value) { + for (int v : value) { + if (v < 0) { + return true; + } + } + return false; + } + + private boolean checkOverflowForUnsignedRational(Rational[] value) { + for (Rational v : value) { + if (v.getNumerator() < 0 || v.getDenominator() < 0 + || v.getNumerator() > UNSIGNED_LONG_MAX + || v.getDenominator() > UNSIGNED_LONG_MAX) { + return true; + } + } + return false; + } + + private boolean checkOverflowForRational(Rational[] value) { + for (Rational v : value) { + if (v.getNumerator() < LONG_MIN || v.getDenominator() < LONG_MIN + || v.getNumerator() > LONG_MAX + || v.getDenominator() > LONG_MAX) { + return true; + } + } + return false; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj instanceof ExifTag) { + ExifTag tag = (ExifTag) obj; + if (tag.mTagId != this.mTagId + || tag.mComponentCountActual != this.mComponentCountActual + || tag.mDataType != this.mDataType) { + return false; + } + if (mValue != null) { + if (tag.mValue == null) { + return false; + } else if (mValue instanceof long[]) { + if (!(tag.mValue instanceof long[])) { + return false; + } + return Arrays.equals((long[]) mValue, (long[]) tag.mValue); + } else if (mValue instanceof Rational[]) { + if (!(tag.mValue instanceof Rational[])) { + return false; + } + return Arrays.equals((Rational[]) mValue, (Rational[]) tag.mValue); + } else if (mValue instanceof byte[]) { + if (!(tag.mValue instanceof byte[])) { + return false; + } + return Arrays.equals((byte[]) mValue, (byte[]) tag.mValue); + } else { + return mValue.equals(tag.mValue); + } + } else { + return tag.mValue == null; + } + } + return false; + } + + @Override + public String toString() { + return String.format("tag id: %04X\n", mTagId) + "ifd id: " + mIfd + "\ntype: " + + convertTypeToString(mDataType) + "\ncount: " + mComponentCountActual + + "\noffset: " + mOffset + "\nvalue: " + forceGetValueAsString() + "\n"; + } + +} diff --git a/gallerycommon/src/com/android/gallery3d/exif/IfdData.java b/gallerycommon/src/com/android/gallery3d/exif/IfdData.java new file mode 100644 index 000000000..093944aec --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/exif/IfdData.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.exif; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class stores all the tags in an IFD. + * + * @see ExifData + * @see ExifTag + */ +class IfdData { + + private final int mIfdId; + private final Map<Short, ExifTag> mExifTags = new HashMap<Short, ExifTag>(); + private int mOffsetToNextIfd = 0; + private static final int[] sIfds = { + IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1, IfdId.TYPE_IFD_EXIF, + IfdId.TYPE_IFD_INTEROPERABILITY, IfdId.TYPE_IFD_GPS + }; + /** + * Creates an IfdData with given IFD ID. + * + * @see IfdId#TYPE_IFD_0 + * @see IfdId#TYPE_IFD_1 + * @see IfdId#TYPE_IFD_EXIF + * @see IfdId#TYPE_IFD_GPS + * @see IfdId#TYPE_IFD_INTEROPERABILITY + */ + IfdData(int ifdId) { + mIfdId = ifdId; + } + + static protected int[] getIfds() { + return sIfds; + } + + /** + * Get a array the contains all {@link ExifTag} in this IFD. + */ + protected ExifTag[] getAllTags() { + return mExifTags.values().toArray(new ExifTag[mExifTags.size()]); + } + + /** + * Gets the ID of this IFD. + * + * @see IfdId#TYPE_IFD_0 + * @see IfdId#TYPE_IFD_1 + * @see IfdId#TYPE_IFD_EXIF + * @see IfdId#TYPE_IFD_GPS + * @see IfdId#TYPE_IFD_INTEROPERABILITY + */ + protected int getId() { + return mIfdId; + } + + /** + * Gets the {@link ExifTag} with given tag id. Return null if there is no + * such tag. + */ + protected ExifTag getTag(short tagId) { + return mExifTags.get(tagId); + } + + /** + * Adds or replaces a {@link ExifTag}. + */ + protected ExifTag setTag(ExifTag tag) { + tag.setIfd(mIfdId); + return mExifTags.put(tag.getTagId(), tag); + } + + protected boolean checkCollision(short tagId) { + return mExifTags.get(tagId) != null; + } + + /** + * Removes the tag of the given ID + */ + protected void removeTag(short tagId) { + mExifTags.remove(tagId); + } + + /** + * Gets the tags count in the IFD. + */ + protected int getTagCount() { + return mExifTags.size(); + } + + /** + * Sets the offset of next IFD. + */ + protected void setOffsetToNextIfd(int offset) { + mOffsetToNextIfd = offset; + } + + /** + * Gets the offset of next IFD. + */ + protected int getOffsetToNextIfd() { + return mOffsetToNextIfd; + } + + /** + * Returns true if all tags in this two IFDs are equal. Note that tags of + * IFDs offset or thumbnail offset will be ignored. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (obj instanceof IfdData) { + IfdData data = (IfdData) obj; + if (data.getId() == mIfdId && data.getTagCount() == getTagCount()) { + ExifTag[] tags = data.getAllTags(); + for (ExifTag tag : tags) { + if (ExifInterface.isOffsetTag(tag.getTagId())) { + continue; + } + ExifTag tag2 = mExifTags.get(tag.getTagId()); + if (!tag.equals(tag2)) { + return false; + } + } + return true; + } + } + return false; + } +} diff --git a/gallerycommon/src/com/android/gallery3d/exif/IfdId.java b/gallerycommon/src/com/android/gallery3d/exif/IfdId.java new file mode 100644 index 000000000..7842edbd4 --- /dev/null +++ b/gallerycommon/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/gallerycommon/src/com/android/gallery3d/exif/JpegHeader.java b/gallerycommon/src/com/android/gallery3d/exif/JpegHeader.java new file mode 100644 index 000000000..e3e787eff --- /dev/null +++ b/gallerycommon/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/gallerycommon/src/com/android/gallery3d/exif/OrderedDataOutputStream.java b/gallerycommon/src/com/android/gallery3d/exif/OrderedDataOutputStream.java new file mode 100644 index 000000000..428e6b9fc --- /dev/null +++ b/gallerycommon/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/gallerycommon/src/com/android/gallery3d/exif/Rational.java b/gallerycommon/src/com/android/gallery3d/exif/Rational.java new file mode 100644 index 000000000..591d63faf --- /dev/null +++ b/gallerycommon/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/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java new file mode 100644 index 000000000..44ccd4c6b --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.jpegstream; + +import android.graphics.Point; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class JPEGInputStream extends FilterInputStream { + private long JNIPointer = 0; // Used by JNI code. Don't touch. + + private boolean mValidConfig = false; + private boolean mConfigChanged = false; + private int mFormat = -1; + private byte[] mTmpBuffer = new byte[1]; + private int mWidth = 0; + private int mHeight = 0; + + public JPEGInputStream(InputStream in) { + super(in); + } + + public JPEGInputStream(InputStream in, int format) { + super(in); + setConfig(format); + } + + public boolean setConfig(int format) { + // Make sure format is valid + switch (format) { + case JpegConfig.FORMAT_GRAYSCALE: + case JpegConfig.FORMAT_RGB: + case JpegConfig.FORMAT_ABGR: + case JpegConfig.FORMAT_RGBA: + break; + default: + return false; + } + mFormat = format; + mValidConfig = true; + mConfigChanged = true; + return true; + } + + public Point getDimensions() throws IOException { + if (mValidConfig) { + applyConfigChange(); + return new Point(mWidth, mHeight); + } + return null; + } + + @Override + public int available() { + return 0; // TODO + } + + @Override + public void close() throws IOException { + cleanup(); + super.close(); + } + + @Override + public synchronized void mark(int readlimit) { + // Do nothing + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public int read() throws IOException { + read(mTmpBuffer, 0, 1); + return 0xFF & mTmpBuffer[0]; + } + + @Override + public int read(byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); + } + + @Override + public int read(byte[] buffer, int offset, int count) throws IOException { + if (offset < 0 || count < 0 || (offset + count) > buffer.length) { + throw new ArrayIndexOutOfBoundsException(String.format( + " buffer length %d, offset %d, length %d", + buffer.length, offset, count)); + } + if (!mValidConfig) { + return 0; + } + applyConfigChange(); + int flag = JpegConfig.J_ERROR_FATAL; + try { + flag = readDecodedBytes(buffer, offset, count); + } finally { + if (flag < 0) { + cleanup(); + } + } + if (flag < 0) { + switch (flag) { + case JpegConfig.J_DONE: + return -1; // Returns -1 after reading EOS. + default: + throw new IOException("Error reading jpeg stream"); + } + } + return flag; + } + + @Override + public synchronized void reset() throws IOException { + throw new IOException("Reset not supported."); + } + + @Override + public long skip(long byteCount) throws IOException { + if (byteCount <= 0) { + return 0; + } + // Shorten skip to a reasonable amount + int flag = skipDecodedBytes((int) (0x7FFFFFFF & byteCount)); + if (flag < 0) { + switch (flag) { + case JpegConfig.J_DONE: + return 0; // Returns 0 after reading EOS. + default: + throw new IOException("Error skipping jpeg stream"); + } + } + return flag; + } + + @Override + protected void finalize() throws Throwable { + try { + cleanup(); + } finally { + super.finalize(); + } + } + + private void applyConfigChange() throws IOException { + if (mConfigChanged) { + cleanup(); + Point dimens = new Point(0, 0); + int flag = setup(dimens, in, mFormat); + switch(flag) { + case JpegConfig.J_SUCCESS: + break; // allow setup to continue + case JpegConfig.J_ERROR_BAD_ARGS: + throw new IllegalArgumentException("Bad arguments to read"); + default: + throw new IOException("Error to reading jpeg headers."); + } + mWidth = dimens.x; + mHeight = dimens.y; + mConfigChanged = false; + } + } + + native private int setup(Point dimens, InputStream in, int format); + + native private void cleanup(); + + native private int readDecodedBytes( byte[] inBuffer, int offset, int inCount); + + native private int skipDecodedBytes(int bytes); + + static { + System.loadLibrary("jni_jpegstream"); + } +} diff --git a/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGOutputStream.java b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGOutputStream.java new file mode 100644 index 000000000..c49d3759c --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGOutputStream.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.jpegstream; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +public class JPEGOutputStream extends FilterOutputStream { + private long JNIPointer = 0; // Used by JNI code. Don't touch. + + private byte[] mTmpBuffer = new byte[1]; + private int mWidth = 0; + private int mHeight = 0; + private int mQuality = 0; + private int mFormat = -1; + private boolean mValidConfig = false; + private boolean mConfigChanged = false; + + public JPEGOutputStream(OutputStream out) { + super(out); + } + + public JPEGOutputStream(OutputStream out, int width, int height, int quality, + int format) { + super(out); + setConfig(width, height, quality, format); + } + + public boolean setConfig(int width, int height, int quality, int format) { + // Clamp quality to range (0, 100] + quality = Math.max(Math.min(quality, 100), 1); + + // Make sure format is valid + switch (format) { + case JpegConfig.FORMAT_GRAYSCALE: + case JpegConfig.FORMAT_RGB: + case JpegConfig.FORMAT_ABGR: + case JpegConfig.FORMAT_RGBA: + break; + default: + return false; + } + + // If valid, set configuration + if (width > 0 && height > 0) { + mWidth = width; + mHeight = height; + mFormat = format; + mQuality = quality; + mValidConfig = true; + mConfigChanged = true; + } else { + return false; + } + + return mValidConfig; + } + + @Override + public void close() throws IOException { + cleanup(); + super.close(); + } + + @Override + public void write(byte[] buffer, int offset, int length) throws IOException { + if (offset < 0 || length < 0 || (offset + length) > buffer.length) { + throw new ArrayIndexOutOfBoundsException(String.format( + " buffer length %d, offset %d, length %d", + buffer.length, offset, length)); + } + if (!mValidConfig) { + return; + } + if (mConfigChanged) { + cleanup(); + int flag = setup(out, mWidth, mHeight, mFormat, mQuality); + switch(flag) { + case JpegConfig.J_SUCCESS: + break; // allow setup to continue + case JpegConfig.J_ERROR_BAD_ARGS: + throw new IllegalArgumentException("Bad arguments to write"); + default: + throw new IOException("Error to writing jpeg headers."); + } + mConfigChanged = false; + } + int returnCode = JpegConfig.J_ERROR_FATAL; + try { + returnCode = writeInputBytes(buffer, offset, length); + } finally { + if (returnCode < 0) { + cleanup(); + } + } + if (returnCode < 0) { + throw new IOException("Error writing jpeg stream"); + } + } + + @Override + public void write(byte[] buffer) throws IOException { + write(buffer, 0, buffer.length); + } + + @Override + public void write(int oneByte) throws IOException { + mTmpBuffer[0] = (byte) oneByte; + write(mTmpBuffer); + } + + @Override + protected void finalize() throws Throwable { + try { + cleanup(); + } finally { + super.finalize(); + } + } + + native private int setup(OutputStream out, int width, int height, int format, int quality); + + native private void cleanup(); + + native private int writeInputBytes(byte[] inBuffer, int offset, int inCount); + + static { + System.loadLibrary("jni_jpegstream"); + } +} diff --git a/gallerycommon/src/com/android/gallery3d/jpegstream/JpegConfig.java b/gallerycommon/src/com/android/gallery3d/jpegstream/JpegConfig.java new file mode 100644 index 000000000..e514e3b8d --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/jpegstream/JpegConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.jpegstream; + +public interface JpegConfig { + // Pixel formats + public static final int FORMAT_GRAYSCALE = 0x001; // 1 byte/pixel + public static final int FORMAT_RGB = 0x003; // 3 bytes/pixel RGBRGBRGBRGB... + public static final int FORMAT_RGBA = 0x004; // 4 bytes/pixel RGBARGBARGBARGBA... + public static final int FORMAT_ABGR = 0x104; // 4 bytes/pixel ABGRABGRABGR... + + // Jni error codes + static final int J_SUCCESS = 0; + static final int J_ERROR_FATAL = -1; + static final int J_ERROR_BAD_ARGS = -2; + static final int J_EXCEPTION = -3; + static final int J_DONE = -4; +} diff --git a/gallerycommon/src/com/android/gallery3d/jpegstream/StreamUtils.java b/gallerycommon/src/com/android/gallery3d/jpegstream/StreamUtils.java new file mode 100644 index 000000000..abd8f681e --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/jpegstream/StreamUtils.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.jpegstream; + +import java.nio.ByteOrder; + +public class StreamUtils { + + private StreamUtils() { + } + + /** + * Copies the input byte array into the output int array with the given + * endianness. If input is not a multiple of 4, ignores the last 1-3 bytes + * and returns true. + */ + public static boolean byteToIntArray(int[] output, byte[] input, ByteOrder endianness) { + int length = input.length - (input.length % 4); + if (output.length * 4 < length) { + throw new ArrayIndexOutOfBoundsException("Output array is too short to hold input"); + } + if (endianness == ByteOrder.BIG_ENDIAN) { + for (int i = 0, j = 0; i < output.length; i++, j += 4) { + output[i] = ((input[j] & 0xFF) << 24) | ((input[j + 1] & 0xFF) << 16) + | ((input[j + 2] & 0xFF) << 8) | ((input[j + 3] & 0xFF)); + } + } else { + for (int i = 0, j = 0; i < output.length; i++, j += 4) { + output[i] = ((input[j + 3] & 0xFF) << 24) | ((input[j + 2] & 0xFF) << 16) + | ((input[j + 1] & 0xFF) << 8) | ((input[j] & 0xFF)); + } + } + return input.length % 4 != 0; + } + + public static int[] byteToIntArray(byte[] input, ByteOrder endianness) { + int[] output = new int[input.length / 4]; + byteToIntArray(output, input, endianness); + return output; + } + + /** + * Uses native endianness. + */ + public static int[] byteToIntArray(byte[] input) { + return byteToIntArray(input, ByteOrder.nativeOrder()); + } + + /** + * Returns the number of bytes in a pixel for a given format defined in + * JpegConfig. + */ + public static int pixelSize(int format) { + switch (format) { + case JpegConfig.FORMAT_ABGR: + case JpegConfig.FORMAT_RGBA: + return 4; + case JpegConfig.FORMAT_RGB: + return 3; + case JpegConfig.FORMAT_GRAYSCALE: + return 1; + default: + return -1; + } + } +} diff --git a/gallerycommon/src/com/android/gallery3d/util/Future.java b/gallerycommon/src/com/android/gallery3d/util/Future.java new file mode 100644 index 000000000..580a2a120 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/util/Future.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +// This Future differs from the java.util.concurrent.Future in these aspects: +// +// - Once cancel() is called, isCancelled() always returns true. It is a sticky +// flag used to communicate to the implementation. The implmentation may +// ignore that flag. Regardless whether the Future is cancelled, a return +// value will be provided to get(). The implementation may choose to return +// null if it finds the Future is cancelled. +// +// - get() does not throw exceptions. +// +public interface Future<T> { + public void cancel(); + public boolean isCancelled(); + public boolean isDone(); + public T get(); + public void waitDone(); +} diff --git a/gallerycommon/src/com/android/gallery3d/util/FutureListener.java b/gallerycommon/src/com/android/gallery3d/util/FutureListener.java new file mode 100644 index 000000000..ed1f820c7 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/util/FutureListener.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +public interface FutureListener<T> { + public void onFutureDone(Future<T> future); +} diff --git a/gallerycommon/src/com/android/gallery3d/util/PriorityThreadFactory.java b/gallerycommon/src/com/android/gallery3d/util/PriorityThreadFactory.java new file mode 100644 index 000000000..30d8e4a96 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/util/PriorityThreadFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.gallery3d.util; + + +import android.os.Process; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A thread factory that creates threads with a given thread priority. + */ +public class PriorityThreadFactory implements ThreadFactory { + + private final int mPriority; + private final AtomicInteger mNumber = new AtomicInteger(); + private final String mName; + + public PriorityThreadFactory(String name, int priority) { + mName = name; + mPriority = priority; + } + + @Override + public Thread newThread(Runnable r) { + return new Thread(r, mName + '-' + mNumber.getAndIncrement()) { + @Override + public void run() { + Process.setThreadPriority(mPriority); + super.run(); + } + }; + } + +} diff --git a/gallerycommon/src/com/android/gallery3d/util/ThreadPool.java b/gallerycommon/src/com/android/gallery3d/util/ThreadPool.java new file mode 100644 index 000000000..115dc6625 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/util/ThreadPool.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.util; + +import android.util.Log; + +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class ThreadPool { + @SuppressWarnings("unused") + private static final String TAG = "ThreadPool"; + private static final int CORE_POOL_SIZE = 4; + private static final int MAX_POOL_SIZE = 8; + private static final int KEEP_ALIVE_TIME = 10; // 10 seconds + + // Resource type + public static final int MODE_NONE = 0; + public static final int MODE_CPU = 1; + public static final int MODE_NETWORK = 2; + + public static final JobContext JOB_CONTEXT_STUB = new JobContextStub(); + + ResourceCounter mCpuCounter = new ResourceCounter(2); + ResourceCounter mNetworkCounter = new ResourceCounter(2); + + // A Job is like a Callable, but it has an addition JobContext parameter. + public interface Job<T> { + public T run(JobContext jc); + } + + public interface JobContext { + boolean isCancelled(); + void setCancelListener(CancelListener listener); + boolean setMode(int mode); + } + + private static class JobContextStub implements JobContext { + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void setCancelListener(CancelListener listener) { + } + + @Override + public boolean setMode(int mode) { + return true; + } + } + + public interface CancelListener { + public void onCancel(); + } + + private static class ResourceCounter { + public int value; + public ResourceCounter(int v) { + value = v; + } + } + + private final Executor mExecutor; + + public ThreadPool() { + this(CORE_POOL_SIZE, MAX_POOL_SIZE); + } + + public ThreadPool(int initPoolSize, int maxPoolSize) { + mExecutor = new ThreadPoolExecutor( + initPoolSize, maxPoolSize, KEEP_ALIVE_TIME, + TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), + new PriorityThreadFactory("thread-pool", + android.os.Process.THREAD_PRIORITY_BACKGROUND)); + } + + // Submit a job to the thread pool. The listener will be called when the + // job is finished (or cancelled). + public <T> Future<T> submit(Job<T> job, FutureListener<T> listener) { + Worker<T> w = new Worker<T>(job, listener); + mExecutor.execute(w); + return w; + } + + public <T> Future<T> submit(Job<T> job) { + return submit(job, null); + } + + private class Worker<T> implements Runnable, Future<T>, JobContext { + @SuppressWarnings("hiding") + private static final String TAG = "Worker"; + private Job<T> mJob; + private FutureListener<T> mListener; + private CancelListener mCancelListener; + private ResourceCounter mWaitOnResource; + private volatile boolean mIsCancelled; + private boolean mIsDone; + private T mResult; + private int mMode; + + public Worker(Job<T> job, FutureListener<T> listener) { + mJob = job; + mListener = listener; + } + + // This is called by a thread in the thread pool. + @Override + public void run() { + T result = null; + + // A job is in CPU mode by default. setMode returns false + // if the job is cancelled. + if (setMode(MODE_CPU)) { + try { + result = mJob.run(this); + } catch (Throwable ex) { + Log.w(TAG, "Exception in running a job", ex); + } + } + + synchronized(this) { + setMode(MODE_NONE); + mResult = result; + mIsDone = true; + notifyAll(); + } + if (mListener != null) mListener.onFutureDone(this); + } + + // Below are the methods for Future. + @Override + public synchronized void cancel() { + if (mIsCancelled) return; + mIsCancelled = true; + if (mWaitOnResource != null) { + synchronized (mWaitOnResource) { + mWaitOnResource.notifyAll(); + } + } + if (mCancelListener != null) { + mCancelListener.onCancel(); + } + } + + @Override + public boolean isCancelled() { + return mIsCancelled; + } + + @Override + public synchronized boolean isDone() { + return mIsDone; + } + + @Override + public synchronized T get() { + while (!mIsDone) { + try { + wait(); + } catch (Exception ex) { + Log.w(TAG, "ingore exception", ex); + // ignore. + } + } + return mResult; + } + + @Override + public void waitDone() { + get(); + } + + // Below are the methods for JobContext (only called from the + // thread running the job) + @Override + public synchronized void setCancelListener(CancelListener listener) { + mCancelListener = listener; + if (mIsCancelled && mCancelListener != null) { + mCancelListener.onCancel(); + } + } + + @Override + public boolean setMode(int mode) { + // Release old resource + ResourceCounter rc = modeToCounter(mMode); + if (rc != null) releaseResource(rc); + mMode = MODE_NONE; + + // Acquire new resource + rc = modeToCounter(mode); + if (rc != null) { + if (!acquireResource(rc)) { + return false; + } + mMode = mode; + } + + return true; + } + + private ResourceCounter modeToCounter(int mode) { + if (mode == MODE_CPU) { + return mCpuCounter; + } else if (mode == MODE_NETWORK) { + return mNetworkCounter; + } else { + return null; + } + } + + private boolean acquireResource(ResourceCounter counter) { + while (true) { + synchronized (this) { + if (mIsCancelled) { + mWaitOnResource = null; + return false; + } + mWaitOnResource = counter; + } + + synchronized (counter) { + if (counter.value > 0) { + counter.value--; + break; + } else { + try { + counter.wait(); + } catch (InterruptedException ex) { + // ignore. + } + } + } + } + + synchronized (this) { + mWaitOnResource = null; + } + + return true; + } + + private void releaseResource(ResourceCounter counter) { + synchronized (counter) { + counter.value++; + counter.notifyAll(); + } + } + } +} |