From a2fba687d4d2dbb3b2db8866b054ecb0e42871b2 Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Wed, 17 Aug 2011 22:07:43 +0800 Subject: Initial code for Gallery2. fix: 5176434 Change-Id: I041e282b9c7b34ceb1db8b033be2b853bb3a992c --- .../com/android/gallery3d/common/BitmapUtils.java | 285 +++++++++ .../com/android/gallery3d/common/BlobCache.java | 653 +++++++++++++++++++++ .../src/com/android/gallery3d/common/Entry.java | 56 ++ .../com/android/gallery3d/common/EntrySchema.java | 529 +++++++++++++++++ .../com/android/gallery3d/common/FileCache.java | 303 ++++++++++ .../com/android/gallery3d/common/Fingerprint.java | 191 ++++++ .../gallery3d/common/HttpClientFactory.java | 134 +++++ .../src/com/android/gallery3d/common/LruCache.java | 90 +++ .../src/com/android/gallery3d/common/Utils.java | 407 +++++++++++++ 9 files changed, 2648 insertions(+) create mode 100644 gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java create mode 100644 gallerycommon/src/com/android/gallery3d/common/BlobCache.java create mode 100644 gallerycommon/src/com/android/gallery3d/common/Entry.java create mode 100644 gallerycommon/src/com/android/gallery3d/common/EntrySchema.java create mode 100644 gallerycommon/src/com/android/gallery3d/common/FileCache.java create mode 100644 gallerycommon/src/com/android/gallery3d/common/Fingerprint.java create mode 100644 gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java create mode 100644 gallerycommon/src/com/android/gallery3d/common/LruCache.java create mode 100644 gallerycommon/src/com/android/gallery3d/common/Utils.java (limited to 'gallerycommon/src/com') 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..04cdc6142 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/BitmapUtils.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.gallery3d.common; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.os.Build; +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"; + public static final int UNCONSTRAINED = -1; + private static final int COMPRESS_JPEG_QUALITY = 90; + + 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) Math.ceil(Math.sqrt((double) (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.min(w / minSideLength, h / minSideLength); + if (initialSize <= 1) return 1; + + return initialSize <= 8 + ? Utils.prevPowerOf2(initialSize) + : initialSize / 8 * 8; + } + + // Fin the min x that 1 / x <= scale + public static int computeSampleSizeLarger(float scale) { + int initialSize = (int) Math.floor(1f / scale); + if (initialSize <= 1) return 1; + + return initialSize <= 8 + ? Utils.prevPowerOf2(initialSize) + : initialSize / 8 * 8; + } + + // Find the max x that 1 / x >= scale. + public static int computeSampleSize(float scale) { + Utils.assertTrue(scale > 0); + int initialSize = Math.max(1, (int) Math.ceil(1 / scale)); + return initialSize <= 8 + ? Utils.nextPowerOf2(initialSize) + : (initialSize + 7) / 8 * 8; + } + + public static Bitmap resizeDownToPixels( + Bitmap bitmap, int targetPixels, boolean recycle) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + float scale = (float) Math.sqrt( + (double) targetPixels / (width * height)); + if (scale >= 1.0f) return bitmap; + return resizeBitmapByScale(bitmap, scale, recycle); + } + + 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); + } + + // Crops a square from the center of the original image. + public static Bitmap cropCenter(Bitmap bitmap, boolean recycle) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + if (width == height) return bitmap; + int size = Math.min(width, height); + + Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap)); + Canvas canvas = new Canvas(target); + canvas.translate((size - width) / 2, (size - height) / 2); + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG); + canvas.drawBitmap(bitmap, 0, 0, paint); + if (recycle) bitmap.recycle(); + return target; + } + + public static Bitmap resizeDownAndCropCenter(Bitmap bitmap, int size, + boolean recycle) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + int minSide = Math.min(w, h); + if (w == h && minSide <= size) return bitmap; + size = Math.min(size, minSide); + + float scale = Math.max((float) size / bitmap.getWidth(), + (float) size / bitmap.getHeight()); + 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) { + 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 { + 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[] compressBitmap(Bitmap bitmap) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, + COMPRESS_JPEG_QUALITY, os); + return os.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"); + } + + public static byte[] compressToBytes(Bitmap bitmap, int quality) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(65536); + bitmap.compress(CompressFormat.JPEG, quality, baos); + return baos.toByteArray(); + + } + + +} 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..19a2e3090 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/BlobCache.java @@ -0,0 +1,653 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.zip.Adler32; + +public class BlobCache { + 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. + 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(); + } + + // 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 != 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..b8cc51205 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/Entry.java @@ -0,0 +1,56 @@ +/* + * 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 ""; + } + + 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..d652ac98a --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/EntrySchema.java @@ -0,0 +1,529 @@ +/* + * 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"; + + private static final int TYPE_STRING = 0; + private static final int TYPE_BOOLEAN = 1; + private static final int TYPE_SHORT = 2; + private static final int TYPE_INT = 3; + private static final int TYPE_LONG = 4; + private static final int TYPE_FLOAT = 5; + private static final int TYPE_DOUBLE = 6; + private 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 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; + } + + private 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 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 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"); + 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); + } + } + } + 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 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 clazz) { + ArrayList columns = new ArrayList(); + 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 clazz, ArrayList 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.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 fullText; + public final String defaultValue; + public final Field field; + public final int projectionIndex; + + public ColumnInfo(String name, int type, boolean indexed, + boolean fullText, String defaultValue, Field field, int projectionIndex) { + this.name = name.toLowerCase(); + this.type = type; + this.indexed = indexed; + 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..a69d6e170 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/FileCache.java @@ -0,0 +1,303 @@ +/* + * 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 com.android.gallery3d.common.Entry.Table; + +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 java.io.File; +import java.io.IOException; + +public class FileCache { + 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 mEntryMap = + new LruCache(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); + } + + 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; + mInitialized = true; + + 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); + } + + 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..39fcf9e09 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/Fingerprint.java @@ -0,0 +1,191 @@ +/* + * 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.ContentResolver; +import android.net.Uri; + +import java.io.FileInputStream; +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 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..cb95e3329 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/HttpClientFactory.java @@ -0,0 +1,134 @@ +/* + * 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 android.util.Log; + +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, + 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 { + + private final HashMap mLruMap; + private final HashMap> mWeakMap = + new HashMap>(); + private ReferenceQueue mQueue = new ReferenceQueue(); + + @SuppressWarnings("serial") + public LruCache(final int capacity) { + mLruMap = new LinkedHashMap(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > capacity; + } + }; + } + + private static class Entry extends WeakReference { + K mKey; + + public Entry(K key, V value, ReferenceQueue queue) { + super(value, queue); + mKey = key; + } + } + + @SuppressWarnings("unchecked") + private void cleanUpWeakMap() { + Entry entry = (Entry) mQueue.poll(); + while (entry != null) { + mWeakMap.remove(entry.mKey); + entry = (Entry) 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 entry = mWeakMap.put( + key, new Entry(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 entry = mWeakMap.get(key); + return entry == null ? null : entry.get(); + } + + public synchronized void clear() { + mLruMap.clear(); + mWeakMap.clear(); + mQueue = new ReferenceQueue(); + } +} 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..efe2be213 --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/common/Utils.java @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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.PendingIntent; +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.Environment; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.StatFs; +import android.text.TextUtils; +import android.util.Log; + +import java.io.Closeable; +import java.io.InterruptedIOException; +import java.util.Random; + +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]; + + // Throws AssertionError if the input is false. + public static void assertTrue(boolean cond) { + if (!cond) { + throw new AssertionError(); + } + } + + // Throws AssertionError if the input is false. + public static void assertTrue(boolean cond, String message, Object ... args) { + if (!cond) { + throw new AssertionError( + args.length == 0 ? message : String.format(message, args)); + } + } + + // Throws NullPointerException if the input is null. + public static T checkNotNull(T object) { + if (object == null) throw new NullPointerException(); + return object; + } + + // Returns true if two input Object are both null or equal + // to each other. + public static boolean equals(Object a, Object b) { + return (a == b) || (a == null ? false : a.equals(b)); + } + + // Returns true if the input is power of 2. + // Throws IllegalArgumentException if the input is <= 0. + public static boolean isPowerOf2(int n) { + if (n <= 0) throw new IllegalArgumentException(); + return (n & -n) == n; + } + + // 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 -= 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 euclidean distance between (x, y) and (sx, sy). + public static float distance(float x, float y, float sx, float sy) { + float dx = x - sx; + float dy = y - sy; + return (float) Math.hypot(dx, dy); + } + + // 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(T[] array, int i, int j) { + T temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + + 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 (Throwable 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; + } + + // Used for debugging. Should be removed before submitting. + public static void debug(String format, Object ... args) { + if (args.length == 0) { + Log.d(DEBUG_TAG, format); + } else { + Log.d(DEBUG_TAG, String.format(format, args)); + } + } + + public static float parseFloatSafely(String content, float defaultValue) { + if (content == null) return defaultValue; + try { + return Float.parseFloat(content); + } catch (NumberFormatException e) { + Log.w(TAG, "invalid float: " + content, e); + return defaultValue; + } + } + + public static int parseIntSafely(String content, int defaultValue) { + if (content == null) return defaultValue; + try { + return Integer.parseInt(content); + } catch (NumberFormatException e) { + Log.w(TAG, "invalid int: " + content, e); + return defaultValue; + } + } + + public static boolean isNullOrEmpty(String exifMake) { + return TextUtils.isEmpty(exifMake); + } + + public static boolean hasSpaceForSize(long size) { + String state = Environment.getExternalStorageState(); + if (!Environment.MEDIA_MOUNTED.equals(state)) { + return false; + } + + String path = Environment.getExternalStorageDirectory().getPath(); + try { + StatFs stat = new StatFs(path); + return stat.getAvailableBlocks() * (long) stat.getBlockSize() > size; + } catch (Exception e) { + Log.i(TAG, "Fail to access external storage", e); + } + return false; + } + + public static void waitWithoutInterrupt(Object object) { + try { + object.wait(); + } catch (InterruptedException e) { + Log.w(TAG, "unexpected interrupt: " + object); + } + } + + public static void shuffle(int array[], Random random) { + for (int i = array.length; i > 0; --i) { + int t = random.nextInt(i); + if (t == i - 1) continue; + int tmp = array[i - 1]; + array[i - 1] = array[t]; + array[t] = tmp; + } + } + + 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, + 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; + } + + public static PendingIntent deserializePendingIntent(byte[] rawPendingIntent) { + Parcel parcel = null; + try { + if (rawPendingIntent != null) { + parcel = Parcel.obtain(); + parcel.unmarshall(rawPendingIntent, 0, rawPendingIntent.length); + return PendingIntent.readPendingIntentOrNullFromParcel(parcel); + } else { + return null; + } + } catch (Exception e) { + throw new IllegalArgumentException("error parsing PendingIntent"); + } finally { + if (parcel != null) parcel.recycle(); + } + } + + public static byte[] serializePendingIntent(PendingIntent pendingIntent) { + Parcel parcel = null; + try { + parcel = Parcel.obtain(); + PendingIntent.writePendingIntentOrNullToParcel(pendingIntent, parcel); + return parcel.marshall(); + } finally { + if (parcel != null) parcel.recycle(); + } + } +} -- cgit v1.2.3