Android Open Source - droid-fu Abstract Cache






From Project

Back to project page droid-fu.

License

The source code is released under:

Apache License

If you think the Android project droid-fu listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/* Copyright (c) 2009 Matthias Kaeppler
 */*w  w w  . ja v  a2  s . com*/
 * 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.github.droidfu.cachefu;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

import android.content.Context;
import android.os.Environment;
import android.util.Log;

import com.github.droidfu.support.StringSupport;
import com.google.common.collect.MapMaker;

/**
 * <p>
 * A simple 2-level cache consisting of a small and fast in-memory cache (1st level cache) and an
 * (optional) slower but bigger disk cache (2nd level cache). For disk caching, either the
 * application's cache directory or the SD card can be used. Please note that in the case of the app
 * cache dir, Android may at any point decide to wipe that entire directory if it runs low on
 * internal storage. The SD card cache <i>must</i> be managed by the application, e.g. by calling
 * {@link #wipe} whenever the app quits.
 * </p>
 * <p>
 * When pulling from the cache, it will first attempt to load the data from memory. If that fails,
 * it will try to load it from disk (assuming disk caching is enabled). If that succeeds, the data
 * will be put in the in-memory cache and returned (read-through). Otherwise it's a cache miss.
 * </p>
 * <p>
 * Pushes to the cache are always write-through (i.e. the data will be stored both on disk, if disk
 * caching is enabled, and in memory).
 * </p>
 * 
 * @author Matthias Kaeppler
 */
public abstract class AbstractCache<KeyT, ValT> implements Map<KeyT, ValT> {

    public static final int DISK_CACHE_INTERNAL = 0;
    public static final int DISK_CACHE_SDCARD = 1;

    private static final String LOG_TAG = "Droid-Fu[CacheFu]";

    private boolean isDiskCacheEnabled;

    protected String diskCacheDirectory;

    private ConcurrentMap<KeyT, ValT> cache;

    private String name;
    
    private long expirationInMinutes;

    /**
     * Creates a new cache instance.
     * 
     * @param name
     *            a human readable identifier for this cache. Note that this value will be used to
     *            derive a directory name if the disk cache is enabled, so don't get too creative
     *            here (camel case names work great)
     * @param initialCapacity
     *            the initial element size of the cache
     * @param expirationInMinutes
     *            time in minutes after which elements will be purged from the cache
     * @param maxConcurrentThreads
     *            how many threads you think may at once access the cache; this need not be an exact
     *            number, but it helps in fragmenting the cache properly
     */
    public AbstractCache(String name, int initialCapacity, long expirationInMinutes,
            int maxConcurrentThreads) {

        this.name = name;
        this.expirationInMinutes = expirationInMinutes;
                
        MapMaker mapMaker = new MapMaker();
        mapMaker.initialCapacity(initialCapacity);
        mapMaker.expiration(expirationInMinutes * 60, TimeUnit.SECONDS);
        mapMaker.concurrencyLevel(maxConcurrentThreads);
        mapMaker.softValues();
        this.cache = mapMaker.makeMap();
    }

    /**
     * Sanitize disk cache. Remove files which are older than expirationInMinutes.
     */
    private void sanitizeDiskCache() {
        File[] cachedFiles = new File(diskCacheDirectory).listFiles();
        if (cachedFiles == null) {
            return;
        }
        for (File f : cachedFiles) {
          // if file older than expirationInMinutes, remove it
          long lastModified = f.lastModified();
          Date now = new Date();
          long ageInMinutes = ((now.getTime() - lastModified) / (1000*60));
          
          if (ageInMinutes >= expirationInMinutes) {
            Log.d(name, "DISK cache expiration for file " + f.toString());
            f.delete();
          }
        }
  }

  /**
     * Enable caching to the phone's internal storage or SD card.
     * 
     * @param context
     *            the current context
     * @param storageDevice
     *            where to store the cached files, either {@link #DISK_CACHE_INTERNAL} or
     *            {@link #DISK_CACHE_SDCARD})
     * @return
     */
    public boolean enableDiskCache(Context context, int storageDevice) {
        Context appContext = context.getApplicationContext();

        String rootDir = null;
        if (storageDevice == DISK_CACHE_SDCARD
                && Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            // SD-card available
            rootDir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/"
                    + appContext.getPackageName() + "/cache";
        } else {
            File internalCacheDir = appContext.getCacheDir();
            // apparently on some configurations this can come back as null
            if (internalCacheDir == null) {
                return (isDiskCacheEnabled = false);
            }
            rootDir = internalCacheDir.getAbsolutePath();
        }

        setRootDir(rootDir);

        File outFile = new File(diskCacheDirectory);
        if (outFile.mkdirs()) {
            File nomedia = new File(diskCacheDirectory, ".nomedia");
            try {
                nomedia.createNewFile();
            } catch (IOException e) {
                Log.e(LOG_TAG, "Failed creating .nomedia file");
            }
        }

        isDiskCacheEnabled = outFile.exists();

        if (!isDiskCacheEnabled) {
            Log.w(LOG_TAG, "Failed creating disk cache directory " + diskCacheDirectory);
        } else {
            Log.d(name, "enabled write through to " + diskCacheDirectory);

            // sanitize disk cache
            Log.d(name, "sanitize DISK cache");
            sanitizeDiskCache();
        }

        return isDiskCacheEnabled;
    }

    private void setRootDir(String rootDir) {
        this.diskCacheDirectory = rootDir + "/cachefu/"
                + StringSupport.underscore(name.replaceAll("\\s", ""));
    }

    /**
     * Only meaningful if disk caching is enabled. See {@link #enableDiskCache}.
     * 
     * @return the full absolute path to the directory where files are cached, if the disk cache is
     *         enabled, otherwise null
     */
    public String getDiskCacheDirectory() {
        return diskCacheDirectory;
    }

    /**
     * Only meaningful if disk caching is enabled. See {@link #enableDiskCache}. Turns a cache key
     * into the file name that will be used to persist the value to disk. Subclasses must implement
     * this.
     * 
     * @param key
     *            the cache key
     * @return the file name
     */
    public abstract String getFileNameForKey(KeyT key);

    /**
     * Only meaningful if disk caching is enabled. See {@link #enableDiskCache}. Restores a value
     * previously persisted to the disk cache.
     * 
     * @param file
     *            the file holding the cached value
     * @return the cached value
     * @throws IOException
     */
    protected abstract ValT readValueFromDisk(File file) throws IOException;

    /**
     * Only meaningful if disk caching is enabled. See {@link #enableDiskCache}. Persists a value to
     * the disk cache.
     * 
     * @param ostream
     *            the file output stream (buffered).
     * @param value
     *            the cache value to persist
     * @throws IOException
     */
    protected abstract void writeValueToDisk(File file, ValT value) throws IOException;

    private void cacheToDisk(KeyT key, ValT value) {
        File file = new File(diskCacheDirectory + "/" + getFileNameForKey(key));
        try {
            file.createNewFile();
            file.deleteOnExit();

            writeValueToDisk(file, value);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private File getFileForKey(KeyT key) {
        return new File(diskCacheDirectory + "/" + getFileNameForKey(key));
    }

    /**
     * Reads a value from the cache by probing the in-memory cache, and if enabled and the in-memory
     * probe was a miss, the disk cache.
     * 
     * @param elementKey
     *            the cache key
     * @return the cached value, or null if element was not cached
     */
    @SuppressWarnings("unchecked")
    public synchronized ValT get(Object elementKey) {
        KeyT key = (KeyT) elementKey;
        ValT value = cache.get(key);
        if (value != null) {
            // memory hit
            Log.d(name, "MEM cache hit for " + key.toString());
            return value;
        }

        // memory miss, try reading from disk
        File file = getFileForKey(key);
        if (file.exists()) {
          // if file older than expirationInMinutes, remove it
          long lastModified = file.lastModified();
          Date now = new Date();
          long ageInMinutes = ((now.getTime() - lastModified) / (1000*60));
          
          if (ageInMinutes >= expirationInMinutes) {
            Log.d(name, "DISK cache expiration for file " + file.toString());
            file.delete();
            return null;
          }
          
          // disk hit
            Log.d(name, "DISK cache hit for " + key.toString());
            try {
                value = readValueFromDisk(file);
            } catch (IOException e) {
                // treat decoding errors as a cache miss
                e.printStackTrace();
                return null;
            }
            if (value == null) {
                return null;
            }
            cache.put(key, value);
            return value;
        }

        // cache miss
        return null;
    }

    /**
     * Writes an element to the cache. NOTE: If disk caching is enabled, this will write through to
     * the disk, which may introduce a performance penalty.
     */
    public synchronized ValT put(KeyT key, ValT value) {
        if (isDiskCacheEnabled) {
            cacheToDisk(key, value);
        }

        return cache.put(key, value);
    }

    public synchronized void putAll(Map<? extends KeyT, ? extends ValT> t) {
        throw new UnsupportedOperationException();
    }

    /**
     * Checks if a value is present in the cache. If the disk cached is enabled, this will also
     * check whether the value has been persisted to disk.
     * 
     * @param key
     *            the cache key
     * @return true if the value is cached in memory or on disk, false otherwise
     */
    @SuppressWarnings("unchecked")
    public synchronized boolean containsKey(Object key) {
        return cache.containsKey(key) || (isDiskCacheEnabled && getFileForKey((KeyT) key).exists());
    }

    /**
     * Checks if a value is present in the in-memory cache. This method ignores the disk cache.
     * 
     * @param key
     *            the cache key
     * @return true if the value is currently hold in memory, false otherwise
     */
    public synchronized boolean containsKeyInMemory(Object key) {
        return cache.containsKey(key);
    }

    /**
     * Checks if the given value is currently hold in memory.
     */
    public synchronized boolean containsValue(Object value) {
        return cache.containsValue(value);
    }

    @SuppressWarnings("unchecked")
    public synchronized ValT remove(Object key) {
        ValT value = removeKey(key);

        if (isDiskCacheEnabled) {
            File cachedValue = getFileForKey((KeyT) key);
            if (cachedValue.exists()) {
                cachedValue.delete();
            }
        }

        return value;
    }

    // Forced key expiration
    public ValT removeKey(Object key) {
        return cache.remove(key);
    }

    public Set<KeyT> keySet() {
        return cache.keySet();
    }

    public Set<Map.Entry<KeyT, ValT>> entrySet() {
        return cache.entrySet();
    }

    public synchronized int size() {
        return cache.size();
    }

    public synchronized boolean isEmpty() {
        return cache.isEmpty();
    }

    public boolean isDiskCacheEnabled() {
        return isDiskCacheEnabled;
    }

    /**
     * 
     * @param rootDir
     *            a folder name to enable caching or null to disable it.
     */
    public void setDiskCacheEnabled(String rootDir) {
        if (rootDir != null && rootDir.length() > 0) {
            setRootDir(rootDir);
            this.isDiskCacheEnabled = true;
        } else {
            this.isDiskCacheEnabled = false;
        }
    }

    public synchronized void clear() {
        cache.clear();

        if (isDiskCacheEnabled) {
            File[] cachedFiles = new File(diskCacheDirectory).listFiles();
            if (cachedFiles == null) {
                return;
            }
            for (File f : cachedFiles) {
                f.delete();
            }
        }

        Log.d(LOG_TAG, "Cache cleared");
    }

    public Collection<ValT> values() {
        return cache.values();
    }
}




Java Source Code List

com.github.droidfu.DroidFuApplication.java
com.github.droidfu.DroidFu.java
com.github.droidfu.activities.BetterActivityHelper.java
com.github.droidfu.activities.BetterActivity.java
com.github.droidfu.activities.BetterDefaultActivity.java
com.github.droidfu.activities.BetterExpandableListActivity.java
com.github.droidfu.activities.BetterListActivity.java
com.github.droidfu.activities.BetterMapActivity.java
com.github.droidfu.activities.BetterPreferenceActivity.java
com.github.droidfu.adapters.ListAdapterWithProgress.java
com.github.droidfu.adapters.WebGalleryAdapter.java
com.github.droidfu.cachefu.AbstractCache.java
com.github.droidfu.cachefu.CacheHelper.java
com.github.droidfu.cachefu.CachedList.java
com.github.droidfu.cachefu.CachedModel.java
com.github.droidfu.cachefu.HttpResponseCache.java
com.github.droidfu.cachefu.ImageCache.java
com.github.droidfu.cachefu.ModelCache.java
com.github.droidfu.concurrent.BetterAsyncTaskCallable.java
com.github.droidfu.concurrent.BetterAsyncTask.java
com.github.droidfu.dialogs.DialogClickListener.java
com.github.droidfu.exception.ResourceMessageException.java
com.github.droidfu.http.BetterHttpRequestBase.java
com.github.droidfu.http.BetterHttpRequestRetryHandler.java
com.github.droidfu.http.BetterHttpRequest.java
com.github.droidfu.http.BetterHttpResponseImpl.java
com.github.droidfu.http.BetterHttpResponse.java
com.github.droidfu.http.BetterHttp.java
com.github.droidfu.http.CachedHttpRequest.java
com.github.droidfu.http.CachedHttpResponse.java
com.github.droidfu.http.ConnectionChangedBroadcastReceiver.java
com.github.droidfu.http.HttpDelete.java
com.github.droidfu.http.HttpGet.java
com.github.droidfu.http.HttpPost.java
com.github.droidfu.http.HttpPut.java
com.github.droidfu.http.ssl.EasySSLSocketFactory.java
com.github.droidfu.http.ssl.TrivialTrustManager.java
com.github.droidfu.imageloader.ImageLoaderHandler.java
com.github.droidfu.imageloader.ImageLoader.java
com.github.droidfu.listeners.MapGestureListener.java
com.github.droidfu.services.BetterService.java
com.github.droidfu.support.ArraySupport.java
com.github.droidfu.support.DiagnosticSupport.java
com.github.droidfu.support.DisplaySupport.java
com.github.droidfu.support.IntentSupport.java
com.github.droidfu.support.StringSupport.java
com.github.droidfu.testsupport.DroidFuAssertions.java
com.github.droidfu.widgets.WebImageView.java
com.google.common.base.FinalizableReferenceQueue.java
com.google.common.base.FinalizableReference.java
com.google.common.base.FinalizableSoftReference.java
com.google.common.base.FinalizableWeakReference.java
com.google.common.base.Function.java
com.google.common.base.Objects.java
com.google.common.base.internal.Finalizer.java
com.google.common.collect.AbstractMapEntry.java
com.google.common.collect.AsynchronousComputationException.java
com.google.common.collect.ComputationException.java
com.google.common.collect.CustomConcurrentHashMap.java
com.google.common.collect.ExpirationTimer.java
com.google.common.collect.MapMaker.java
com.google.common.collect.NullOutputException.java