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:
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
================================================
================================================
FILE: Shutterbug/.project
================================================
Shutterbugcom.android.ide.eclipse.adt.ResourceManagerBuildercom.android.ide.eclipse.adt.PreCompilerBuilderorg.eclipse.jdt.core.javabuildercom.android.ide.eclipse.adt.ApkBuildercom.android.ide.eclipse.adt.AndroidNatureorg.eclipse.jdt.core.javanature
================================================
FILE: Shutterbug/AndroidManifest.xml
================================================
================================================
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.
*
*
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.
*
*
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.
*
*
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.
*
*
When an entry is being created it is necessary to
* supply a full set of values; the empty value should be used as a
* placeholder if necessary.
*
When an entry is being edited, it is not necessary
* to supply data for every value; values default to their previous
* value.
*
* 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.
*
*
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.
*
*
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[] 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 lruEntries
= new LinkedHashMap(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());
private final Callable cleanupCallable = new Callable() {
@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 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(lruEntries.values())) {
if (entry.currentEditor != null) {
entry.currentEditor.abort();
}
}
trimToSize();
journalWriter.close();
journalWriter = null;
}
private void trimToSize() throws IOException {
while (size > maxSize) {
Map.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 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(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 {
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 {
private final LinkedHashMap 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(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 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.
*
* 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.
*
* The method is called without synchronization: other threads may access
* the cache while this method is executing.
*
* 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.
*
* 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 snapshot() {
return new LinkedHashMap(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 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() {
@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 mFailedUrls = new ArrayList();
private List mCacheListeners = new ArrayList();
private List mCacheUrls = new ArrayList();
private Map mDownloadersMap = new HashMap();
private List mDownloadRequests = new ArrayList();
private List mDownloadImageListeners = new ArrayList();
private List mDownloaders = new ArrayList();
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 {
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 queue = new LinkedList();
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
================================================
================================================
FILE: ShutterbugDemo/.project
================================================
ShutterbugDemocom.android.ide.eclipse.adt.ResourceManagerBuildercom.android.ide.eclipse.adt.PreCompilerBuilderorg.eclipse.jdt.core.javabuildercom.android.ide.eclipse.adt.ApkBuildercom.android.ide.eclipse.adt.AndroidNatureorg.eclipse.jdt.core.javanature
================================================
FILE: ShutterbugDemo/AndroidManifest.xml
================================================
================================================
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
================================================
================================================
FILE: ShutterbugDemo/res/layout/shutterbug_demo_row.xml
================================================
================================================
FILE: ShutterbugDemo/res/values/strings.xml
================================================
ShutterbugDemoHTTPConnectorShutterbugLoading…Clear Cache
================================================
FILE: ShutterbugDemo/res/values/styles.xml
================================================
================================================
FILE: ShutterbugDemo/res/values-v11/styles.xml
================================================
================================================
FILE: ShutterbugDemo/res/values-v14/styles.xml
================================================
================================================
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 mUrls = new ArrayList();
private List mTitles = new ArrayList();
@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() {
@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();
}
}