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