[
  {
    "path": ".gitignore",
    "content": "*.apk\nbin\ngen\n"
  },
  {
    "path": "LICENSE",
    "content": "* Copyright (c) 2012, Applidium\n* All rights reserved.\n* Redistribution and use in source and binary forms, with or without\n* modification, are permitted provided that the following conditions are met:\n*\n*     * Redistributions of source code must retain the above copyright\n*       notice, this list of conditions and the following disclaimer.\n*     * Redistributions in binary form must reproduce the above copyright\n*       notice, this list of conditions and the following disclaimer in the\n*       documentation and/or other materials provided with the distribution.\n*     * Neither the name of Applidium nor the names of its contributors may\n*       be used to endorse or promote products derived from this software\n*       without specific prior written permission.\n*\n* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY\n* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n* DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY\n* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\n* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# Shutterbug - Remote image loader with caching for Android\n\n`Shutterbug` is an Android library that lets you fetch remote images and cache them. It is particularly suited for displaying remote images in lists or grids as it includes a convenience subclass of `ImageView` (`FetchableImageView`) that make implementation a one-liner.\n\nA dual memory and disk cache was implemented. It makes use of two backports of Android classes: [LruCache][] for the memory part and [DiskLruCache][] for the disk part. `LruCache` was introduced by API Level 12, but we provide it here as a standalone class so you can use the library under lower level APIs. Both `LruCache` and `DiskLruCache` are licensed under the Apache Software License, 2.0.\n\n`Shutterbug` was inspired by [SDWebImage][] which does the same thing on iOS. It uses the same structure and interface. People who are familiar with `SDWebImage` on iOS will feel at home with `Shutterbug` on Android.\n\n[SDWebImage]: https://github.com/rs/SDWebImage\n[LruCache]: http://developer.android.com/reference/android/util/LruCache.html\n[DiskLruCache]: https://github.com/JakeWharton/DiskLruCache\n[Android Support Library]: http://developer.android.com/tools/extras/support-library.html\n\n## How to use\n\nFirst, ensure that the following permissions were added to your AndroidManifest.xml file:\n\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>\n\nThen, you just have to add the jar or the library project to your project.\n\n### Basic usage\n\nYou only need a context, an url and an `ImageView`:\n\n\tShutterbugManager.getSharedImageManager(context).download(url, imageView);\n\n### Using FetchableImageView\n\n1. Instantiate the subclass (either in your code or in an xml file, for example by replacing `ImageView` by `com.applidium.shutterbug.FetchableImageView`).\n2. Fetch the image (`setImage(String url)` or `setImage(String url, Drawable placeholderDrawable)` if you need to add a placeholder while waiting for the image to be fetched)\n3. That's it!\n\nWe also provide you with a listener interface (`FetchableImageViewListener`) which will help you refresh your UI if need.\n\n### Using ShutterbugManager\n\nIf you need to do more advanced coding, you can use `ShutterbugManager`. It is a singleton class whose instance is accessed by the static method `ShutterbugManager.getSharedManager(context)`. Downloading and caching is done by calling `download(String url, ShutterbugManagerListener listener)` on this instance.\n"
  },
  {
    "path": "Shutterbug/.classpath",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<classpath>\n\t<classpathentry exported=\"true\" kind=\"con\" path=\"com.android.ide.eclipse.adt.ANDROID_FRAMEWORK\"/>\n\t<classpathentry exported=\"true\" kind=\"con\" path=\"com.android.ide.eclipse.adt.LIBRARIES\"/>\n\t<classpathentry kind=\"src\" path=\"src\"/>\n\t<classpathentry kind=\"src\" path=\"gen\"/>\n\t<classpathentry exported=\"true\" kind=\"con\" path=\"com.android.ide.eclipse.adt.DEPENDENCIES\"/>\n\t<classpathentry kind=\"output\" path=\"bin/classes\"/>\n</classpath>\n"
  },
  {
    "path": "Shutterbug/.project",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<projectDescription>\n\t<name>Shutterbug</name>\n\t<comment></comment>\n\t<projects>\n\t</projects>\n\t<buildSpec>\n\t\t<buildCommand>\n\t\t\t<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t\t<buildCommand>\n\t\t\t<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t\t<buildCommand>\n\t\t\t<name>org.eclipse.jdt.core.javabuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t\t<buildCommand>\n\t\t\t<name>com.android.ide.eclipse.adt.ApkBuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t</buildSpec>\n\t<natures>\n\t\t<nature>com.android.ide.eclipse.adt.AndroidNature</nature>\n\t\t<nature>org.eclipse.jdt.core.javanature</nature>\n\t</natures>\n</projectDescription>\n"
  },
  {
    "path": "Shutterbug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.applidium.shutterbug\"\n    android:versionCode=\"1\"\n    android:versionName=\"1.0\" >\n\n    <uses-sdk\n        android:minSdkVersion=\"7\"\n        android:targetSdkVersion=\"15\" />\n\n</manifest>"
  },
  {
    "path": "Shutterbug/proguard-project.txt",
    "content": "# To enable ProGuard in your project, edit project.properties\n# to define the proguard.config property as described in that file.\n#\n# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in ${sdk.dir}/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the ProGuard\n# include property in project.properties.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "Shutterbug/project.properties",
    "content": "# This file is automatically generated by Android Tools.\n# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n#\n# This file must be checked in Version Control Systems.\n#\n# To customize properties used by the Ant build system edit\n# \"ant.properties\", and override values to adapt the script to your\n# project structure.\n#\n# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):\n#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt\n\n# Project target.\ntarget=android-7\nandroid.library=true\n"
  },
  {
    "path": "Shutterbug/src/com/applidium/shutterbug/FetchableImageView.java",
    "content": "package com.applidium.shutterbug;\n\nimport android.R;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.util.AttributeSet;\nimport android.widget.ImageView;\n\nimport com.applidium.shutterbug.utils.ShutterbugManager;\nimport com.applidium.shutterbug.utils.ShutterbugManager.ShutterbugManagerListener;\n\npublic class FetchableImageView extends ImageView implements ShutterbugManagerListener {\n    public interface FetchableImageViewListener {\n        void onImageFetched(Bitmap bitmap, String url);\n\n        void onImageFailure(String url);\n    }\n\n    private FetchableImageViewListener mListener;\n\n    public FetchableImageView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public FetchableImageViewListener getListener() {\n        return mListener;\n    }\n\n    public void setListener(FetchableImageViewListener listener) {\n        mListener = listener;\n    }\n\n    public void setImage(String url) {\n        setImage(url, new ColorDrawable(getContext().getResources().getColor(R.color.transparent)));\n    }\n\n    public void setImage(String url, int desiredHeight, int desiredWidth) {\n        setImage(url, new ColorDrawable(getContext().getResources().getColor(R.color.transparent)), desiredHeight, desiredWidth);\n    }\n\n    public void setImage(String url, int placeholderDrawableId) {\n        setImage(url, getContext().getResources().getDrawable(placeholderDrawableId));\n    }\n\n    public void setImage(String url, Drawable placeholderDrawable) {\n        setImage(url, placeholderDrawable, -1, -1);\n    }\n\n    public void setImage(String url, Drawable placeholderDrawable, int desiredHeight, int desiredWidth) {\n        final ShutterbugManager manager = ShutterbugManager.getSharedImageManager(getContext());\n        manager.cancel((ShutterbugManagerListener) this);\n        setImageDrawable(placeholderDrawable);\n        if (url != null) {\n            manager.download(url, (ShutterbugManagerListener) this, desiredHeight, desiredWidth);\n        }\n    }\n\n    public void cancelCurrentImageLoad() {\n        ShutterbugManager.getSharedImageManager(getContext()).cancel((ShutterbugManagerListener) this);\n    }\n\n    @Override\n    public void onImageSuccess(ShutterbugManager imageManager, Bitmap bitmap, String url) {\n        setImageBitmap(bitmap);\n        requestLayout();\n        if (mListener != null) {\n            mListener.onImageFetched(bitmap, url);\n        }\n    }\n\n    @Override\n    public void onImageFailure(ShutterbugManager imageManager, String url) {\n        if (mListener != null) {\n            mListener.onImageFailure(url);\n        }\n    }\n\n}\n"
  },
  {
    "path": "Shutterbug/src/com/applidium/shutterbug/cache/DiskLruCache.java",
    "content": "/*\n * Copyright (C) 2011 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.applidium.shutterbug.cache;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedWriter;\nimport java.io.Closeable;\nimport java.io.EOFException;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.FileWriter;\nimport java.io.FilterOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.Reader;\nimport java.io.StringWriter;\nimport java.io.Writer;\nimport java.lang.reflect.Array;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * A cache that uses a bounded amount of space on a filesystem. Each cache\n * entry has a string key and a fixed number of values. Values are byte\n * sequences, accessible as streams or files. Each value must be between {@code\n * 0} and {@code Integer.MAX_VALUE} bytes in length.\n *\n * <p>The cache stores its data in a directory on the filesystem. This\n * directory must be exclusive to the cache; the cache may delete or overwrite\n * files from its directory. It is an error for multiple processes to use the\n * same cache directory at the same time.\n *\n * <p>This cache limits the number of bytes that it will store on the\n * filesystem. When the number of stored bytes exceeds the limit, the cache will\n * remove entries in the background until the limit is satisfied. The limit is\n * not strict: the cache may temporarily exceed it while waiting for files to be\n * deleted. The limit does not include filesystem overhead or the cache\n * journal so space-sensitive applications should set a conservative limit.\n *\n * <p>Clients call {@link #edit} to create or update the values of an entry. An\n * entry may have only one editor at one time; if a value is not available to be\n * edited then {@link #edit} will return null.\n * <ul>\n *     <li>When an entry is being <strong>created</strong> it is necessary to\n *         supply a full set of values; the empty value should be used as a\n *         placeholder if necessary.\n *     <li>When an entry is being <strong>edited</strong>, it is not necessary\n *         to supply data for every value; values default to their previous\n *         value.\n * </ul>\n * Every {@link #edit} call must be matched by a call to {@link Editor#commit}\n * or {@link Editor#abort}. Committing is atomic: a read observes the full set\n * of values as they were before or after the commit, but never a mix of values.\n *\n * <p>Clients call {@link #get} to read a snapshot of an entry. The read will\n * observe the value at the time that {@link #get} was called. Updates and\n * removals after the call do not impact ongoing reads.\n *\n * <p>This class is tolerant of some I/O errors. If files are missing from the\n * filesystem, the corresponding entries will be dropped from the cache. If\n * an error occurs while writing a cache value, the edit will fail silently.\n * Callers should handle other problems by catching {@code IOException} and\n * responding appropriately.\n */\npublic final class DiskLruCache implements Closeable {\n    static final String JOURNAL_FILE = \"journal\";\n    static final String JOURNAL_FILE_TMP = \"journal.tmp\";\n    static final String MAGIC = \"libcore.io.DiskLruCache\";\n    static final String VERSION_1 = \"1\";\n    static final long ANY_SEQUENCE_NUMBER = -1;\n    private static final String CLEAN = \"CLEAN\";\n    private static final String DIRTY = \"DIRTY\";\n    private static final String REMOVE = \"REMOVE\";\n    private static final String READ = \"READ\";\n\n    /* XXX From java.util.Arrays */\n    @SuppressWarnings(\"unchecked\")\n    private static <T> T[] copyOfRange(T[] original, int start, int end) {\n        int originalLength = original.length; // For exception priority compatibility.\n        if (start > end) {\n            throw new IllegalArgumentException();\n        }\n        if (start < 0 || start > originalLength) {\n            throw new ArrayIndexOutOfBoundsException();\n        }\n        int resultLength = end - start;\n        int copyLength = Math.min(resultLength, originalLength - start);\n        T[] result = (T[]) Array.newInstance(original.getClass().getComponentType(), resultLength);\n        System.arraycopy(original, start, result, 0, copyLength);\n        return result;\n    }\n\n    /* XXX From java.nio.charset.Charsets */\n    private static final Charset UTF_8 = Charset.forName(\"UTF-8\");\n\n    /* XXX From libcore.io.IoUtils */\n    private static void deleteContents(File dir) throws IOException {\n        File[] files = dir.listFiles();\n        if (files == null) {\n            throw new IllegalArgumentException(\"not a directory: \" + dir);\n        }\n        for (File file : files) {\n            if (file.isDirectory()) {\n                deleteContents(file);\n            }\n            if (!file.delete()) {\n//                throw new IOException(\"failed to delete file: \" + file);\n            }\n        }\n    }\n\n    /* XXX From libcore.io.IoUtils */\n    private static void closeQuietly(/*Auto*/Closeable closeable) {\n        if (closeable != null) {\n            try {\n                closeable.close();\n            } catch (RuntimeException rethrown) {\n                throw rethrown;\n            } catch (Exception ignored) {\n            }\n        }\n    }\n\n    /* XXX From libcore.io.Streams */\n    private static String readFully(Reader reader) throws IOException {\n        try {\n            StringWriter writer = new StringWriter();\n            char[] buffer = new char[1024];\n            int count;\n            while ((count = reader.read(buffer)) != -1) {\n                writer.write(buffer, 0, count);\n            }\n            return writer.toString();\n        } finally {\n            reader.close();\n        }\n    }\n\n    /* XXX From libcore.io.Streams */\n    private static String readAsciiLine(InputStream in) throws IOException {\n        // TODO: support UTF-8 here instead\n\n        StringBuilder result = new StringBuilder(80);\n        while (true) {\n            int c = in.read();\n            if (c == -1) {\n                throw new EOFException();\n            } else if (c == '\\n') {\n                break;\n            }\n\n            result.append((char) c);\n        }\n        int length = result.length();\n        if (length > 0 && result.charAt(length - 1) == '\\r') {\n            result.setLength(length - 1);\n        }\n        return result.toString();\n    }\n\n    /*\n     * This cache uses a journal file named \"journal\". A typical journal file\n     * looks like this:\n     *     libcore.io.DiskLruCache\n     *     1\n     *     100\n     *     2\n     *\n     *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054\n     *     DIRTY 335c4c6028171cfddfbaae1a9c313c52\n     *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342\n     *     REMOVE 335c4c6028171cfddfbaae1a9c313c52\n     *     DIRTY 1ab96a171faeeee38496d8b330771a7a\n     *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234\n     *     READ 335c4c6028171cfddfbaae1a9c313c52\n     *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6\n     *\n     * The first five lines of the journal form its header. They are the\n     * constant string \"libcore.io.DiskLruCache\", the disk cache's version,\n     * the application's version, the value count, and a blank line.\n     *\n     * Each of the subsequent lines in the file is a record of the state of a\n     * cache entry. Each line contains space-separated values: a state, a key,\n     * and optional state-specific values.\n     *   o DIRTY lines track that an entry is actively being created or updated.\n     *     Every successful DIRTY action should be followed by a CLEAN or REMOVE\n     *     action. DIRTY lines without a matching CLEAN or REMOVE indicate that\n     *     temporary files may need to be deleted.\n     *   o CLEAN lines track a cache entry that has been successfully published\n     *     and may be read. A publish line is followed by the lengths of each of\n     *     its values.\n     *   o READ lines track accesses for LRU.\n     *   o REMOVE lines track entries that have been deleted.\n     *\n     * The journal file is appended to as cache operations occur. The journal may\n     * occasionally be compacted by dropping redundant lines. A temporary file named\n     * \"journal.tmp\" will be used during compaction; that file should be deleted if\n     * it exists when the cache is opened.\n     */\n\n    private final File directory;\n    private final File journalFile;\n    private final File journalFileTmp;\n    private final int appVersion;\n    private final long maxSize;\n    private final int valueCount;\n    private long size = 0;\n    private Writer journalWriter;\n    private final LinkedHashMap<String, Entry> lruEntries\n            = new LinkedHashMap<String, Entry>(0, 0.75f, true);\n    private int redundantOpCount;\n\n    /**\n     * To differentiate between old and current snapshots, each entry is given\n     * a sequence number each time an edit is committed. A snapshot is stale if\n     * its sequence number is not equal to its entry's sequence number.\n     */\n    private long nextSequenceNumber = 0;\n\n    /** This cache uses a single background thread to evict entries. */\n    private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,\n            60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());\n    private final Callable<Void> cleanupCallable = new Callable<Void>() {\n        @Override public Void call() throws Exception {\n            synchronized (DiskLruCache.this) {\n                if (journalWriter == null) {\n                    return null; // closed\n                }\n                trimToSize();\n                if (journalRebuildRequired()) {\n                    rebuildJournal();\n                    redundantOpCount = 0;\n                }\n            }\n            return null;\n        }\n    };\n\n    private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {\n        this.directory = directory;\n        this.appVersion = appVersion;\n        this.journalFile = new File(directory, JOURNAL_FILE);\n        this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);\n        this.valueCount = valueCount;\n        this.maxSize = maxSize;\n    }\n\n    /**\n     * Opens the cache in {@code directory}, creating a cache if none exists\n     * there.\n     *\n     * @param directory a writable directory\n     * @param appVersion\n     * @param valueCount the number of values per cache entry. Must be positive.\n     * @param maxSize the maximum number of bytes this cache should use to store\n     * @throws IOException if reading or writing the cache directory fails\n     */\n    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)\n            throws IOException {\n        if (maxSize <= 0) {\n            throw new IllegalArgumentException(\"maxSize <= 0\");\n        }\n        if (valueCount <= 0) {\n            throw new IllegalArgumentException(\"valueCount <= 0\");\n        }\n\n        // prefer to pick up where we left off\n        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);\n        if (cache.journalFile.exists()) {\n            try {\n                cache.readJournal();\n                cache.processJournal();\n                cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true));\n                return cache;\n            } catch (IOException journalIsCorrupt) {\n                System.out.println(\"DiskLruCache \" + directory + \" is corrupt: \"\n                        + journalIsCorrupt.getMessage() + \", removing\");\n                cache.delete();\n            }\n        }\n\n        // create a new empty cache\n        directory.mkdirs();\n        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);\n        cache.rebuildJournal();\n        return cache;\n    }\n\n    private void readJournal() throws IOException {\n        InputStream in = new BufferedInputStream(new FileInputStream(journalFile));\n        try {\n            String magic = /*Streams.*/readAsciiLine(in);\n            String version = /*Streams.*/readAsciiLine(in);\n            String appVersionString = /*Streams.*/readAsciiLine(in);\n            String valueCountString = /*Streams.*/readAsciiLine(in);\n            String blank = /*Streams.*/readAsciiLine(in);\n            if (!MAGIC.equals(magic)\n                    || !VERSION_1.equals(version)\n                    || !Integer.toString(appVersion).equals(appVersionString)\n                    || !Integer.toString(valueCount).equals(valueCountString)\n                    || !\"\".equals(blank)) {\n                throw new IOException(\"unexpected journal header: [\"\n                        + magic + \", \" + version + \", \" + valueCountString + \", \" + blank + \"]\");\n            }\n\n            while (true) {\n                try {\n                    readJournalLine(/*Streams.*/readAsciiLine(in));\n                } catch (EOFException endOfJournal) {\n                    break;\n                }\n            }\n        } finally {\n            /*IoUtils.*/closeQuietly(in);\n        }\n    }\n\n    private void readJournalLine(String line) throws IOException {\n        String[] parts = line.split(\" \");\n        if (parts.length < 2) {\n            throw new IOException(\"unexpected journal line: \" + line);\n        }\n\n        String key = parts[1];\n        if (parts[0].equals(REMOVE) && parts.length == 2) {\n            lruEntries.remove(key);\n            return;\n        }\n\n        Entry entry = lruEntries.get(key);\n        if (entry == null) {\n            entry = new Entry(key);\n            lruEntries.put(key, entry);\n        }\n\n        if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {\n            entry.readable = true;\n            entry.currentEditor = null;\n            entry.setLengths(/*Arrays.*/copyOfRange(parts, 2, parts.length));\n        } else if (parts[0].equals(DIRTY) && parts.length == 2) {\n            entry.currentEditor = new Editor(entry);\n        } else if (parts[0].equals(READ) && parts.length == 2) {\n            // this work was already done by calling lruEntries.get()\n        } else {\n            throw new IOException(\"unexpected journal line: \" + line);\n        }\n    }\n\n    /**\n     * Computes the initial size and collects garbage as a part of opening the\n     * cache. Dirty entries are assumed to be inconsistent and will be deleted.\n     */\n    private void processJournal() throws IOException {\n        deleteIfExists(journalFileTmp);\n        for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {\n            Entry entry = i.next();\n            if (entry.currentEditor == null) {\n                for (int t = 0; t < valueCount; t++) {\n                    size += entry.lengths[t];\n                }\n            } else {\n                entry.currentEditor = null;\n                for (int t = 0; t < valueCount; t++) {\n                    deleteIfExists(entry.getCleanFile(t));\n                    deleteIfExists(entry.getDirtyFile(t));\n                }\n                i.remove();\n            }\n        }\n    }\n\n    /**\n     * Creates a new journal that omits redundant information. This replaces the\n     * current journal if it exists.\n     */\n    private synchronized void rebuildJournal() throws IOException {\n        if (journalWriter != null) {\n            journalWriter.close();\n        }\n\n        Writer writer = new BufferedWriter(new FileWriter(journalFileTmp));\n        writer.write(MAGIC);\n        writer.write(\"\\n\");\n        writer.write(VERSION_1);\n        writer.write(\"\\n\");\n        writer.write(Integer.toString(appVersion));\n        writer.write(\"\\n\");\n        writer.write(Integer.toString(valueCount));\n        writer.write(\"\\n\");\n        writer.write(\"\\n\");\n\n        for (Entry entry : lruEntries.values()) {\n            if (entry.currentEditor != null) {\n                writer.write(DIRTY + ' ' + entry.key + '\\n');\n            } else {\n                writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\\n');\n            }\n        }\n\n        writer.close();\n        journalFileTmp.renameTo(journalFile);\n        journalWriter = new BufferedWriter(new FileWriter(journalFile, true));\n    }\n\n    private static void deleteIfExists(File file) throws IOException {\n        /*try {\n            Libcore.os.remove(file.getPath());\n        } catch (ErrnoException errnoException) {\n            if (errnoException.errno != OsConstants.ENOENT) {\n                throw errnoException.rethrowAsIOException();\n            }\n        }*/\n        if (file.exists() && !file.delete()) {\n            throw new IOException();\n        }\n    }\n\n    /**\n     * Returns a snapshot of the entry named {@code key}, or null if it doesn't\n     * exist is not currently readable. If a value is returned, it is moved to\n     * the head of the LRU queue.\n     */\n    public synchronized Snapshot get(String key) throws IOException {\n        checkNotClosed();\n        validateKey(key);\n        Entry entry = lruEntries.get(key);\n        if (entry == null) {\n            return null;\n        }\n\n        if (!entry.readable) {\n            return null;\n        }\n\n        /*\n         * Open all streams eagerly to guarantee that we see a single published\n         * snapshot. If we opened streams lazily then the streams could come\n         * from different edits.\n         */\n        InputStream[] ins = new InputStream[valueCount];\n        try {\n            for (int i = 0; i < valueCount; i++) {\n                ins[i] = new FileInputStream(entry.getCleanFile(i));\n            }\n        } catch (FileNotFoundException e) {\n            // a file must have been deleted manually!\n            return null;\n        }\n\n        redundantOpCount++;\n        journalWriter.append(READ + ' ' + key + '\\n');\n        if (journalRebuildRequired()) {\n            executorService.submit(cleanupCallable);\n        }\n\n        return new Snapshot(key, entry.sequenceNumber, ins);\n    }\n\n    /**\n     * Returns an editor for the entry named {@code key}, or null if another\n     * edit is in progress.\n     */\n    public Editor edit(String key) throws IOException {\n        return edit(key, ANY_SEQUENCE_NUMBER);\n    }\n\n    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {\n        checkNotClosed();\n        validateKey(key);\n        Entry entry = lruEntries.get(key);\n        if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER\n                && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {\n            return null; // snapshot is stale\n        }\n        if (entry == null) {\n            entry = new Entry(key);\n            lruEntries.put(key, entry);\n        } else if (entry.currentEditor != null) {\n            return null; // another edit is in progress\n        }\n\n        Editor editor = new Editor(entry);\n        entry.currentEditor = editor;\n\n        // flush the journal before creating files to prevent file leaks\n        journalWriter.write(DIRTY + ' ' + key + '\\n');\n        journalWriter.flush();\n        return editor;\n    }\n\n    /**\n     * Returns the directory where this cache stores its data.\n     */\n    public File getDirectory() {\n        return directory;\n    }\n\n    /**\n     * Returns the maximum number of bytes that this cache should use to store\n     * its data.\n     */\n    public long maxSize() {\n        return maxSize;\n    }\n\n    /**\n     * Returns the number of bytes currently being used to store the values in\n     * this cache. This may be greater than the max size if a background\n     * deletion is pending.\n     */\n    public synchronized long size() {\n        return size;\n    }\n\n    private synchronized void completeEdit(Editor editor, boolean success) throws IOException {\n        Entry entry = editor.entry;\n        if (entry.currentEditor != editor) {\n            throw new IllegalStateException();\n        }\n\n        // if this edit is creating the entry for the first time, every index must have a value\n        if (success && !entry.readable) {\n            for (int i = 0; i < valueCount; i++) {\n                if (!entry.getDirtyFile(i).exists()) {\n                    editor.abort();\n                    throw new IllegalStateException(\"edit didn't create file \" + i);\n                }\n            }\n        }\n\n        for (int i = 0; i < valueCount; i++) {\n            File dirty = entry.getDirtyFile(i);\n            if (success) {\n                if (dirty.exists()) {\n                    File clean = entry.getCleanFile(i);\n                    dirty.renameTo(clean);\n                    long oldLength = entry.lengths[i];\n                    long newLength = clean.length();\n                    entry.lengths[i] = newLength;\n                    size = size - oldLength + newLength;\n                }\n            } else {\n                deleteIfExists(dirty);\n            }\n        }\n\n        redundantOpCount++;\n        entry.currentEditor = null;\n        if (entry.readable | success) {\n            entry.readable = true;\n            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\\n');\n            if (success) {\n                entry.sequenceNumber = nextSequenceNumber++;\n            }\n        } else {\n            lruEntries.remove(entry.key);\n            journalWriter.write(REMOVE + ' ' + entry.key + '\\n');\n        }\n\n        if (size > maxSize || journalRebuildRequired()) {\n            executorService.submit(cleanupCallable);\n        }\n    }\n\n    /**\n     * We only rebuild the journal when it will halve the size of the journal\n     * and eliminate at least 2000 ops.\n     */\n    private boolean journalRebuildRequired() {\n        final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;\n        return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD\n                && redundantOpCount >= lruEntries.size();\n    }\n\n    /**\n     * Drops the entry for {@code key} if it exists and can be removed. Entries\n     * actively being edited cannot be removed.\n     *\n     * @return true if an entry was removed.\n     */\n    public synchronized boolean remove(String key) throws IOException {\n        checkNotClosed();\n        validateKey(key);\n        Entry entry = lruEntries.get(key);\n        if (entry == null || entry.currentEditor != null) {\n            return false;\n        }\n\n        for (int i = 0; i < valueCount; i++) {\n            File file = entry.getCleanFile(i);\n            if (!file.delete()) {\n                throw new IOException(\"failed to delete \" + file);\n            }\n            size -= entry.lengths[i];\n            entry.lengths[i] = 0;\n        }\n\n        redundantOpCount++;\n        journalWriter.append(REMOVE + ' ' + key + '\\n');\n        lruEntries.remove(key);\n\n        if (journalRebuildRequired()) {\n            executorService.submit(cleanupCallable);\n        }\n\n        return true;\n    }\n\n    /**\n     * Returns true if this cache has been closed.\n     */\n    public boolean isClosed() {\n        return journalWriter == null;\n    }\n\n    private void checkNotClosed() {\n        if (journalWriter == null) {\n            throw new IllegalStateException(\"cache is closed\");\n        }\n    }\n\n    /**\n     * Force buffered operations to the filesystem.\n     */\n    public synchronized void flush() throws IOException {\n        checkNotClosed();\n        trimToSize();\n        journalWriter.flush();\n    }\n\n    /**\n     * Closes this cache. Stored values will remain on the filesystem.\n     */\n    public synchronized void close() throws IOException {\n        if (journalWriter == null) {\n            return; // already closed\n        }\n        for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {\n            if (entry.currentEditor != null) {\n                entry.currentEditor.abort();\n            }\n        }\n        trimToSize();\n        journalWriter.close();\n        journalWriter = null;\n    }\n\n    private void trimToSize() throws IOException {\n        while (size > maxSize) {\n            Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();//lruEntries.eldest();\n            remove(toEvict.getKey());\n        }\n    }\n\n    /**\n     * Closes the cache and deletes all of its stored values. This will delete\n     * all files in the cache directory including files that weren't created by\n     * the cache.\n     */\n    public void delete() throws IOException {\n        close();\n        /*IoUtils.*/deleteContents(directory);\n    }\n\n    private void validateKey(String key) {\n        if (key.contains(\" \") || key.contains(\"\\n\") || key.contains(\"\\r\")) {\n            throw new IllegalArgumentException(\n                    \"keys must not contain spaces or newlines: \\\"\" + key + \"\\\"\");\n        }\n    }\n\n    private static String inputStreamToString(InputStream in) throws IOException {\n        return /*Streams.*/readFully(new InputStreamReader(in, /*Charsets.*/UTF_8));\n    }\n\n    /**\n     * A snapshot of the values for an entry.\n     */\n    public final class Snapshot implements Closeable {\n        private final String key;\n        private final long sequenceNumber;\n        private final InputStream[] ins;\n\n        private Snapshot(String key, long sequenceNumber, InputStream[] ins) {\n            this.key = key;\n            this.sequenceNumber = sequenceNumber;\n            this.ins = ins;\n        }\n\n        /**\n         * Returns an editor for this snapshot's entry, or null if either the\n         * entry has changed since this snapshot was created or if another edit\n         * is in progress.\n         */\n        public Editor edit() throws IOException {\n            return DiskLruCache.this.edit(key, sequenceNumber);\n        }\n\n        /**\n         * Returns the unbuffered stream with the value for {@code index}.\n         */\n        public InputStream getInputStream(int index) {\n            return ins[index];\n        }\n\n        /**\n         * Returns the string value for {@code index}.\n         */\n        public String getString(int index) throws IOException {\n            return inputStreamToString(getInputStream(index));\n        }\n\n        @Override public void close() {\n            for (InputStream in : ins) {\n                /*IoUtils.*/closeQuietly(in);\n            }\n        }\n    }\n\n    /**\n     * Edits the values for an entry.\n     */\n    public final class Editor {\n        private final Entry entry;\n        private boolean hasErrors;\n\n        private Editor(Entry entry) {\n            this.entry = entry;\n        }\n\n        /**\n         * Returns an unbuffered input stream to read the last committed value,\n         * or null if no value has been committed.\n         */\n        public InputStream newInputStream(int index) throws IOException {\n            synchronized (DiskLruCache.this) {\n                if (entry.currentEditor != this) {\n                    throw new IllegalStateException();\n                }\n                if (!entry.readable) {\n                    return null;\n                }\n                return new FileInputStream(entry.getCleanFile(index));\n            }\n        }\n\n        /**\n         * Returns the last committed value as a string, or null if no value\n         * has been committed.\n         */\n        public String getString(int index) throws IOException {\n            InputStream in = newInputStream(index);\n            return in != null ? inputStreamToString(in) : null;\n        }\n\n        /**\n         * Returns a new unbuffered output stream to write the value at\n         * {@code index}. If the underlying output stream encounters errors\n         * when writing to the filesystem, this edit will be aborted when\n         * {@link #commit} is called. The returned output stream does not throw\n         * IOExceptions.\n         */\n        public OutputStream newOutputStream(int index) throws IOException {\n            synchronized (DiskLruCache.this) {\n                if (entry.currentEditor != this) {\n                    throw new IllegalStateException();\n                }\n                return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));\n            }\n        }\n\n        /**\n         * Sets the value at {@code index} to {@code value}.\n         */\n        public void set(int index, String value) throws IOException {\n            Writer writer = null;\n            try {\n                writer = new OutputStreamWriter(newOutputStream(index), /*Charsets.*/UTF_8);\n                writer.write(value);\n            } finally {\n                /*IoUtils.*/closeQuietly(writer);\n            }\n        }\n\n        /**\n         * Commits this edit so it is visible to readers.  This releases the\n         * edit lock so another edit may be started on the same key.\n         */\n        public void commit() throws IOException {\n            if (hasErrors) {\n                completeEdit(this, false);\n                remove(entry.key); // the previous entry is stale\n            } else {\n                completeEdit(this, true);\n            }\n        }\n\n        /**\n         * Aborts this edit. This releases the edit lock so another edit may be\n         * started on the same key.\n         */\n        public void abort() throws IOException {\n            completeEdit(this, false);\n        }\n\n        private class FaultHidingOutputStream extends FilterOutputStream {\n            private FaultHidingOutputStream(OutputStream out) {\n                super(out);\n            }\n\n            @Override public void write(int oneByte) {\n                try {\n                    out.write(oneByte);\n                } catch (IOException e) {\n                    hasErrors = true;\n                }\n            }\n\n            @Override public void write(byte[] buffer, int offset, int length) {\n                try {\n                    out.write(buffer, offset, length);\n                } catch (IOException e) {\n                    hasErrors = true;\n                }\n            }\n\n            @Override public void close() {\n                try {\n                    out.close();\n                } catch (IOException e) {\n                    hasErrors = true;\n                }\n            }\n\n            @Override public void flush() {\n                try {\n                    out.flush();\n                } catch (IOException e) {\n                    hasErrors = true;\n                }\n            }\n        }\n    }\n\n    private final class Entry {\n        private final String key;\n\n        /** Lengths of this entry's files. */\n        private final long[] lengths;\n\n        /** True if this entry has ever been published */\n        private boolean readable;\n\n        /** The ongoing edit or null if this entry is not being edited. */\n        private Editor currentEditor;\n\n        /** The sequence number of the most recently committed edit to this entry. */\n        private long sequenceNumber;\n\n        private Entry(String key) {\n            this.key = key;\n            this.lengths = new long[valueCount];\n        }\n\n        public String getLengths() throws IOException {\n            StringBuilder result = new StringBuilder();\n            for (long size : lengths) {\n                result.append(' ').append(size);\n            }\n            return result.toString();\n        }\n\n        /**\n         * Set lengths using decimal numbers like \"10123\".\n         */\n        private void setLengths(String[] strings) throws IOException {\n            if (strings.length != valueCount) {\n                throw invalidLengths(strings);\n            }\n\n            try {\n                for (int i = 0; i < strings.length; i++) {\n                    lengths[i] = Long.parseLong(strings[i]);\n                }\n            } catch (NumberFormatException e) {\n                throw invalidLengths(strings);\n            }\n        }\n\n        private IOException invalidLengths(String[] strings) throws IOException {\n            throw new IOException(\"unexpected journal line: \" + Arrays.toString(strings));\n        }\n\n        public File getCleanFile(int i) {\n            return new File(directory, key + \".\" + i);\n        }\n\n        public File getDirtyFile(int i) {\n            return new File(directory, key + \".\" + i + \".tmp\");\n        }\n    }\n}\n"
  },
  {
    "path": "Shutterbug/src/com/applidium/shutterbug/cache/ImageCache.java",
    "content": "package com.applidium.shutterbug.cache;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\nimport android.app.ActivityManager;\nimport android.content.Context;\nimport android.content.pm.PackageManager.NameNotFoundException;\nimport android.graphics.Bitmap;\nimport android.os.AsyncTask;\n\nimport com.applidium.shutterbug.cache.DiskLruCache.Editor;\nimport com.applidium.shutterbug.cache.DiskLruCache.Snapshot;\nimport com.applidium.shutterbug.utils.BitmapFactoryScale;\nimport com.applidium.shutterbug.utils.BitmapFactoryScale.InputStreamGenerator;\nimport com.applidium.shutterbug.utils.DownloadRequest;\n\npublic class ImageCache {\n    public interface ImageCacheListener {\n        void onImageFound(ImageCache imageCache, Bitmap bitmap, String key, DownloadRequest downloadRequest);\n\n        void onImageNotFound(ImageCache imageCache, String key, DownloadRequest downloadRequest);\n    }\n\n    // 1 entry per key\n    private final static int         DISK_CACHE_VALUE_COUNT = 1;\n    // 100 MB of disk cache\n    private final static int         DISK_CACHE_MAX_SIZE    = 100 * 1024 * 1024;\n\n    private static ImageCache        sImageCache;\n    private Context                  mContext;\n    private LruCache<String, Bitmap> mMemoryCache;\n    private DiskLruCache             mDiskCache;\n\n    ImageCache(Context context) {\n        mContext = context;\n        // Get memory class of this device, exceeding this amount will throw an\n        // OutOfMemory exception.\n        final int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();\n\n        // Use 1/8th of the available memory for this memory cache.\n        final int cacheSize = 1024 * 1024 * memClass / 8;\n\n        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {\n            @Override\n            protected int sizeOf(String key, Bitmap bitmap) {\n                // The cache size will be measured in bytes rather than number\n                // of items.\n                return bitmap.getRowBytes() * bitmap.getHeight();\n            }\n        };\n\n        openDiskCache();\n    }\n\n    public static ImageCache getSharedImageCache(Context context) {\n        if (sImageCache == null) {\n            sImageCache = new ImageCache(context);\n        }\n        return sImageCache;\n    }\n\n    public void queryCache(String cacheKey, ImageCacheListener listener, DownloadRequest downloadRequest) {\n        if (cacheKey == null) {\n            listener.onImageNotFound(this, cacheKey, downloadRequest);\n            return;\n        }\n\n        // First check the in-memory cache...\n        Bitmap cachedBitmap = mMemoryCache.get(cacheKey);\n\n        if (cachedBitmap != null) {\n            // ...notify listener immediately, no need to go async\n            listener.onImageFound(this, cachedBitmap, cacheKey, downloadRequest);\n            return;\n        }\n\n        if (mDiskCache != null) {\n            new BitmapDecoderTask(cacheKey, listener, downloadRequest).execute();\n            return;\n        }\n        listener.onImageNotFound(this, cacheKey, downloadRequest);\n    }\n\n    public Snapshot storeToDisk(InputStream inputStream, String cacheKey) {\n        try {\n            Editor editor = mDiskCache.edit(cacheKey);\n            final OutputStream outputStream = editor.newOutputStream(0);\n            final int bufferSize = 1024;\n            byte[] bytes = new byte[bufferSize];\n            for (;;) {\n                int count = inputStream.read(bytes, 0, bufferSize);\n                if (count == -1)\n                    break;\n                outputStream.write(bytes, 0, count);\n            }\n            outputStream.close();\n            editor.commit();\n            return mDiskCache.get(cacheKey);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    public Snapshot queryDiskCache(String cacheKey) {\n        try {\n            return mDiskCache.get(cacheKey);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    public void storeToMemory(Bitmap bitmap, String cacheKey) {\n        mMemoryCache.put(cacheKey, bitmap);\n    }\n\n    public void clear() {\n        try {\n            mDiskCache.delete();\n            openDiskCache();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        mMemoryCache.evictAll();\n    }\n\n    private class BitmapDecoderTask extends AsyncTask<Void, Void, Bitmap> {\n        private String             mCacheKey;\n        private ImageCacheListener mListener;\n        private DownloadRequest    mDownloadRequest;\n\n        public BitmapDecoderTask(String cacheKey, ImageCacheListener listener, DownloadRequest downloadRequest) {\n            mCacheKey = cacheKey;\n            mListener = listener;\n            mDownloadRequest = downloadRequest;\n        }\n\n        @Override\n        protected Bitmap doInBackground(Void... params) {\n            try {\n                Snapshot snapshot = mDiskCache.get(mCacheKey);\n                if (snapshot != null) {\n                    return BitmapFactoryScale.decodeSampledBitmapFromStream(new InputStreamGenerator() {\n\n                        @Override\n                        public InputStream getStream() {\n                            try {\n                                return mDiskCache.get(mCacheKey).getInputStream(0);\n                            } catch (IOException e) {\n                                e.printStackTrace();\n                                return null;\n                            }\n                        }\n                    }, mDownloadRequest);\n                } else {\n                    return null;\n                }\n            } catch (IOException e) {\n                e.printStackTrace();\n                return null;\n            }\n        }\n\n        @Override\n        protected void onPostExecute(Bitmap result) {\n            if (result != null) {\n                storeToMemory(result, mCacheKey);\n                mListener.onImageFound(ImageCache.this, result, mCacheKey, mDownloadRequest);\n            } else {\n                mListener.onImageNotFound(ImageCache.this, mCacheKey, mDownloadRequest);\n            }\n        }\n\n    }\n\n    private void openDiskCache() {\n        File directory;\n        if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED)) {\n            directory = new File(android.os.Environment.getExternalStorageDirectory(), \"Applidium Image Cache\");\n        } else {\n            directory = mContext.getCacheDir();\n        }\n        int versionCode;\n        try {\n            versionCode = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).versionCode;\n        } catch (NameNotFoundException e) {\n            versionCode = 0;\n            e.printStackTrace();\n        }\n        try {\n            mDiskCache = DiskLruCache.open(directory, versionCode, DISK_CACHE_VALUE_COUNT, DISK_CACHE_MAX_SIZE);\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n}\n"
  },
  {
    "path": "Shutterbug/src/com/applidium/shutterbug/cache/LruCache.java",
    "content": "package com.applidium.shutterbug.cache;\n\n/*\n * Copyright (C) 2011 The Android Open Source Project Licensed under the Apache\n * License, Version 2.0 (the \"License\"); you may not use this file except in\n * compliance with the License. You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law\n * or agreed to in writing, software distributed under the License is\n * distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\n/**\n * Static library version of {@code android.util.LruCache}. Used to write apps\n * that run on API levels prior to 12. When running on API level 12 or above,\n * this implementation is still used; it does not try to switch to the\n * framework's implementation. See the framework SDK documentation for a class\n * overview.\n */\npublic class LruCache<K, V> {\n    private final LinkedHashMap<K, V> map;\n\n    /** Size of this cache in units. Not necessarily the number of elements. */\n    private int                       size;\n    private int                       maxSize;\n\n    private int                       putCount;\n    private int                       createCount;\n    private int                       evictionCount;\n    private int                       hitCount;\n    private int                       missCount;\n\n    /**\n     * @param maxSize\n     *            for caches that do not override {@link #sizeOf}, this is the\n     *            maximum number of entries in the cache. For all other caches,\n     *            this is the maximum sum of the sizes of the entries in this\n     *            cache.\n     */\n    public LruCache(int maxSize) {\n        if (maxSize <= 0) {\n            throw new IllegalArgumentException(\"maxSize <= 0\");\n        }\n        this.maxSize = maxSize;\n        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);\n    }\n\n    /**\n     * Returns the value for {@code key} if it exists in the cache or can be\n     * created by {@code #create}. If a value was returned, it is moved to the\n     * head of the queue. This returns null if a value is not cached and cannot\n     * be created.\n     */\n    public final V get(K key) {\n        if (key == null) {\n            throw new NullPointerException(\"key == null\");\n        }\n\n        V mapValue;\n        synchronized (this) {\n            mapValue = map.get(key);\n            if (mapValue != null) {\n                hitCount++;\n                return mapValue;\n            }\n            missCount++;\n        }\n\n        /*\n         * Attempt to create a value. This may take a long time, and the map may\n         * be different when create() returns. If a conflicting value was added\n         * to the map while create() was working, we leave that value in the map\n         * and release the created value.\n         */\n\n        V createdValue = create(key);\n        if (createdValue == null) {\n            return null;\n        }\n\n        synchronized (this) {\n            createCount++;\n            mapValue = map.put(key, createdValue);\n\n            if (mapValue != null) {\n                // There was a conflict so undo that last put\n                map.put(key, mapValue);\n            } else {\n                size += safeSizeOf(key, createdValue);\n            }\n        }\n\n        if (mapValue != null) {\n            entryRemoved(false, key, createdValue, mapValue);\n            return mapValue;\n        } else {\n            trimToSize(maxSize);\n            return createdValue;\n        }\n    }\n\n    /**\n     * Caches {@code value} for {@code key}. The value is moved to the head of\n     * the queue.\n     * \n     * @return the previous value mapped by {@code key}.\n     */\n    public final V put(K key, V value) {\n        if (key == null || value == null) {\n            throw new NullPointerException(\"key == null || value == null\");\n        }\n\n        V previous;\n        synchronized (this) {\n            putCount++;\n            size += safeSizeOf(key, value);\n            previous = map.put(key, value);\n            if (previous != null) {\n                size -= safeSizeOf(key, previous);\n            }\n        }\n\n        if (previous != null) {\n            entryRemoved(false, key, previous, value);\n        }\n\n        trimToSize(maxSize);\n        return previous;\n    }\n\n    /**\n     * @param maxSize\n     *            the maximum size of the cache before returning. May be -1 to\n     *            evict even 0-sized elements.\n     */\n    private void trimToSize(int maxSize) {\n        while (true) {\n            K key;\n            V value;\n            synchronized (this) {\n                if (size < 0 || (map.isEmpty() && size != 0)) {\n                    throw new IllegalStateException(getClass().getName() + \".sizeOf() is reporting inconsistent results!\");\n                }\n\n                if (size <= maxSize || map.isEmpty()) {\n                    break;\n                }\n\n                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();\n                key = toEvict.getKey();\n                value = toEvict.getValue();\n                map.remove(key);\n                size -= safeSizeOf(key, value);\n                evictionCount++;\n            }\n\n            entryRemoved(true, key, value, null);\n        }\n    }\n\n    /**\n     * Removes the entry for {@code key} if it exists.\n     * \n     * @return the previous value mapped by {@code key}.\n     */\n    public final V remove(K key) {\n        if (key == null) {\n            throw new NullPointerException(\"key == null\");\n        }\n\n        V previous;\n        synchronized (this) {\n            previous = map.remove(key);\n            if (previous != null) {\n                size -= safeSizeOf(key, previous);\n            }\n        }\n\n        if (previous != null) {\n            entryRemoved(false, key, previous, null);\n        }\n\n        return previous;\n    }\n\n    /**\n     * Called for entries that have been evicted or removed. This method is\n     * invoked when a value is evicted to make space, removed by a call to\n     * {@link #remove}, or replaced by a call to {@link #put}. The default\n     * implementation does nothing.\n     * <p>\n     * The method is called without synchronization: other threads may access\n     * the cache while this method is executing.\n     * \n     * @param evicted\n     *            true if the entry is being removed to make space, false if the\n     *            removal was caused by a {@link #put} or {@link #remove}.\n     * @param newValue\n     *            the new value for {@code key}, if it exists. If non-null, this\n     *            removal was caused by a {@link #put}. Otherwise it was caused\n     *            by an eviction or a {@link #remove}.\n     */\n    protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {\n    }\n\n    /**\n     * Called after a cache miss to compute a value for the corresponding key.\n     * Returns the computed value or null if no value can be computed. The\n     * default implementation returns null.\n     * <p>\n     * The method is called without synchronization: other threads may access\n     * the cache while this method is executing.\n     * <p>\n     * If a value for {@code key} exists in the cache when this method returns,\n     * the created value will be released with {@link #entryRemoved} and\n     * discarded. This can occur when multiple threads request the same key at\n     * the same time (causing multiple values to be created), or when one thread\n     * calls {@link #put} while another is creating a value for the same key.\n     */\n    protected V create(K key) {\n        return null;\n    }\n\n    private int safeSizeOf(K key, V value) {\n        int result = sizeOf(key, value);\n        if (result < 0) {\n            throw new IllegalStateException(\"Negative size: \" + key + \"=\" + value);\n        }\n        return result;\n    }\n\n    /**\n     * Returns the size of the entry for {@code key} and {@code value} in\n     * user-defined units. The default implementation returns 1 so that size is\n     * the number of entries and max size is the maximum number of entries.\n     * <p>\n     * An entry's size must not change while it is in the cache.\n     */\n    protected int sizeOf(K key, V value) {\n        return 1;\n    }\n\n    /**\n     * Clear the cache, calling {@link #entryRemoved} on each removed entry.\n     */\n    public final void evictAll() {\n        trimToSize(-1); // -1 will evict 0-sized elements\n    }\n\n    /**\n     * For caches that do not override {@link #sizeOf}, this returns the number\n     * of entries in the cache. For all other caches, this returns the sum of\n     * the sizes of the entries in this cache.\n     */\n    public synchronized final int size() {\n        return size;\n    }\n\n    /**\n     * For caches that do not override {@link #sizeOf}, this returns the maximum\n     * number of entries in the cache. For all other caches, this returns the\n     * maximum sum of the sizes of the entries in this cache.\n     */\n    public synchronized final int maxSize() {\n        return maxSize;\n    }\n\n    /**\n     * Returns the number of times {@link #get} returned a value.\n     */\n    public synchronized final int hitCount() {\n        return hitCount;\n    }\n\n    /**\n     * Returns the number of times {@link #get} returned null or required a new\n     * value to be created.\n     */\n    public synchronized final int missCount() {\n        return missCount;\n    }\n\n    /**\n     * Returns the number of times {@link #create(Object)} returned a value.\n     */\n    public synchronized final int createCount() {\n        return createCount;\n    }\n\n    /**\n     * Returns the number of times {@link #put} was called.\n     */\n    public synchronized final int putCount() {\n        return putCount;\n    }\n\n    /**\n     * Returns the number of values that have been evicted.\n     */\n    public synchronized final int evictionCount() {\n        return evictionCount;\n    }\n\n    /**\n     * Returns a copy of the current contents of the cache, ordered from least\n     * recently accessed to most recently accessed.\n     */\n    public synchronized final Map<K, V> snapshot() {\n        return new LinkedHashMap<K, V>(map);\n    }\n\n    @Override\n    public synchronized final String toString() {\n        int accesses = hitCount + missCount;\n        int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;\n        return String.format(\"LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]\", maxSize, hitCount, missCount, hitPercent);\n    }\n}"
  },
  {
    "path": "Shutterbug/src/com/applidium/shutterbug/downloader/ShutterbugDownloader.java",
    "content": "package com.applidium.shutterbug.downloader;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\n\nimport org.apache.http.client.ClientProtocolException;\nimport org.apache.http.client.methods.HttpGet;\n\nimport android.os.AsyncTask;\n\nimport com.applidium.shutterbug.utils.DownloadRequest;\n\npublic class ShutterbugDownloader {\n    public interface ShutterbugDownloaderListener {\n        void onImageDownloadSuccess(ShutterbugDownloader downloader, InputStream inputStream, DownloadRequest downloadRequest);\n\n        void onImageDownloadFailure(ShutterbugDownloader downloader, DownloadRequest downloadRequest);\n    }\n\n    private String                             mUrl;\n    private ShutterbugDownloaderListener       mListener;\n    private byte[]                             mImageData;\n    private DownloadRequest                    mDownloadRequest;\n    private final static int                   TIMEOUT = 30000;\n    private AsyncTask<Void, Void, InputStream> mCurrentTask;\n\n    public ShutterbugDownloader(String url, ShutterbugDownloaderListener listener, DownloadRequest downloadRequest) {\n        mUrl = url;\n        mListener = listener;\n        mDownloadRequest = downloadRequest;\n    }\n\n    public String getUrl() {\n        return mUrl;\n    }\n\n    public ShutterbugDownloaderListener getListener() {\n        return mListener;\n    }\n\n    public byte[] getImageData() {\n        return mImageData;\n    }\n\n    public DownloadRequest getDownloadRequest() {\n        return mDownloadRequest;\n    }\n\n    public void start() {\n        mCurrentTask = new AsyncTask<Void, Void, InputStream>() {\n\n            @Override\n            protected InputStream doInBackground(Void... params) {\n                HttpGet request = new HttpGet(mUrl);\n                request.setHeader(\"Content-Type\", \"application/x-www-form-urlencoded\");\n\n                try {\n                    URL imageUrl = new URL(mUrl);\n                    HttpURLConnection connection = (HttpURLConnection) imageUrl.openConnection();\n                    connection.setConnectTimeout(TIMEOUT);\n                    connection.setReadTimeout(TIMEOUT);\n                    connection.setInstanceFollowRedirects(true);\n                    InputStream inputStream = connection.getInputStream();\n                    return inputStream;\n                } catch (ClientProtocolException e) {\n                    e.printStackTrace();\n                } catch (IOException e) {\n                    e.printStackTrace();\n                }\n                return null;\n            }\n\n            @Override\n            protected void onPostExecute(InputStream inputStream) {\n                if (isCancelled()) {\n                    inputStream = null;\n                }\n\n                if (inputStream != null) {\n                    mListener.onImageDownloadSuccess(ShutterbugDownloader.this, inputStream, mDownloadRequest);\n                } else {\n                    mListener.onImageDownloadFailure(ShutterbugDownloader.this, mDownloadRequest);\n                }\n            }\n\n        }.execute();\n\n    }\n\n    public void cancel() {\n        if (mCurrentTask != null) {\n            mCurrentTask.cancel(true);\n        }\n    }\n}\n"
  },
  {
    "path": "Shutterbug/src/com/applidium/shutterbug/utils/BitmapFactoryScale.java",
    "content": "package com.applidium.shutterbug.utils;\n\nimport java.io.InputStream;\n\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\n\npublic class BitmapFactoryScale {\n    public interface InputStreamGenerator {\n        public InputStream getStream();\n    }\n\n    public static Bitmap decodeSampledBitmapFromStream(InputStreamGenerator generator, DownloadRequest request) {\n        if (generator == null || request == null) {\n            return null;\n        }\n        try {\n            BitmapFactory.Options options = new BitmapFactory.Options();\n            options.inJustDecodeBounds = true;\n            BitmapFactory.decodeStream(generator.getStream(), null, options);\n\n            options.inSampleSize = request.getSampleSize(options);\n            options.inJustDecodeBounds = false;\n            return BitmapFactory.decodeStream(generator.getStream(), null, options);\n        } catch (OutOfMemoryError e) {\n            e.printStackTrace();\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "Shutterbug/src/com/applidium/shutterbug/utils/DownloadRequest.java",
    "content": "package com.applidium.shutterbug.utils;\n\nimport android.graphics.BitmapFactory;\n\nimport com.applidium.shutterbug.utils.ShutterbugManager.ShutterbugManagerListener;\n\npublic class DownloadRequest {\n    private String                    mUrl;\n    private ShutterbugManagerListener mListener;\n\n    private int                       mDesiredHeight = -1;\n    private int                       mDesiredWidth  = -1;\n\n    public DownloadRequest(String url, ShutterbugManagerListener listener) {\n        mUrl = url;\n        mListener = listener;\n    }\n\n    public DownloadRequest(String url, ShutterbugManagerListener listener, int desiredHeight, int desiredWidth) {\n        mUrl = url;\n        mListener = listener;\n\n        mDesiredHeight = desiredHeight;\n        mDesiredWidth = desiredWidth;\n    }\n\n    public int getSampleSize(BitmapFactory.Options options) {\n        if (mDesiredHeight <= 0 || mDesiredWidth <= 0) {\n            return 1;\n        }\n\n        // Raw height and width of image\n        final int height = options.outHeight;\n        final int width = options.outWidth;\n        int inSampleSize = 1;\n\n        if (height > mDesiredHeight || width > mDesiredWidth) {\n\n            final int halfHeight = height / 2;\n            final int halfWidth = width / 2;\n\n            // Calculate the largest inSampleSize value that is a power of 2 and keeps both\n            // height and width larger than the requested height and width.\n            while ((halfHeight / inSampleSize) > mDesiredHeight && (halfWidth / inSampleSize) > mDesiredWidth) {\n                inSampleSize *= 2;\n            }\n        }\n\n        return inSampleSize;\n    }\n\n    public String getUrl() {\n        return mUrl;\n    }\n\n    public ShutterbugManagerListener getListener() {\n        return mListener;\n    }\n}\n"
  },
  {
    "path": "Shutterbug/src/com/applidium/shutterbug/utils/ShutterbugManager.java",
    "content": "package com.applidium.shutterbug.utils;\n\nimport java.io.InputStream;\nimport java.io.UnsupportedEncodingException;\nimport java.math.BigInteger;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Queue;\n\nimport android.R;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.drawable.ColorDrawable;\nimport android.os.AsyncTask;\nimport android.widget.ImageView;\n\nimport com.applidium.shutterbug.cache.DiskLruCache.Snapshot;\nimport com.applidium.shutterbug.cache.ImageCache;\nimport com.applidium.shutterbug.cache.ImageCache.ImageCacheListener;\nimport com.applidium.shutterbug.downloader.ShutterbugDownloader;\nimport com.applidium.shutterbug.downloader.ShutterbugDownloader.ShutterbugDownloaderListener;\nimport com.applidium.shutterbug.utils.BitmapFactoryScale.InputStreamGenerator;\n\npublic class ShutterbugManager implements ImageCacheListener, ShutterbugDownloaderListener {\n    public interface ShutterbugManagerListener {\n        void onImageSuccess(ShutterbugManager imageManager, Bitmap bitmap, String url);\n\n        void onImageFailure(ShutterbugManager imageManager, String url);\n    }\n\n    private static ShutterbugManager          sImageManager;\n\n    private Context                           mContext;\n    private List<String>                      mFailedUrls             = new ArrayList<String>();\n    private List<ShutterbugManagerListener>   mCacheListeners         = new ArrayList<ShutterbugManagerListener>();\n    private List<String>                      mCacheUrls              = new ArrayList<String>();\n    private Map<String, ShutterbugDownloader> mDownloadersMap         = new HashMap<String, ShutterbugDownloader>();\n    private List<DownloadRequest>             mDownloadRequests       = new ArrayList<DownloadRequest>();\n    private List<ShutterbugManagerListener>   mDownloadImageListeners = new ArrayList<ShutterbugManagerListener>();\n    private List<ShutterbugDownloader>        mDownloaders            = new ArrayList<ShutterbugDownloader>();\n\n    final static private int                  LISTENER_NOT_FOUND      = -1;\n\n    public ShutterbugManager(Context context) {\n        mContext = context;\n    }\n\n    public static ShutterbugManager getSharedImageManager(Context context) {\n        if (sImageManager == null) {\n            sImageManager = new ShutterbugManager(context);\n        }\n        return sImageManager;\n    }\n\n    public void download(String url, ShutterbugManagerListener listener) {\n        download(url, listener, -1, -1);\n    }\n\n    public void download(String url, ShutterbugManagerListener listener, int desiredHeight, int desiredWidth) {\n        if (url == null || listener == null || mFailedUrls.contains(url)) {\n            return;\n        }\n\n        mCacheListeners.add(listener);\n        mCacheUrls.add(url);\n        ImageCache.getSharedImageCache(mContext).queryCache(getCacheKey(url), this, new DownloadRequest(url, listener, desiredHeight, desiredWidth));\n    }\n\n    public void download(String url, final ImageView imageView) {\n        download(url, imageView, -1, -1);\n    }\n\n    public void download(String url, final ImageView imageView, int desiredHeight, int desiredWidth) {\n        imageView.setImageDrawable(new ColorDrawable(mContext.getResources().getColor(R.color.transparent)));\n        cancel(imageView);\n        download(url, new ImageManagerListener(imageView), desiredHeight, desiredWidth);\n    }\n\n    public static String getCacheKey(String url) {\n        try {\n            MessageDigest md = MessageDigest.getInstance(\"SHA-1\");\n            md.update(url.getBytes(\"UTF-8\"), 0, url.length());\n            return String.format(\"%x\", new BigInteger(md.digest()));\n        } catch (NoSuchAlgorithmException e) {\n            e.printStackTrace();\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    private int getListenerIndex(ShutterbugManagerListener listener, String url) {\n        for (int index = 0; index < mCacheListeners.size(); index++) {\n            if (mCacheListeners.get(index) == listener && mCacheUrls.get(index).equals(url)) {\n                return index;\n            }\n        }\n        return LISTENER_NOT_FOUND;\n    }\n\n    @Override\n    public void onImageFound(ImageCache imageCache, Bitmap bitmap, String key, DownloadRequest downloadRequest) {\n        final String url = downloadRequest.getUrl();\n        final ShutterbugManagerListener listener = downloadRequest.getListener();\n\n        int idx = getListenerIndex(listener, url);\n        if (idx == LISTENER_NOT_FOUND) {\n            // Request has since been canceled\n            return;\n        }\n\n        listener.onImageSuccess(this, bitmap, url);\n        mCacheListeners.remove(idx);\n        mCacheUrls.remove(idx);\n    }\n\n    @Override\n    public void onImageNotFound(ImageCache imageCache, String key, DownloadRequest downloadRequest) {\n        final String url = downloadRequest.getUrl();\n        final ShutterbugManagerListener listener = downloadRequest.getListener();\n\n        int idx = getListenerIndex(listener, url);\n        if (idx == LISTENER_NOT_FOUND) {\n            // Request has since been canceled\n            return;\n        }\n        mCacheListeners.remove(idx);\n        mCacheUrls.remove(idx);\n\n        // Share the same downloader for identical URLs so we don't download the\n        // same URL several times\n        ShutterbugDownloader downloader = mDownloadersMap.get(url);\n        if (downloader == null) {\n            downloader = new ShutterbugDownloader(url, this, downloadRequest);\n            downloader.start();\n            mDownloadersMap.put(url, downloader);\n        }\n        mDownloadRequests.add(downloadRequest);\n        mDownloadImageListeners.add(listener);\n        mDownloaders.add(downloader);\n    }\n\n    @Override\n    public void onImageDownloadSuccess(final ShutterbugDownloader downloader, final InputStream inputStream, final DownloadRequest downloadRequest) {\n        new InputStreamHandlingTask(downloader, downloadRequest).execute(inputStream);\n    }\n\n    @Override\n    public void onImageDownloadFailure(ShutterbugDownloader downloader, DownloadRequest downloadRequest) {\n        for (int idx = mDownloaders.size() - 1; idx >= 0; idx--) {\n            final int uidx = idx;\n            ShutterbugDownloader aDownloader = mDownloaders.get(uidx);\n            if (aDownloader == downloader) {\n                ShutterbugManagerListener listener = mDownloadImageListeners.get(uidx);\n                listener.onImageFailure(this, downloadRequest.getUrl());\n                mDownloaders.remove(uidx);\n                mDownloadImageListeners.remove(uidx);\n            }\n        }\n        mDownloadersMap.remove(downloadRequest.getUrl());\n\n    }\n\n    private class InputStreamHandlingTask extends AsyncTask<InputStream, Void, Bitmap> {\n        ShutterbugDownloader mDownloader;\n        DownloadRequest      mDownloadRequest;\n\n        InputStreamHandlingTask(ShutterbugDownloader downloader, DownloadRequest downloadRequest) {\n            mDownloader = downloader;\n            mDownloadRequest = downloadRequest;\n        }\n\n        @Override\n        protected Bitmap doInBackground(InputStream... params) {\n            final ImageCache sharedImageCache = ImageCache.getSharedImageCache(mContext);\n            final String cacheKey = getCacheKey(mDownloadRequest.getUrl());\n            // Store the image in the cache\n            Snapshot cachedSnapshot = sharedImageCache.storeToDisk(params[0], cacheKey);\n            Bitmap bitmap = null;\n            if (cachedSnapshot != null) {\n                bitmap = BitmapFactoryScale.decodeSampledBitmapFromStream(new InputStreamGenerator() {\n                    @Override\n                    public InputStream getStream() {\n                        return sharedImageCache.queryDiskCache(cacheKey).getInputStream(0);\n                    }\n                }, mDownloadRequest);\n                if (bitmap != null) {\n                    sharedImageCache.storeToMemory(bitmap, cacheKey);\n                }\n            }\n            return bitmap;\n        }\n\n        @Override\n        protected void onPostExecute(Bitmap bitmap) {\n            // Notify all the downloadListener with this downloader\n            for (int idx = mDownloaders.size() - 1; idx >= 0; idx--) {\n                final int uidx = idx;\n                ShutterbugDownloader aDownloader = mDownloaders.get(uidx);\n                if (aDownloader == mDownloader) {\n                    ShutterbugManagerListener listener = mDownloadImageListeners.get(uidx);\n                    if (bitmap != null) {\n                        listener.onImageSuccess(ShutterbugManager.this, bitmap, mDownloadRequest.getUrl());\n                    } else {\n                        listener.onImageFailure(ShutterbugManager.this, mDownloadRequest.getUrl());\n                    }\n                    mDownloaders.remove(uidx);\n                    mDownloadImageListeners.remove(uidx);\n                }\n            }\n            if (bitmap != null) {\n            } else { // TODO add retry option\n                mFailedUrls.add(mDownloadRequest.getUrl());\n            }\n            mDownloadersMap.remove(mDownloadRequest.getUrl());\n        }\n\n    }\n\n    public void cancel(ShutterbugManagerListener listener) {\n        int idx;\n        while ((idx = mCacheListeners.indexOf(listener)) != -1) {\n            mCacheListeners.remove(idx);\n            mCacheUrls.remove(idx);\n        }\n\n        while ((idx = mDownloadImageListeners.indexOf(listener)) != -1) {\n            ShutterbugDownloader downloader = mDownloaders.get(idx);\n\n            mDownloadRequests.remove(idx);\n            mDownloadImageListeners.remove(idx);\n            mDownloaders.remove(idx);\n\n            if (!mDownloaders.contains(downloader)) {\n                // No more listeners are waiting for this download, cancel it\n                downloader.cancel();\n                mDownloadersMap.remove(downloader.getUrl());\n            }\n        }\n    }\n\n    public void cancel(ImageView imageView) {\n        Queue<ShutterbugManagerListener> queue = new LinkedList<ShutterbugManagerListener>();\n        for (ShutterbugManagerListener listener : mCacheListeners) {\n            if (listener instanceof ImageManagerListener && ((ImageManagerListener) listener).mImageView.equals(imageView)) {\n                queue.add(listener);\n            }\n        }\n        for (ShutterbugManagerListener listener : mDownloadImageListeners) {\n            if (listener instanceof ImageManagerListener && ((ImageManagerListener) listener).mImageView.equals(imageView)) {\n                queue.add(listener);\n            }\n        }\n        for (ShutterbugManagerListener listener : queue) {\n            cancel(listener);\n        }\n    }\n\n    private static class ImageManagerListener implements ShutterbugManagerListener {\n        private ImageView mImageView;\n\n        public ImageManagerListener(ImageView imageView) {\n            mImageView = imageView;\n        }\n\n        @Override\n        public void onImageSuccess(ShutterbugManager imageManager, Bitmap bitmap, String url) {\n            mImageView.setImageBitmap(bitmap);\n        }\n\n        @Override\n        public void onImageFailure(ShutterbugManager imageManager, String url) {\n\n        }\n    }\n}\n"
  },
  {
    "path": "ShutterbugDemo/.classpath",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<classpath>\n\t<classpathentry exported=\"true\" kind=\"con\" path=\"com.android.ide.eclipse.adt.ANDROID_FRAMEWORK\"/>\n\t<classpathentry exported=\"true\" kind=\"con\" path=\"com.android.ide.eclipse.adt.LIBRARIES\"/>\n\t<classpathentry kind=\"src\" path=\"src\"/>\n\t<classpathentry kind=\"src\" path=\"gen\"/>\n\t<classpathentry kind=\"src\" path=\"/Shutterbug\"/>\n\t<classpathentry exported=\"true\" kind=\"con\" path=\"com.android.ide.eclipse.adt.DEPENDENCIES\"/>\n\t<classpathentry kind=\"output\" path=\"bin/classes\"/>\n</classpath>\n"
  },
  {
    "path": "ShutterbugDemo/.project",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<projectDescription>\n\t<name>ShutterbugDemo</name>\n\t<comment></comment>\n\t<projects>\n\t</projects>\n\t<buildSpec>\n\t\t<buildCommand>\n\t\t\t<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t\t<buildCommand>\n\t\t\t<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t\t<buildCommand>\n\t\t\t<name>org.eclipse.jdt.core.javabuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t\t<buildCommand>\n\t\t\t<name>com.android.ide.eclipse.adt.ApkBuilder</name>\n\t\t\t<arguments>\n\t\t\t</arguments>\n\t\t</buildCommand>\n\t</buildSpec>\n\t<natures>\n\t\t<nature>com.android.ide.eclipse.adt.AndroidNature</nature>\n\t\t<nature>org.eclipse.jdt.core.javanature</nature>\n\t</natures>\n</projectDescription>\n"
  },
  {
    "path": "ShutterbugDemo/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.applidium.shutterbugdemo\"\n    android:versionCode=\"1\"\n    android:versionName=\"1.0\" >\n\n    <uses-sdk\n        android:minSdkVersion=\"7\"\n        android:targetSdkVersion=\"15\" />\n\n    <!-- required for Shutterbug -->\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n\n    <application\n        android:icon=\"@drawable/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:theme=\"@style/AppTheme\" >\n        <activity\n            android:name=\".ShutterbugActivity\"\n            android:label=\"@string/title_shutterbug_activity\" >\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>"
  },
  {
    "path": "ShutterbugDemo/proguard-project.txt",
    "content": "# To enable ProGuard in your project, edit project.properties\n# to define the proguard.config property as described in that file.\n#\n# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in ${sdk.dir}/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the ProGuard\n# include property in project.properties.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "ShutterbugDemo/project.properties",
    "content": "# This file is automatically generated by Android Tools.\n# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n#\n# This file must be checked in Version Control Systems.\n#\n# To customize properties used by the Ant build system edit\n# \"ant.properties\", and override values to adapt the script to your\n# project structure.\n#\n# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):\n#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt\n\n# Project target.\ntarget=android-16\nandroid.library.reference.1=../Shutterbug\n"
  },
  {
    "path": "ShutterbugDemo/res/layout/activity_shutterbug.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"fill_parent\"\n    android:layout_height=\"fill_parent\">\n    <ListView\n        android:id=\"@+id/list\"\n        android:layout_width=\"fill_parent\"\n        android:layout_height=\"0dp\" \n        android:layout_weight=\"1\"/>\n    <Button\n        android:id=\"@+id/clear_cache_button\"\n        android:layout_width=\"fill_parent\"\n        android:layout_height=\"wrap_content\" \n        android:text=\"@string/clear_cache\"/>\n</LinearLayout>\n"
  },
  {
    "path": "ShutterbugDemo/res/layout/shutterbug_demo_row.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"fill_parent\"\n    android:layout_height=\"wrap_content\" >\n\n    <com.applidium.shutterbug.FetchableImageView\n        android:id=\"@+id/image\"\n        android:layout_width=\"50dip\"\n        android:layout_height=\"50dip\"\n        android:scaleType=\"centerCrop\" />\n\n    <TextView\n        android:id=\"@+id/text\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"left|center_vertical\"\n        android:layout_marginLeft=\"10dip\"\n        android:layout_weight=\"1\"\n        android:maxLines=\"2\"\n        android:textSize=\"20dip\" />\n\n</LinearLayout>"
  },
  {
    "path": "ShutterbugDemo/res/values/strings.xml",
    "content": "<resources>\n\n    <string name=\"app_name\">ShutterbugDemo</string>\n    <string name=\"title_http_connector_activity\">HTTPConnector</string>\n    <string name=\"title_shutterbug_activity\">Shutterbug</string>\n    <string name=\"loading\">Loading…</string>\n    <string name=\"clear_cache\">Clear Cache</string>\n\n</resources>"
  },
  {
    "path": "ShutterbugDemo/res/values/styles.xml",
    "content": "<resources>\n\n    <style name=\"AppTheme\" parent=\"android:Theme.Light\" />\n\n</resources>"
  },
  {
    "path": "ShutterbugDemo/res/values-v11/styles.xml",
    "content": "<resources>\n\n    <style name=\"AppTheme\" parent=\"android:Theme.Holo.Light\" />\n\n</resources>"
  },
  {
    "path": "ShutterbugDemo/res/values-v14/styles.xml",
    "content": "<resources>\n\n    <style name=\"AppTheme\" parent=\"android:Theme.Holo.Light.DarkActionBar\" />\n\n</resources>"
  },
  {
    "path": "ShutterbugDemo/src/com/applidium/shutterbugdemo/ShutterbugActivity.java",
    "content": "package com.applidium.shutterbugdemo;\n\nimport java.io.BufferedInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.HttpURLConnection;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport android.app.Activity;\nimport android.app.ProgressDialog;\nimport android.os.AsyncTask;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.view.View.OnClickListener;\nimport android.view.ViewGroup;\nimport android.widget.BaseAdapter;\nimport android.widget.Button;\nimport android.widget.ListView;\nimport android.widget.TextView;\n\nimport com.applidium.shutterbug.FetchableImageView;\nimport com.applidium.shutterbug.cache.ImageCache;\n\npublic class ShutterbugActivity extends Activity {\n    private ListView       mListView;\n    private DemoAdapter    mAdapter;\n    private ProgressDialog mProgressDialog;\n    private List<String>   mUrls   = new ArrayList<String>();\n    private List<String>   mTitles = new ArrayList<String>();\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_shutterbug);\n\n        mListView = (ListView) findViewById(R.id.list);\n        mAdapter = new DemoAdapter();\n        mListView.setAdapter(mAdapter);\n\n        Button b = (Button) findViewById(R.id.clear_cache_button);\n        b.setOnClickListener(new OnClickListener() {\n            @Override\n            public void onClick(View arg0) {\n                ImageCache.getSharedImageCache(ShutterbugActivity.this).clear();\n                mAdapter.notifyDataSetChanged();\n            }\n        });\n\n        loadGalleryContents();\n    }\n\n    private class DemoAdapter extends BaseAdapter {\n\n        public int getCount() {\n            return mUrls.size();\n        }\n\n        public Object getItem(int position) {\n            return position;\n        }\n\n        public long getItemId(int position) {\n            return position;\n        }\n\n        public View getView(int position, View convertView, ViewGroup parent) {\n            View view = convertView;\n            if (view == null) {\n                view = getLayoutInflater().inflate(R.layout.shutterbug_demo_row, null);\n            }\n            \n            TextView text = (TextView) view.findViewById(R.id.text);\n            text.setText(\"#\" + position + \": \" + mTitles.get(position));\n\n            FetchableImageView image = (FetchableImageView) view.findViewById(R.id.image);\n            image.setImage(mUrls.get(position));\n\n            return view;\n        }\n    }\n\n    private void loadGalleryContents() {\n        mProgressDialog = ProgressDialog.show(this, \"\", getString(R.string.loading));\n        new AsyncTask<Void, Void, Void>() {\n\n            @Override\n            protected Void doInBackground(Void... params) {\n                try {\n                    URL url = new URL(\"http://imgur.com/gallery/top/all.json\");\n                    HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();\n                    urlConnection.setRequestProperty(\"User-Agent\", \"\");\n                    InputStream in = new BufferedInputStream(urlConnection.getInputStream());\n                    JSONObject result = new JSONObject(new java.util.Scanner(in).useDelimiter(\"\\\\A\").next());\n                    if (result.has(\"data\")) {\n                        JSONArray data = result.getJSONArray(\"data\");\n                        mUrls.clear();\n                        mTitles.clear();\n                        for (int i = 0; i < data.length(); i++) {\n                            JSONObject dataObject = data.getJSONObject(i);\n                            mUrls.add(\"http://api.imgur.com/\" + dataObject.getString(\"hash\") + \"s\" + dataObject.getString(\"ext\"));\n                            mTitles.add(dataObject.getString(\"title\"));\n                        }\n                    }\n                } catch (MalformedURLException e) {\n                    e.printStackTrace();\n                } catch (IOException e) {\n                    e.printStackTrace();\n                } catch (JSONException e) {\n                    e.printStackTrace();\n                }\n                return null;\n            }\n\n            @Override\n            protected void onPostExecute(Void result) {\n                super.onPostExecute(result);\n                mAdapter.notifyDataSetChanged();\n                mProgressDialog.dismiss();\n            }\n\n        }.execute();\n\n    }\n}\n"
  }
]