package net.i2p.android.router.util; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.net.Uri; import net.i2p.android.router.provider.CacheProvider; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * A least recently used cache with a max number of entries * and a max total disk space. * Inserts and deletes entries in the local ContentProvider. * * Like Android's CacheManager but usable. */ public class AppCache { private static AppCache _instance; private static File _cacheDir; private static long _totalSize; private static ContentResolver _resolver; /** the LRU cache */ private final Map<Integer, Object> _cache; private static final Integer DUMMY = 0; private static final String DIR_NAME = "appCache"; /** fragment into this many subdirectories */ private static final int NUM_DIRS = 32; private static final int MAX_FILES = 1024; /** total used space */ private static final long MAX_SPACE = 1024 * 1024; private static final long MAX_AGE = 12 * 60 * 60 * 1000l; public static AppCache getInstance(Context ctx) { synchronized (AppCache.class) { if (_instance == null) _instance = new AppCache(ctx); } return _instance; } /** * If you don't have a context. Could return null. */ public static AppCache getInstance() { return _instance; } private AppCache(Context ctx) { _cacheDir = new File(ctx.getCacheDir(), DIR_NAME); _cacheDir.mkdir(); Util.d("AppCache cache dir " + _cacheDir); _resolver = ctx.getContentResolver(); _cache = new LHM(MAX_FILES); initialize(); } /** * Caller MUST close stream AND call either * addCacheFile() or removeCacheFile() after the data is written. * @param key no fragment allowed */ public OutputStream createCacheFile(Uri key) throws IOException { // remove any old file so the total stays correct removeCacheFile(key); File f = toFile(key); f.getParentFile().mkdirs(); return new FileOutputStream(f); } /** * Add a previously written file to the cache index. * Return a content:// uri for the cached content in question, * or null on error * * @param key no fragment allowed * @param setAsCurrentBase tell CacheProvider */ public Uri addCacheFile(Uri key, boolean setAsCurrentBase) { int hash = toHash(key); synchronized(_cache) { _cache.put(hash, DUMMY); } // file:/// uri //return Uri.fromFile(toFile(hash)).toString(); // content:// uri return insertContent(key, setAsCurrentBase); } /** * Remove a previously written file from the cache index and disk. * @param key no fragment allowed */ public void removeCacheFile(Uri key) { int hash = toHash(key); synchronized(_cache) { _cache.remove(hash); } deleteContent(key); } /** * Return a content:// uri for any cached content in question. * The file may or may not exist, and it may be deleted at any time. * Side effect: If exists, sets as current base * * @param key no fragment allowed */ public Uri getCacheUri(Uri key) { int hash = toHash(key); // poke the LRU Object present; synchronized(_cache) { present = _cache.get(hash); } if (present != null) setAsCurrentBase(key); return CacheProvider.getContentUri(key); } /** * Return an absolute file path for any cached content in question. * The file may or may not exist, and it may be deleted at any time. * @param key no fragment allowed */ public File getCacheFile(Uri key) { int hash = toHash(key); return toFile(hash); } ////// private below here private void initialize() { _totalSize = 0; List<File> fileList = new ArrayList<File>(MAX_FILES); long total = enumerate(_cacheDir, fileList); Util.d("AppCache found " + fileList.size() + " files totalling " + total + " bytes"); Collections.sort(fileList, new FileComparator()); // oldest first, delete if too big or too old, else add to LHM long now = System.currentTimeMillis(); for (File f : fileList) { if (total > MAX_SPACE || f.lastModified() < now - MAX_AGE) { total -= f.length(); f.delete(); } else { addToCache(f); // TODO insertContent } } Util.d("after init " + _cache.size() + " files totalling " + total + " bytes"); } /** oldest first */ private static class FileComparator implements Comparator<File> { public int compare(File l, File r) { return (int) (l.lastModified() - r.lastModified()); } } /** get all the files, deleting empty ones on the way, returning total size */ private static long enumerate(File dir, List<File> fileList) { long rv = 0; File[] files = dir.listFiles(); if (files == null) return 0; for (File f : files) { if (f.isDirectory()) { rv += enumerate(f, fileList); } else { long len = f.length(); if (len > 0) { fileList.add(f); rv += len; } else { f.delete(); } } } return rv; } /** for initialization only */ private void addToCache(File f) { try { int hash = toHash(f); synchronized(_cache) { _cache.put(hash, DUMMY); } } catch (IllegalArgumentException iae) { Util.d("Huh bad file?" + iae); f.delete(); } } /** for initialization only */ private static int toHash(File f) throws IllegalArgumentException { String path = f.getAbsolutePath(); int slash = path.lastIndexOf("/"); String basename = path.substring(slash + 1); try { return Integer.parseInt(basename); } catch (NumberFormatException nfe) { throw new IllegalArgumentException("bad file name " + f); } } /** * Just use the hashcode for the hash for now * TODO switch to something secure like SHA-1 */ private static int toHash(Uri key) { return key.toString().hashCode(); } /** * /path/to/cache/dir/(hashCode(key) % 32)/hashCode(key) */ private static File toFile(Uri key) { int hash = toHash(key); return toFile(hash); } private static File toFile(int hash) { int dir = hash % NUM_DIRS; if (dir < 0) dir = 0 - dir; return new File(_cacheDir, dir + "/" + hash); } /** * @return the uri inserted or null on failure */ private static Uri insertContent(Uri key, boolean setAsCurrentBase) { String path = toFile(key).getAbsolutePath(); ContentValues cv = new ContentValues(); cv.put(CacheProvider.DATA, path); if (setAsCurrentBase) cv.put(CacheProvider.CURRENT_BASE, Boolean.TRUE); Uri uri = CacheProvider.getContentUri(key); if (uri != null) { _resolver.insert(uri, cv); return uri; } return null; } /** * Set key as current base. May be content or i2p key. */ private static void setAsCurrentBase(Uri key) { ContentValues cv = new ContentValues(); cv.put(CacheProvider.CURRENT_BASE, Boolean.TRUE); Uri uri = CacheProvider.getContentUri(key); if (uri != null) _resolver.insert(uri, cv); } /** ok for now but we will need to store key in the map and delete by integer */ private static void deleteContent(Uri key) { Uri uri = CacheProvider.getContentUri(key); if (uri != null) _resolver.delete(uri, null, null); } /** * An LRU set of hashcodes, implemented on a HashMap. * We use a dummy for the value to save space, because the * hashcode key is reversable to the file name. * The put and remove methods are overridden to * keep the total size counter updated, and to delete the underlying file * on remove. */ private static class LHM extends LinkedHashMap<Integer, Object> { private static final long serialVersionUID = 1L; private final int _max; public LHM(int max) { super(max, 0.75f, true); _max = max; } /** Add the entry, and update the total size */ @Override public Object put(Integer key, Object value) { Object rv = super.put(key, value); File f = toFile(key); if (f.exists()) { _totalSize += f.length(); } return rv; } /** Remove the entry and the file, and update the total size */ @Override public Object remove(Object key) { Object rv = super.remove(key); if ( /* rv != null && */ key instanceof Integer) { File f = toFile((Integer) key); if (f.exists()) { _totalSize -= f.length(); f.delete(); Util.d("AppCache deleted file " + f); } } return rv; } @Override protected boolean removeEldestEntry(Map.Entry<Integer, Object> eldest) { if (size() > _max || _totalSize > MAX_SPACE) { Integer key = eldest.getKey(); remove(key); // TODO deleteContent() } // we modified the map, we must return false return false; } } }