/* * 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.aincc.lib.util; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; import android.os.Environment; import com.aincc.lib.io.DiskLruCache; /** * A simple disk LRU bitmap cache to illustrate how a disk cache would be used for bitmap caching. A * much more robust and efficient disk LRU cache solution can be found in the ICS source code * (libcore/luni/src/main/java/libcore/io/DiskLruCache.java) and is preferable to this simple * implementation. */ public class BitmapDiskLruCache { private static final String TAG = "DiskLruCache"; private static final String CACHE_FILENAME_PREFIX = "cache_"; private static final int MAX_REMOVALS = 4; private static final int INITIAL_CAPACITY = 32; private static final float LOAD_FACTOR = 0.75f; private final File mCacheDir; private int cacheSize = 0; private int cacheByteSize = 0; private final int maxCacheItemSize = 64; // 64 item default private long maxCacheByteSize = 1024 * 1024 * 5; // 5MB default private CompressFormat mCompressFormat = CompressFormat.JPEG; private int mCompressQuality = 70; private final Map<String, String> mLinkedHashMap = Collections.synchronizedMap(new LinkedHashMap<String, String>(INITIAL_CAPACITY, LOAD_FACTOR, true)); // 2012.08.20 aincc : add for using DiskLruCache private static final int APP_VERSION = 1; private static final int VALUE_COUNT = 1; private DiskLruCache cache; /** * A filename filter to use to identify the cache filenames which have CACHE_FILENAME_PREFIX * prepended. */ @SuppressWarnings("unused") private static final FilenameFilter cacheFileFilter = new FilenameFilter() { @Override public boolean accept(File dir, String filename) { return filename.startsWith(CACHE_FILENAME_PREFIX); } }; /** * Used to fetch an instance of DiskLruCache. * * @param context * @param cacheDir * @param maxByteSize * @return */ public static BitmapDiskLruCache openCache(Context context, File cacheDir, long maxByteSize) { if (!cacheDir.exists()) { cacheDir.mkdir(); } if (cacheDir.isDirectory() && cacheDir.canWrite() && Utils.getUsableSpace(cacheDir) > maxByteSize) { return new BitmapDiskLruCache(cacheDir, maxByteSize); } return null; } /** * Constructor that should not be called directly, instead use {@link BitmapDiskLruCache#openCache(Context, File, long)} which runs some extra checks before * creating a DiskLruCache instance. * * @param cacheDir * @param maxByteSize * @throws IOException */ private BitmapDiskLruCache(File cacheDir, long maxByteSize) { mCacheDir = cacheDir; maxCacheByteSize = maxByteSize; // 2012.08.20 aincc try { cache = DiskLruCache.open(cacheDir, APP_VERSION, VALUE_COUNT, maxByteSize); } catch (IOException e) { e.printStackTrace(); } } /** * Add a bitmap to the disk cache. * * @param key * A unique identifier for the bitmap. * @param data * The bitmap to store. */ public void put(String key, Bitmap data) { // 2012.08.20 aincc DiskLruCache.Editor editor = null; Logger.d1(TAG, "try image put on disk cache " + key); String keypath = getKeyPath(key); try { // 기존의 파일로 저장하기 위한 로직을 키값으로 사용한다. editor = cache.edit(keypath); if (null == editor) { return; } if (writeBitmapToFile(data, editor)) { cache.flush(); editor.commit(); Logger.d1(TAG, "image put on disk cache " + keypath); } else { editor.abort(); Logger.d1(TAG, "ERROR on : image put on disk cache " + keypath); } } catch (IOException e) { e.printStackTrace(); Logger.e1(TAG, "ERROR on : image put on disk cache " + keypath); try { if (null != editor) { editor.abort(); } } catch (IOException ignored) { } } // SIMPLE VERSION // synchronized (mLinkedHashMap) // { // if (mLinkedHashMap.get(key) == null) // { // try // { // final String file = createFilePath(mCacheDir, key); // if (writeBitmapToFile(data, file)) // { // put(key, file); // flushCache(); // } // } // catch (final FileNotFoundException e) // { // Log.e(TAG, "Error in put: " + e.getMessage()); // } // catch (final IOException e) // { // Log.e(TAG, "Error in put: " + e.getMessage()); // } // } // } } @SuppressWarnings("unused") private void put(String key, String file) { mLinkedHashMap.put(key, file); cacheSize = mLinkedHashMap.size(); cacheByteSize += new File(file).length(); } /** * Flush the cache, removing oldest entries if the total size is over the specified cache size. * Note that this isn't keeping track of stale files in the cache directory that aren't in the * HashMap. If the images and keys in the disk cache change often then they probably won't ever * be removed. */ @SuppressWarnings("unused") private void flushCache() { Entry<String, String> eldestEntry; File eldestFile; long eldestFileSize; int count = 0; while (count < MAX_REMOVALS && (cacheSize > maxCacheItemSize || cacheByteSize > maxCacheByteSize)) { eldestEntry = mLinkedHashMap.entrySet().iterator().next(); eldestFile = new File(eldestEntry.getValue()); eldestFileSize = eldestFile.length(); mLinkedHashMap.remove(eldestEntry.getKey()); eldestFile.delete(); cacheSize = mLinkedHashMap.size(); cacheByteSize -= eldestFileSize; count++; Logger.d1(TAG, "flushCache - Removed cache file, " + eldestFile + ", " + eldestFileSize); } } /** * Get an image from the disk cache. * * @param key * The unique identifier for the bitmap * @return The bitmap or null if not found */ public Bitmap get(String key) { // 2012.08.20 aincc Bitmap bitmap = null; DiskLruCache.Snapshot snapshot = null; String keypath = getKeyPath(key); try { snapshot = cache.get(keypath); if (null == snapshot) { return null; } final InputStream in = snapshot.getInputStream(0); if (null != in) { final BufferedInputStream buffIn = new BufferedInputStream(in, Utils.IO_BUFFER_SIZE); bitmap = BitmapFactory.decodeStream(buffIn); } } catch (IOException e) { e.printStackTrace(); } finally { if (null != snapshot) { snapshot.close(); } } Logger.d1(TAG, null == bitmap ? "" : "image read from disk " + keypath); return bitmap; // SIMPLE VERSION // synchronized (mLinkedHashMap) // { // final String file = mLinkedHashMap.get(key); // if (file != null) // { // Logger.d1(TAG, "Disk cache hit"); // return BitmapFactory.decodeFile(file); // } // else // { // final String existingFile = createFilePath(mCacheDir, key); // if (new File(existingFile).exists()) // { // put(key, existingFile); // Logger.d1(TAG, "Disk cache hit (existing file)"); // return BitmapFactory.decodeFile(existingFile); // } // } // return null; // } } /** * Checks if a specific key exist in the cache. * * @param key * The unique identifier for the bitmap * @return true if found, false otherwise */ public boolean containsKey(String key) { // 2012.08.20 aincc boolean contained = false; DiskLruCache.Snapshot snapshot = null; String keypath = getKeyPath(key); try { snapshot = cache.get(keypath); contained = null != snapshot; } catch (IOException e) { e.printStackTrace(); } finally { if (null != snapshot) { snapshot.close(); } } return contained; // SIMPLE VERSION // // See if the key is in our HashMap // if (mLinkedHashMap.containsKey(key)) // { // return true; // } // // // Now check if there's an actual file that exists based on the key // final String existingFile = createFilePath(mCacheDir, key); // if (new File(existingFile).exists()) // { // // File found, add it to the HashMap for future use // put(key, existingFile); // return true; // } // return false; } /** * Removes all disk cache entries from this instance cache dir */ public void clearCache() { // 2012.08.20 aincc Logger.d1(TAG, "disk cache CLEAR"); try { cache.delete(); } catch (IOException e) { e.printStackTrace(); } // SIMPLE VERSION // BitmapDiskLruCache.clearCache(mCacheDir); } /** * Removes all disk cache entries from the application cache directory in the uniqueName * sub-directory. * * @param context * The context to use * @param uniqueName * A unique cache directory name to append to the app cache directory */ public static void clearCache(Context context, String uniqueName) throws IOException { Logger.d1(TAG, "disk cache CLEAR " + uniqueName); File cacheDir = getDiskCacheDir(context, uniqueName); clearCache(cacheDir); } /** * Removes all disk cache entries from the given directory. This should not be called directly, * call {@link BitmapDiskLruCache#clearCache(Context, String)} or {@link BitmapDiskLruCache#clearCache()} instead. * * @param cacheDir * The directory to remove the cache files from */ private static void clearCache(File cacheDir) throws IOException { // 2012.08.20 aincc File[] files = cacheDir.listFiles(); if (files == null) { throw new IllegalArgumentException("not a directory: " + cacheDir); } for (File file : files) { if (file.isDirectory()) { clearCache(file); } if (!file.delete()) { // 2012.08.30 aincc : 실패해도 다른 파일을 삭제하기 위해 예외 발생 주석처리. // throw new IOException("failed to delete file: " + file); } } // SIMPLE VERSION // final File[] files = cacheDir.listFiles(cacheFileFilter); // for (int i = 0; i < files.length; i++) // { // files[i].delete(); // } } /** * Get a usable cache directory (external if available, internal otherwise). * * @param context * The context to use * @param uniqueName * A unique directory name to append to the cache dir * @return The cache dir */ public static File getDiskCacheDir(Context context, String uniqueName) { // Check if media is mounted or storage is built-in, if so, try and use external cache dir // otherwise use internal cache dir final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED || !Utils.isExternalStorageRemovable() ? Utils.getExternalCacheDir(context).getPath() : context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); } /** * Creates a constant cache file path given a target cache directory and an image key. * * @param cacheDir * @param key * @return */ public static String createFilePath(File cacheDir, String key) { try { // Use URLEncoder to ensure we have a valid filename, a tad hacky but it will do for // this example return cacheDir.getAbsolutePath() + File.separator + CACHE_FILENAME_PREFIX + URLEncoder.encode(key.replace("*", ""), "UTF-8"); } catch (final UnsupportedEncodingException e) { Logger.e1(TAG, "createFilePath - " + e); } return null; } /** * Create a constant cache file path using the current cache directory and an image key. * * @param key * @return */ public String createFilePath(String key) { return createFilePath(mCacheDir, key); } /** * * @since 1.0.0 * @param key * @return */ public String getKeyPath(String key) { try { // Use URLEncoder to ensure we have a valid filename, a tad hacky but it will do for // this example return URLEncoder.encode(key.replace("*", ""), "UTF-8"); } catch (final UnsupportedEncodingException e) { Logger.e1(TAG, "createFilePath - " + e); } return null; } /** * Sets the target compression format and quality for images written to the disk cache. * * @param compressFormat * @param quality */ public void setCompressParams(CompressFormat compressFormat, int quality) { mCompressFormat = compressFormat; mCompressQuality = quality; } /** * Writes a bitmap to a file. Call {@link BitmapDiskLruCache#setCompressParams(CompressFormat, int)} first to set the target bitmap compression and format. * * @param bitmap * @param file * @return */ @SuppressWarnings("unused") private boolean writeBitmapToFile(Bitmap bitmap, String file) throws IOException, FileNotFoundException { OutputStream out = null; try { out = new BufferedOutputStream(new FileOutputStream(file), Utils.IO_BUFFER_SIZE); return bitmap.compress(mCompressFormat, mCompressQuality, out); } finally { if (out != null) { out.close(); } } } /** * Writes a bitmap to a file. Call {@link BitmapDiskLruCache#setCompressParams(CompressFormat, int)} first to set the target bitmap compression and format. * * @param bitmap * @param file * @return */ private boolean writeBitmapToFile(Bitmap bitmap, DiskLruCache.Editor editor) throws IOException, FileNotFoundException { OutputStream out = null; try { out = new BufferedOutputStream(editor.newOutputStream(0), Utils.IO_BUFFER_SIZE); return bitmap.compress(mCompressFormat, mCompressQuality, out); } finally { if (out != null) { out.close(); } } } }