Write to file in Android but failing to get an OuputStream - android

I need to write to a file which is in the assets folder. I must use
context.getResources().getAssets()
but when I use the open(path_to_my_txt) I get an InputStream, and I have found nothing nice to convert it to an OutputStream
How can I write to this file using the getResources method ?

this below class is taken from Unviersal imageloader libraray
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Provides I/O operations
*
* #author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* #since 1.0.0
*/
public final class IoUtils {
/** {#value} */
public static final int DEFAULT_BUFFER_SIZE = 32 * 1024; // 32 KB
/** {#value} */
public static final int DEFAULT_IMAGE_TOTAL_SIZE = 500 * 1024; // 500 Kb
/** {#value} */
public static final int CONTINUE_LOADING_PERCENTAGE = 75;
private IoUtils() {
}
/**
* Copies stream, fires progress events by listener, can be interrupted by
* listener. Uses buffer size = {#value #DEFAULT_BUFFER_SIZE} bytes.
*
* #param is
* Input stream
* #param os
* Output stream
* #param listener
* null-ok; Listener of copying progress and controller of
* copying interrupting
* #return <b>true</b> - if stream copied successfully; <b>false</b> - if
* copying was interrupted by listener
* #throws IOException
*/
public static boolean copyStream(InputStream is, OutputStream os,
CopyListener listener) throws IOException {
return copyStream(is, os, listener, DEFAULT_BUFFER_SIZE);
}
/**
* Copies stream, fires progress events by listener, can be interrupted by
* listener.
*
* #param is
* Input stream
* #param os
* Output stream
* #param listener
* null-ok; Listener of copying progress and controller of
* copying interrupting
* #param bufferSize
* Buffer size for copying, also represents a step for firing
* progress listener callback, i.e. progress event will be fired
* after every copied <b>bufferSize</b> bytes
* #return <b>true</b> - if stream copied successfully; <b>false</b> - if
* copying was interrupted by listener
* #throws IOException
*/
public static boolean copyStream(InputStream is, OutputStream os,
CopyListener listener, int bufferSize) throws IOException {
int current = 0;
int total = is.available();
if (total <= 0) {
total = DEFAULT_IMAGE_TOTAL_SIZE;
}
final byte[] bytes = new byte[bufferSize];
int count;
if (shouldStopLoading(listener, current, total))
return false;
while ((count = is.read(bytes, 0, bufferSize)) != -1) {
os.write(bytes, 0, count);
current += count;
if (shouldStopLoading(listener, current, total))
return false;
}
os.flush();
return true;
}
private static boolean shouldStopLoading(CopyListener listener,
int current, int total) {
if (listener != null) {
boolean shouldContinue = listener.onBytesCopied(current, total);
if (!shouldContinue) {
if (100 * current / total < CONTINUE_LOADING_PERCENTAGE) {
return true; // if loaded more than 75% then continue
// loading anyway
}
}
}
return false;
}
/**
* Reads all data from stream and close it silently
*
* #param is
* Input stream
*/
public static void readAndCloseStream(InputStream is) {
final byte[] bytes = new byte[DEFAULT_BUFFER_SIZE];
try {
while (is.read(bytes, 0, DEFAULT_BUFFER_SIZE) != -1)
;
} catch (IOException e) {
// Do nothing
} finally {
closeSilently(is);
}
}
public static void closeSilently(Closeable closeable) {
try {
closeable.close();
} catch (Exception e) {
// Do nothing
}
}
/** Listener and controller for copy process */
public static interface CopyListener {
/**
* #param current
* Loaded bytes
* #param total
* Total bytes for loading
* #return <b>true</b> - if copying should be continued; <b>false</b> -
* if copying should be interrupted
*/
boolean onBytesCopied(int current, int total);
}
}
AssetManager assetManager = getAssets();
InputStream is = null;
try {
is = assetManager.open("filename");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
OutputStream os = null;
IoUtils.copyStream(is, os);
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}

you can't write to a file in assets fold.But you can write it to file system.

Related

Get the font name from the file from the asset folder

I have this function that the user selects from a list of different fonts. Now I want to get the exact name of the font file I'm trying to use.
I'm displaying the font file name, but not the name of the font. Ex. "Arial.tff" or "BROADW.tff".
This is the one that I want to get from the file.
I want to get the title field here. Is that possible?
Here's my code when trying to get all the font files from my asset folder.
String[] fileList;
AssetManager aMan = getAssets();
try {
fileList = aMan.list("");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Then I just display this into a adapter and when the user choose that font, I convert it. Any Ideas? Thanks.
You will need to parse the font file. I will first paste example code on getting the font name. Then I will paste the code I extracted and modified from Apache FOP.
Example usage:
try {
String pathToFontInAssets = "fonts/Arrial.ttf";
InputStream inputStream = getAssets().open(pathToFontInAssets);
TTFFile ttfFile = FontFileReader.readTTF(inputStream);
String fontName = ttfFile.getFullName();
} catch (IOException e) {
e.printStackTrace();
}
Copy the following classes to your project:
FontFileReader.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/* $Id: FontFileReader.java 1357883 2012-07-05 20:29:53Z gadams $ */
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Reads a TrueType font file into a byte array and provides file like functions for array access.
*/
public class FontFileReader {
/**
* Read a font file
*
* #param path absolute path to the font file.
* #return
* #throws IOException if an error occurred while reading the font.
*/
public static TTFFile readTTF(String path) throws IOException {
TTFFile ttfFile = new TTFFile();
ttfFile.readFont(new FontFileReader(path));
return ttfFile;
}
/**
* Read a font file
*
* #param inputStream InputStream to read from
* #return
* #throws IOException if an error occurred while reading the font.
*/
public static TTFFile readTTF(InputStream inputStream) throws IOException {
TTFFile ttfFile = new TTFFile();
ttfFile.readFont(new FontFileReader(inputStream));
return ttfFile;
}
private int fsize; // file size
private int current; // current position in file
private byte[] file;
/**
* Constructor
*
* #param in
* InputStream to read from
* #throws IOException
* In case of an I/O problem
*/
public FontFileReader(InputStream in) throws IOException {
init(in);
}
/**
* Constructor
*
* #param fileName
* filename to read
* #throws IOException
* In case of an I/O problem
*/
public FontFileReader(String fileName) throws IOException {
File f = new File(fileName);
InputStream in = new FileInputStream(f);
try {
init(in);
} finally {
in.close();
}
}
/**
* Returns the full byte array representation of the file.
*
* #return byte array.
*/
public byte[] getAllBytes() {
return file;
}
/**
* Returns current file position.
*
* #return int The current position.
*/
public int getCurrentPos() {
return current;
}
/**
* Returns the size of the file.
*
* #return int The filesize
*/
public int getFileSize() {
return fsize;
}
/**
* Initializes class and reads stream. Init does not close stream.
*
* #param in
* InputStream to read from new array with size + inc
* #throws IOException
* In case of an I/O problem
*/
private void init(InputStream in) throws java.io.IOException {
file = IOUtils.toByteArray(in);
fsize = file.length;
current = 0;
}
/**
* Read 1 byte.
*
* #return One byte
* #throws IOException
* If EOF is reached
*/
private byte read() throws IOException {
if (current >= fsize) {
throw new EOFException("Reached EOF, file size=" + fsize);
}
byte ret = file[current++];
return ret;
}
/**
* Read 1 signed byte.
*
* #return One byte
* #throws IOException
* If EOF is reached
*/
public byte readTTFByte() throws IOException {
return read();
}
/**
* Read 4 bytes.
*
* #return One signed integer
* #throws IOException
* If EOF is reached
*/
public int readTTFLong() throws IOException {
long ret = readTTFUByte(); // << 8;
ret = (ret << 8) + readTTFUByte();
ret = (ret << 8) + readTTFUByte();
ret = (ret << 8) + readTTFUByte();
return (int) ret;
}
/**
* Read an ISO-8859-1 string of len bytes.
*
* #param len
* The bytesToUpload of the string to read
* #return A String
* #throws IOException
* If EOF is reached
*/
public String readTTFString(int len) throws IOException {
if ((len + current) > fsize) {
throw new EOFException("Reached EOF, file size=" + fsize);
}
byte[] tmp = new byte[len];
System.arraycopy(file, current, tmp, 0, len);
current += len;
String encoding;
if ((tmp.length > 0) && (tmp[0] == 0)) {
encoding = "UTF-16BE";
} else {
encoding = "ISO-8859-1";
}
return new String(tmp, encoding);
}
/**
* Read an ISO-8859-1 string of len bytes.
*
* #param len
* The bytesToUpload of the string to read
* #param encodingID
* the string encoding id (presently ignored; always uses UTF-16BE)
* #return A String
* #throws IOException
* If EOF is reached
*/
public String readTTFString(int len, int encodingID) throws IOException {
if ((len + current) > fsize) {
throw new java.io.EOFException("Reached EOF, file size=" + fsize);
}
byte[] tmp = new byte[len];
System.arraycopy(file, current, tmp, 0, len);
current += len;
String encoding;
encoding = "UTF-16BE"; // Use this for all known encoding IDs for now
return new String(tmp, encoding);
}
/**
* Read 1 unsigned byte.
*
* #return One unsigned byte
* #throws IOException
* If EOF is reached
*/
public int readTTFUByte() throws IOException {
byte buf = read();
if (buf < 0) {
return 256 + buf;
} else {
return buf;
}
}
/**
* Read 4 bytes.
*
* #return One unsigned integer
* #throws IOException
* If EOF is reached
*/
public long readTTFULong() throws IOException {
long ret = readTTFUByte();
ret = (ret << 8) + readTTFUByte();
ret = (ret << 8) + readTTFUByte();
ret = (ret << 8) + readTTFUByte();
return ret;
}
/**
* Read 2 bytes unsigned.
*
* #return One unsigned short
* #throws IOException
* If EOF is reached
*/
public int readTTFUShort() throws IOException {
int ret = (readTTFUByte() << 8) + readTTFUByte();
return ret;
}
/**
* Set current file position to offset
*
* #param offset
* The new offset to set
* #throws IOException
* In case of an I/O problem
*/
public void seekSet(long offset) throws IOException {
if (offset > fsize || offset < 0) {
throw new EOFException("Reached EOF, file size=" + fsize + " offset=" + offset);
}
current = (int) offset;
}
/**
* Skip a given number of bytes.
*
* #param add
* The number of bytes to advance
* #throws IOException
* In case of an I/O problem
*/
public void skip(long add) throws IOException {
seekSet(current + add);
}
}
TTFDirTabEntry.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/* $Id: TTFDirTabEntry.java 1357883 2012-07-05 20:29:53Z gadams $ */
import java.io.IOException;
import java.io.UnsupportedEncodingException;
/**
* This class represents an entry to a TrueType font's Dir Tab.
*/
public class TTFDirTabEntry {
private final byte[] tag = new byte[4];
private long offset;
private long length;
TTFDirTabEntry() {
}
public TTFDirTabEntry(long offset, long length) {
this.offset = offset;
this.length = length;
}
/**
* Returns the bytesToUpload.
*
* #return long
*/
public long getLength() {
return length;
}
/**
* Returns the offset.
*
* #return long
*/
public long getOffset() {
return offset;
}
/**
* Returns the tag bytes.
*
* #return byte[]
*/
public byte[] getTag() {
return tag;
}
/**
* Returns the tag bytes.
*
* #return byte[]
*/
public String getTagString() {
try {
return new String(tag, "ISO-8859-1");
} catch (UnsupportedEncodingException e) {
return toString(); // Should never happen.
}
}
/**
* Read Dir Tab.
*
* #param in
* font file reader
* #return tag name
* #throws IOException
* upon I/O exception
*/
public String read(FontFileReader in) throws IOException {
tag[0] = in.readTTFByte();
tag[1] = in.readTTFByte();
tag[2] = in.readTTFByte();
tag[3] = in.readTTFByte();
in.skip(4); // Skip checksum
offset = in.readTTFULong();
length = in.readTTFULong();
String tagStr = new String(tag, "ISO-8859-1");
return tagStr;
}
#Override
public String toString() {
return "Read dir tab [" + tag[0] + " " + tag[1] + " " + tag[2] + " " + tag[3] + "]"
+ " offset: " + offset + " bytesToUpload: " + length + " name: " + tag;
}
}
TTFFile.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/* $Id: TTFFile.java 1395925 2012-10-09 09:13:18Z jeremias $ */
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Reads a TrueType file or a TrueType Collection. The TrueType spec can be found at the Microsoft.
* Typography site: http://www.microsoft.com/truetype/
*/
public class TTFFile {
/** The FontFileReader used to read this TrueType font. */
private FontFileReader fontFile;
/**
* Table directory
*/
private Map<TTFTableName, TTFDirTabEntry> dirTabs;
private String postScriptName = "";
private String fullName = "";
private String notice = "";
private final Set<String> familyNames = new HashSet<String>();
private String subFamilyName = "";
TTFFile() {
}
/**
* Returns the font family names of the font.
*
* #return Set The family names (a Set of Strings)
*/
public Set<String> getFamilyNames() {
return familyNames;
}
/**
* Returns the full name of the font.
*
* #return String The full name
*/
public String getFullName() {
return fullName;
}
public String getNotice() {
return notice;
}
/**
* Returns the PostScript name of the font.
*
* #return String The PostScript name
*/
public String getPostScriptName() {
return postScriptName;
}
/**
* Returns the font sub family name of the font.
*
* #return String The sub family name
*/
public String getSubFamilyName() {
return subFamilyName;
}
/**
* Read Table Directory from the current position in the FontFileReader and fill the global
* HashMap dirTabs with the table name (String) as key and a TTFDirTabEntry as value.
*
* #throws IOException
* in case of an I/O problem
*/
private void readDirTabs() throws IOException {
fontFile.readTTFLong(); // TTF_FIXED_SIZE (4 bytes)
int ntabs = fontFile.readTTFUShort();
fontFile.skip(6); // 3xTTF_USHORT_SIZE
dirTabs = new HashMap<>();
TTFDirTabEntry[] pd = new TTFDirTabEntry[ntabs];
for (int i = 0; i < ntabs; i++) {
pd[i] = new TTFDirTabEntry();
String tableName = pd[i].read(fontFile);
dirTabs.put(TTFTableName.getValue(tableName), pd[i]);
}
dirTabs.put(TTFTableName.TABLE_DIRECTORY, new TTFDirTabEntry(0L, fontFile.getCurrentPos()));
}
/**
* Reads the font using a FontFileReader.
*
* #param in
* The FontFileReader to use
* #throws IOException
* In case of an I/O problem
*/
void readFont(FontFileReader in) throws IOException {
fontFile = in;
readDirTabs();
readName();
}
/**
* Read the "name" table.
*
* #throws IOException
* In case of a I/O problem
*/
private void readName() throws IOException {
seekTab(fontFile, TTFTableName.NAME, 2);
int i = fontFile.getCurrentPos();
int n = fontFile.readTTFUShort();
int j = fontFile.readTTFUShort() + i - 2;
i += 2 * 2;
while (n-- > 0) {
fontFile.seekSet(i);
int platformID = fontFile.readTTFUShort();
int encodingID = fontFile.readTTFUShort();
int languageID = fontFile.readTTFUShort();
int k = fontFile.readTTFUShort();
int l = fontFile.readTTFUShort();
if (((platformID == 1 || platformID == 3) && (encodingID == 0 || encodingID == 1))) {
fontFile.seekSet(j + fontFile.readTTFUShort());
String txt;
if (platformID == 3) {
txt = fontFile.readTTFString(l, encodingID);
} else {
txt = fontFile.readTTFString(l);
}
switch (k) {
case 0:
if (notice.length() == 0) {
notice = txt;
}
break;
case 1: // Font Family Name
case 16: // Preferred Family
familyNames.add(txt);
break;
case 2:
if (subFamilyName.length() == 0) {
subFamilyName = txt;
}
break;
case 4:
if (fullName.length() == 0 || (platformID == 3 && languageID == 1033)) {
fullName = txt;
}
break;
case 6:
if (postScriptName.length() == 0) {
postScriptName = txt;
}
break;
default:
break;
}
}
i += 6 * 2;
}
}
/**
* Position inputstream to position indicated in the dirtab offset + offset
*
* #param in
* font file reader
* #param tableName
* (tag) of table
* #param offset
* from start of table
* #return true if seek succeeded
* #throws IOException
* if I/O exception occurs during seek
*/
private boolean seekTab(FontFileReader in, TTFTableName tableName, long offset)
throws IOException
{
TTFDirTabEntry dt = dirTabs.get(tableName);
if (dt == null) {
return false;
} else {
in.seekSet(dt.getOffset() + offset);
}
return true;
}
}
TTFTableName.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/* $Id: TTFTableName.java 1357883 2012-07-05 20:29:53Z gadams $ */
/**
* Represents table names as found in a TrueType font's Table Directory. TrueType fonts may have
* custom tables so we cannot use an enum.
*/
public final class TTFTableName {
/** The first table in a TrueType font file containing metadata about other tables. */
public static final TTFTableName TABLE_DIRECTORY = new TTFTableName("tableDirectory");
/** Naming table. */
public static final TTFTableName NAME = new TTFTableName("name");
/**
* Returns an instance of this class corresponding to the given string representation.
*
* #param tableName
* table name as in the Table Directory
* #return TTFTableName
*/
public static TTFTableName getValue(String tableName) {
if (tableName != null) {
return new TTFTableName(tableName);
}
throw new IllegalArgumentException("A TrueType font table name must not be null");
}
private final String name;
private TTFTableName(String name) {
this.name = name;
}
#Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof TTFTableName)) {
return false;
}
TTFTableName to = (TTFTableName) o;
return name.equals(to.getName());
}
/**
* Returns the name of the table as it should be in the Directory Table.
*/
public String getName() {
return name;
}
#Override
public int hashCode() {
return name.hashCode();
}
#Override
public String toString() {
return name;
}
}
IOUtils.java (used in FontFileReader)
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class IOUtils {
private static int DEFAULT_BUFFER = 4096; // 4kb
private static int EOF = -1; // end of file
/**
* Get the contents of an {#code InputStream} as a {#code byte[]}.
* <p/>
* This method buffers the input internally, so there is no need to use a {#code
* BufferedInputStream}.
*
* #param input
* the {#code InputStream} to read from
* #return the requested byte array
* #throws NullPointerException
* if the input is null
* #throws IOException
* if an I/O error occurs
*/
public static byte[] toByteArray(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
copy(input, output);
return output.toByteArray();
}
/**
* Copy bytes from an {#code InputStream} to an {#code OutputStream}.</p>
* <p/>
* This method buffers the input internally, so there is no need to use a {#code
* BufferedInputStream}.</p>
* <p/>
* Large streams (over 2GB) will return a bytes copied value of {#code -1} after the copy has
* completed since the correct number of bytes cannot be returned as an int. For large streams
* use the {#code copyLarge(InputStream, OutputStream)} method.</p>
*
* #param input
* the {#code InputStream} to read from
* #param output
* the {#code OutputStream} to write to
* #return the number of bytes copied, or -1 if > Integer.MAX_VALUE
* #throws NullPointerException
* if the input or output is null
* #throws IOException
* if an I/O error occurs
*/
public static int copy(InputStream input, OutputStream output) throws IOException {
long count = copyLarge(input, output);
if (count > Integer.MAX_VALUE) {
return -1;
}
return (int) count;
}
/**
* Copy bytes from a large (over 2GB) {#code InputStream} to an {#code OutputStream}.</p>
* <p/>
* This method buffers the input internally, so there is no need to use a {#code
* BufferedInputStream}.</p>
* <p/>
* The buffer size is given by {#link #DEFAULT_BUFFER}.</p>
*
* #param input
* the {#code InputStream} to read from
* #param output
* the {#code OutputStream} to write to
* #return the number of bytes copied
* #throws NullPointerException
* if the input or output is null
* #throws IOException
* if an I/O error occurs
*/
public static long copyLarge(InputStream input, OutputStream output) throws
IOException
{
return copyLarge(input, output, new byte[DEFAULT_BUFFER]);
}
/**
* Copy bytes from a large (over 2GB) {#code InputStream} to an {#code OutputStream}.</p>
* <p/>
* This method uses the provided buffer, so there is no need to use a {#code
* BufferedInputStream}.</p>
*
* #param input
* the {#code InputStream} to read from
* #param output
* the {#code OutputStream} to write to
* #param buffer
* the buffer to use for the copy
* #return the number of bytes copied
* #throws NullPointerException
* if the input or output is null
* #throws IOException
* if an I/O error occurs
*/
public static long copyLarge(InputStream input, OutputStream output, byte[] buffer)
throws IOException
{
long count = 0;
int n = 0;
while (EOF != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
private IOUtils() {
}
}

iitialize ImageCache from the context of an activity

I try to understand this sample to match with mine:(DisplayingBitmaps)
http://developer.android.com/downloads/samples/DisplayingBitmaps.zip
This code uses the class ImageCache that manages the caching of bitmaps:
public class ImageCache {
private static final String TAG = "ImageCache";
// Default memory cache size in kilobytes
private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 5; // 5MB
// Default disk cache size in bytes
private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
// Compression settings when writing images to disk cache
private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
private static final int DEFAULT_COMPRESS_QUALITY = 70;
private static final int DISK_CACHE_INDEX = 0;
// Constants to easily toggle various caches
private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
private DiskLruCache mDiskLruCache;
private LruCache<String, BitmapDrawable> mMemoryCache;
public static ImageCacheParams mCacheParams;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private Set<SoftReference<Bitmap>> mReusableBitmaps;
/**
* Create a new ImageCache object using the specified parameters. This should not be
* called directly by other classes, instead use
* {#link ImageCache#getInstance(FragmentManager, ImageCacheParams)} to fetch an ImageCache
* instance.
*
* #param cacheParams The cache parameters to use to initialize the cache
*/
public ImageCache(ImageCacheParams cacheParams) {
init(cacheParams);
}
/**
* Return an {#link ImageCache} instance. A {#link RetainFragment} is used to retain the
* ImageCache object across configuration changes such as a change in device orientation.
*
* #param fragmentManager The fragment manager to use when dealing with the retained fragment.
* #param cacheParams The cache parameters to use if the ImageCache needs instantiation.
* #return An existing retained ImageCache object or a new one if one did not exist
*/
public static ImageCache getInstance(
FragmentManager fragmentManager, ImageCacheParams cacheParams) {
// Search for, or create an instance of the non-UI RetainFragment
final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager);
// See if we already have an ImageCache stored in RetainFragment
ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
// No existing ImageCache, create one and store it in RetainFragment
if (imageCache == null) {
imageCache = new ImageCache(cacheParams);
mRetainFragment.setObject(imageCache);
}
return imageCache;
}
/**
* Initialize the cache, providing all parameters.
*
* #param cacheParams The cache parameters to initialize the cache
*/
public void init(ImageCacheParams cacheParams) {
mCacheParams = cacheParams;
//BEGIN_INCLUDE(init_memory_cache)
// Set up memory cache
if (mCacheParams.memoryCacheEnabled) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")");
}
// If we're running on Honeycomb or newer, create a set of reusable bitmaps that can be
// populated into the inBitmap field of BitmapFactory.Options. Note that the set is
// of SoftReferences which will actually not be very effective due to the garbage
// collector being aggressive clearing Soft/WeakReferences. A better approach
// would be to use a strongly references bitmaps, however this would require some
// balancing of memory usage between this set and the bitmap LruCache. It would also
// require knowledge of the expected size of the bitmaps. From Honeycomb to JellyBean
// the size would need to be precise, from KitKat onward the size would just need to
// be the upper bound (due to changes in how inBitmap can re-use bitmaps).
if (Utils.hasHoneycomb()) {
mReusableBitmaps =
Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}
mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
/**
* Notify the removed entry that is no longer being cached
*/
#Override
protected void entryRemoved(boolean evicted, String key,
BitmapDrawable oldValue, BitmapDrawable newValue) {
if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
// The removed entry is a recycling drawable, so notify it
// that it has been removed from the memory cache
((RecyclingBitmapDrawable) oldValue).setIsCached(false);
} else {
// The removed entry is a standard BitmapDrawable
if (Utils.hasHoneycomb()) {
// We're running on Honeycomb or later, so add the bitmap
// to a SoftReference set for possible use with inBitmap later
mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));
}
}
}
/**
* Measure item size in kilobytes rather than units which is more practical
* for a bitmap cache
*/
#Override
protected int sizeOf(String key, BitmapDrawable value) {
final int bitmapSize = getBitmapSize(value) / 1024;
return bitmapSize == 0 ? 1 : bitmapSize;
}
};
}
//END_INCLUDE(init_memory_cache)
// By default the disk cache is not initialized here as it should be initialized
// on a separate thread due to disk access.
if (cacheParams.initDiskCacheOnCreate) {
// Set up disk cache
initDiskCache();
}
}
/**
* Initializes the disk cache. Note that this includes disk access so this should not be
* executed on the main/UI thread. By default an ImageCache does not initialize the disk
* cache when it is created, instead you should call initDiskCache() to initialize it on a
* background thread.
*/
public void initDiskCache() {
// Set up disk cache
synchronized (mDiskCacheLock) {
if (mDiskLruCache == null || mDiskLruCache.isClosed()) {
File diskCacheDir = mCacheParams.diskCacheDir;
if (mCacheParams.diskCacheEnabled && diskCacheDir != null) {
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) {
try {
mDiskLruCache = DiskLruCache.open(
diskCacheDir, 1, 1, mCacheParams.diskCacheSize);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache initialized");
}
} catch (final IOException e) {
mCacheParams.diskCacheDir = null;
Log.e(TAG, "initDiskCache - " + e);
}
}
}
}
mDiskCacheStarting = false;
mDiskCacheLock.notifyAll();
}
}
public void addBitmapToCache(String data, BitmapDrawable value) {
//BEGIN_INCLUDE(add_bitmap_to_cache)
if (data == null || value == null) {
return;
}
// Add to memory cache
if (mMemoryCache != null) {
if (RecyclingBitmapDrawable.class.isInstance(value)) {
// The removed entry is a recycling drawable, so notify it
// that it has been added into the memory cache
((RecyclingBitmapDrawable) value).setIsCached(true);
}
mMemoryCache.put(data, value);
}
synchronized (mDiskCacheLock) {
// Add to disk cache
if (mDiskLruCache != null) {
final String key = hashKeyForDisk(data);
OutputStream out = null;
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot == null) {
final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
out = editor.newOutputStream(DISK_CACHE_INDEX);
value.getBitmap().compress(
mCacheParams.compressFormat, mCacheParams.compressQuality, out);
editor.commit();
out.close();
}
} else {
snapshot.getInputStream(DISK_CACHE_INDEX).close();
}
} catch (final IOException e) {
Log.e(TAG, "addBitmapToCache - " + e);
} catch (Exception e) {
Log.e(TAG, "addBitmapToCache - " + e);
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {}
}
}
}
//END_INCLUDE(add_bitmap_to_cache)
}
public BitmapDrawable getBitmapFromMemCache(String data) {
//BEGIN_INCLUDE(get_bitmap_from_mem_cache)
BitmapDrawable memValue = null;
if (mMemoryCache != null) {
memValue = mMemoryCache.get(data);
}
if (BuildConfig.DEBUG && memValue != null) {
Log.d(TAG, "Memory cache hit");
}
return memValue;
//END_INCLUDE(get_bitmap_from_mem_cache)
}
public Bitmap getBitmapFromDiskCache(String data) {
//BEGIN_INCLUDE(get_bitmap_from_disk_cache)
final String key = hashKeyForDisk(data);
Bitmap bitmap = null;
synchronized (mDiskCacheLock) {
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
InputStream inputStream = null;
try {
final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache hit");
}
inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
if (inputStream != null) {
FileDescriptor fd = ((FileInputStream) inputStream).getFD();
// Decode bitmap, but we don't want to sample so give
// MAX_VALUE as the target dimensions
bitmap = ImageResizer.decodeSampledBitmapFromDescriptor(
fd, Integer.MAX_VALUE, Integer.MAX_VALUE, this);
}
}
} catch (final IOException e) {
Log.e(TAG, "getBitmapFromDiskCache - " + e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {}
}
}
return bitmap;
}
//END_INCLUDE(get_bitmap_from_disk_cache)
}
/**
* #param options - BitmapFactory.Options with out* options populated
* #return Bitmap that case be used for inBitmap
*/
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
//BEGIN_INCLUDE(get_bitmap_from_reusable_set)
Bitmap bitmap = null;
if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
synchronized (mReusableBitmaps) {
final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();
Bitmap item;
while (iterator.hasNext()) {
item = iterator.next().get();
if (null != item && item.isMutable()) {
// Check to see it the item can be used for inBitmap
if (canUseForInBitmap(item, options)) {
bitmap = item;
// Remove from reusable set so it can't be used again
iterator.remove();
break;
}
} else {
// Remove from the set if the reference has been cleared.
iterator.remove();
}
}
}
}
return bitmap;
//END_INCLUDE(get_bitmap_from_reusable_set)
}
/**
* Clears both the memory and disk cache associated with this ImageCache object. Note that
* this includes disk access so this should not be executed on the main/UI thread.
*/
public void clearCache() {
if (mMemoryCache != null) {
mMemoryCache.evictAll();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache cleared");
}
}
synchronized (mDiskCacheLock) {
mDiskCacheStarting = true;
if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
try {
mDiskLruCache.delete();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache cleared");
}
} catch (IOException e) {
Log.e(TAG, "clearCache - " + e);
}
mDiskLruCache = null;
initDiskCache();
}
}
}
/**
* Flushes the disk cache associated with this ImageCache object. Note that this includes
* disk access so this should not be executed on the main/UI thread.
*/
public void flush() {
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null) {
try {
mDiskLruCache.flush();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache flushed");
}
} catch (IOException e) {
Log.e(TAG, "flush - " + e);
}
}
}
}
/**
* Closes the disk cache associated with this ImageCache object. Note that this includes
* disk access so this should not be executed on the main/UI thread.
*/
public void close() {
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null) {
try {
if (!mDiskLruCache.isClosed()) {
mDiskLruCache.close();
mDiskLruCache = null;
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache closed");
}
}
} catch (IOException e) {
Log.e(TAG, "close - " + e);
}
}
}
}
/**
* A holder class that contains cache parameters.
*/
public static class ImageCacheParams {
public int memCacheSize = DEFAULT_MEM_CACHE_SIZE;
public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
public File diskCacheDir;
public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
public int compressQuality = DEFAULT_COMPRESS_QUALITY;
public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED;
public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED;
public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE;
/**
* Create a set of image cache parameters that can be provided to
* {#link ImageCache#getInstance(FragmentManager, ImageCacheParams)} or
* {#link ImageWorker#addImageCache(FragmentManager, ImageCacheParams)}.
* #param context A context to use.
* #param diskCacheDirectoryName A unique subdirectory name that will be appended to the
* application cache directory. Usually "cache" or "images"
* is sufficient.
*/
public ImageCacheParams(Context context, String diskCacheDirectoryName) {
diskCacheDir = getDiskCacheDir(context, diskCacheDirectoryName);
}
/**
* Sets the memory cache size based on a percentage of the max available VM memory.
* Eg. setting percent to 0.2 would set the memory cache to one fifth of the available
* memory. Throws {#link IllegalArgumentException} if percent is < 0.01 or > .8.
* memCacheSize is stored in kilobytes instead of bytes as this will eventually be passed
* to construct a LruCache which takes an int in its constructor.
*
* This value should be chosen carefully based on a number of factors
* Refer to the corresponding Android Training class for more discussion:
* http://developer.android.com/training/displaying-bitmaps/
*
* #param percent Percent of available app memory to use to size memory cache
*/
public void setMemCacheSizePercent(float percent) {
if (percent < 0.01f || percent > 0.8f) {
throw new IllegalArgumentException("setMemCacheSizePercent - percent must be "
+ "between 0.01 and 0.8 (inclusive)");
}
memCacheSize = Math.round(percent * Runtime.getRuntime().maxMemory() / 1024);
}
}
/**
* #param candidate - Bitmap to check
* #param targetOptions - Options that have the out* value populated
* #return true if <code>candidate</code> can be used for inBitmap re-use with
* <code>targetOptions</code>
*/
#TargetApi(VERSION_CODES.KITKAT)
private static boolean canUseForInBitmap(
Bitmap candidate, BitmapFactory.Options targetOptions) {
//BEGIN_INCLUDE(can_use_for_inbitmap)
if (!Utils.hasKitKat()) {
// On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
return candidate.getWidth() == targetOptions.outWidth
&& candidate.getHeight() == targetOptions.outHeight
&& targetOptions.inSampleSize == 1;
}
// From Android 4.4 (KitKat) onward we can re-use if the byte size of the new bitmap
// is smaller than the reusable bitmap candidate allocation byte count.
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;
int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
return byteCount <= candidate.getAllocationByteCount();
//END_INCLUDE(can_use_for_inbitmap)
}
/**
* Return the byte usage per pixel of a bitmap based on its configuration.
* #param config The bitmap configuration.
* #return The byte usage per pixel.
*/
private static int getBytesPerPixel(Config config) {
if (config == Config.ARGB_8888) {
return 4;
} else if (config == Config.RGB_565) {
return 2;
} else if (config == Config.ARGB_4444) {
return 2;
} else if (config == Config.ALPHA_8) {
return 1;
}
return 1;
}
/**
* Get a usable cache directory (external if available, internal otherwise).
*
* #param context The context to use
* #param uniqueName A unique directory name to append to the cache dir
* #return The cache dir
*/
public static File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
/**
* A hashing method that changes a string (like a URL) into a hash suitable for using as a
* disk filename.
*/
public static String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
private static String bytesToHexString(byte[] bytes) {
// http://stackoverflow.com/questions/332079
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
/**
* Get the size in bytes of a bitmap in a BitmapDrawable. Note that from Android 4.4 (KitKat)
* onward this returns the allocated memory size of the bitmap which can be larger than the
* actual bitmap data byte count (in the case it was re-used).
*
* #param value
* #return size in bytes
*/
#TargetApi(VERSION_CODES.KITKAT)
public static int getBitmapSize(BitmapDrawable value) {
Bitmap bitmap = value.getBitmap();
// From KitKat onward use getAllocationByteCount() as allocated bytes can potentially be
// larger than bitmap byte count.
if (Utils.hasKitKat()) {
return bitmap.getAllocationByteCount();
}
if (Utils.hasHoneycombMR1()) {
return bitmap.getByteCount();
}
// Pre HC-MR1
return bitmap.getRowBytes() * bitmap.getHeight();
}
/**
* Check if external storage is built-in or removable.
*
* #return True if external storage is removable (like an SD card), false
* otherwise.
*/
#TargetApi(VERSION_CODES.GINGERBREAD)
public static boolean isExternalStorageRemovable() {
if (Utils.hasGingerbread()) {
return Environment.isExternalStorageRemovable();
}
return true;
}
/**
* Get the external app cache directory.
*
* #param context The context to use
* #return The external cache dir
*/
#TargetApi(VERSION_CODES.FROYO)
public static File getExternalCacheDir(Context context) {
if (Utils.hasFroyo()) {
return context.getExternalCacheDir();
}
// Before Froyo we need to construct the external cache dir ourselves
final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
}
/**
* Check how much usable space is available at a given path.
*
* #param path The path to check
* #return The space available in bytes
*/
#TargetApi(VERSION_CODES.GINGERBREAD)
public static long getUsableSpace(File path) {
if (Utils.hasGingerbread()) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
/**
* Locate an existing instance of this Fragment or if not found, create and
* add it using FragmentManager.
*
* #param fm The FragmentManager manager to use.
* #return The existing instance of the Fragment or the new instance if just
* created.
*/
private static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
//BEGIN_INCLUDE(find_create_retain_fragment)
// Check to see if we have retained the worker fragment.
RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);
// If not retained (or first time running), we need to create and add it.
if (mRetainFragment == null) {
mRetainFragment = new RetainFragment();
fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss();
}
return mRetainFragment;
//END_INCLUDE(find_create_retain_fragment)
}
/**
* A simple non-UI Fragment that stores a single Object and is retained over configuration
* changes. It will be used to retain the ImageCache object.
*/
public static class RetainFragment extends Fragment {
private Object mObject;
/**
* Empty constructor as per the Fragment documentation
*/
public RetainFragment() {}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Make sure this Fragment is retained over a configuration change
setRetainInstance(true);
}
/**
* Store a single object in this Fragment.
*
* #param object The object to store
*/
public void setObject(Object object) {
mObject = object;
}
/**
* Get the stored object.
*
* #return The stored object
*/
public Object getObject() {
return mObject;
}
}
}
In the sample, the cache (LRUCache) is initialized with the following code from a fragment (ImageGridFragment):
ImageCache.ImageCacheParams cacheParams =
new ImageCache.ImageCacheParams(getActivity(), IMAGE_CACHE_DIR);
cacheParams.setMemCacheSizePercent(0.25f); // Set memory cache to 25% of app memory
// The ImageFetcher takes care of loading images into our ImageView children asynchronously
mImageFetcher = new ImageFetcher(getActivity(), mImageThumbSize);
mImageFetcher.setLoadingImage(R.drawable.empty_photo);
mImageFetcher.addImageCache(getActivity().getSupportFragmentManager(), cacheParams);// <-- here we inititialise cache by method ImageWorker with the fragment context in parameter ?
The methods of ImageWorker that initialize the cache in ImageCache with a fragment context:
/**
* Adds an {#link ImageCache} to this {#link ImageWorker} to handle disk and memory bitmap
* caching.
* #param fragmentManager
* #param cacheParams The cache parameters to use for the image cache.
*/
public void addImageCache(FragmentManager fragmentManager,
ImageCache.ImageCacheParams cacheParams) {
mImageCacheParams = cacheParams;
mImageCache = ImageCache.getInstance(fragmentManager, mImageCacheParams);
new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE);
}
/**
* Adds an {#link ImageCache} to this {#link ImageWorker} to handle disk and memory bitmap
* caching.
* #param activity
* #param diskCacheDirectoryName See
* {#link ImageCache.ImageCacheParams#ImageCacheParams(android.content.Context, String)}.
*/
public void addImageCache(FragmentActivity activity, String diskCacheDirectoryName) {
mImageCacheParams = new ImageCache.ImageCacheParams(activity, diskCacheDirectoryName);
mImageCache = ImageCache.getInstance(activity.getSupportFragmentManager(), mImageCacheParams);
new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE);
}
How to "instantiate" and initialize the cahe from an activity context with this class?
Thanks

Maintain Activity State, After Control Passes To Next Activity ,For Back Button Traversing

OnClicking an Item in A ListView Fragment, Control passes to Detail Activity, which downloads the data related to this item and sets the appropriate adapters for the viewpagers in this activity.Within this activity, if a user clicks on a image, a FullScreen Activity starts which shows this particular image in fullscreen.
Now My problem is: when user leaves this FullScreen Activity by either tapping Close Icon or Back Button Traversing, How do i reuse the Detail Activity Data(Downloaded earlier for this item) instead of downloading it again.
Solution Should also work for scenario when user clicks a new item on Detail Activity,Data Downloading should begin for this item.
Don't want to use SQLLite, FileSystem for storage. I am ok with downloading data when a item is clicked in a ListView but not when user traverses back from the fullscreen activity
You need to cache the Data so then you don't download the same. There are storage options other then sqlite.
Its upto you to decide who you implement the cache
http://developer.android.com/guide/topics/data/data-storage.html
Here's the implementation
public class ImageCache {
private static final String TAG = "ImageCache";
// Default memory cache size
private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 1024 * 5; // 5MB
// Default disk cache size
private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
// Compression settings when writing images to disk cache
private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG;
private static final int DEFAULT_COMPRESS_QUALITY = 70;
private static final int DISK_CACHE_INDEX = 0;
// Constants to easily toggle various caches
private static final boolean DEFAULT_MEM_CACHE_ENABLED = true;
private static final boolean DEFAULT_DISK_CACHE_ENABLED = true;
private static final boolean DEFAULT_CLEAR_DISK_CACHE_ON_START = false;
private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
private DiskLruCache mDiskLruCache;
private LruCache<String, Bitmap> mMemoryCache;
private ImageCacheParams mCacheParams;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
/**
* Creating a new ImageCache object using the specified parameters.
*
* #param cacheParams The cache parameters to use to initialize the cache
*/
public ImageCache(ImageCacheParams cacheParams) {
init(cacheParams);
}
/**
* Creating a new ImageCache object using the default parameters.
*
* #param context The context to use
* #param uniqueName A unique name that will be appended to the cache directory
*/
public ImageCache(Context context, String uniqueName) {
init(new ImageCacheParams(context, uniqueName));
}
/**
* Find and return an existing ImageCache stored in a {#link RetainFragment}, if not found a new
* one is created using the supplied params and saved to a {#link RetainFragment}.
*
* #param fragmentManager The fragment manager to use when dealing with the retained fragment.
* #param cacheParams The cache parameters to use if creating the ImageCache
* #return An existing retained ImageCache object or a new one if one did not exist
*/
public static ImageCache findOrCreateCache(
FragmentManager fragmentManager, ImageCacheParams cacheParams) {
// Search for, or create an instance of the non-UI RetainFragment
final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager);
// See if we already have an ImageCache stored in RetainFragment
ImageCache imageCache = (ImageCache) mRetainFragment.getObject();
// No existing ImageCache, create one and store it in RetainFragment
if (imageCache == null) {
imageCache = new ImageCache(cacheParams);
mRetainFragment.setObject(imageCache);
}
return imageCache;
}
/**
* Initialize the cache, providing all parameters.
*
* #param cacheParams The cache parameters to initialize the cache
*/
private void init(ImageCacheParams cacheParams) {
mCacheParams = cacheParams;
// Set up memory cache
if (mCacheParams.memoryCacheEnabled) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")");
}
mMemoryCache = new LruCache<String, Bitmap>(mCacheParams.memCacheSize) {
/**
* Measure item size in bytes rather than units which is more practical
* for a bitmap cache
*/
#Override
protected int sizeOf(String key, Bitmap bitmap) {
return getBitmapSize(bitmap);
}
};
}
// By default the disk cache is not initialized here as it should be initialized
// on a separate thread due to disk access.
if (cacheParams.initDiskCacheOnCreate) {
// Set up disk cache
initDiskCache();
}
}
/**
* Initializes the disk cache. Note that this includes disk access so this should not be
* executed on the main/UI thread. By default an ImageCache does not initialize the disk
* cache when it is created, instead you should call initDiskCache() to initialize it on a
* background thread.
*/
public void initDiskCache() {
// Set up disk cache
synchronized (mDiskCacheLock) {
if (mDiskLruCache == null || mDiskLruCache.isClosed()) {
File diskCacheDir = mCacheParams.diskCacheDir;
if (mCacheParams.diskCacheEnabled && diskCacheDir != null) {
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) {
try {
mDiskLruCache = DiskLruCache.open(
diskCacheDir, 1, 1, mCacheParams.diskCacheSize);
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache initialized");
}
} catch (final IOException e) {
mCacheParams.diskCacheDir = null;
Log.e(TAG, "initDiskCache - " + e);
}
}
}
}
mDiskCacheStarting = false;
mDiskCacheLock.notifyAll();
}
}
/**
* Adds a bitmap to both memory and disk cache.
* #param data Unique identifier for the bitmap to store
* #param bitmap The bitmap to store
*/
public void addBitmapToCache(String data, Bitmap bitmap) {
if (data == null || bitmap == null) {
return;
}
// Add to memory cache
if (mMemoryCache != null && mMemoryCache.get(data) == null) {
mMemoryCache.put(data, bitmap);
}
synchronized (mDiskCacheLock) {
// Add to disk cache
if (mDiskLruCache != null) {
final String key = hashKeyForDisk(data);
OutputStream out = null;
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot == null) {
final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
out = editor.newOutputStream(DISK_CACHE_INDEX);
bitmap.compress(
mCacheParams.compressFormat, mCacheParams.compressQuality, out);
editor.commit();
out.close();
}
} else {
snapshot.getInputStream(DISK_CACHE_INDEX).close();
}
} catch (final IOException e) {
Log.e(TAG, "addBitmapToCache - " + e);
} catch (Exception e) {
Log.e(TAG, "addBitmapToCache - " + e);
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {}
}
}
}
}
/**
* Get from memory cache.
*
* #param data Unique identifier for which item to get
* #return The bitmap if found in cache, null otherwise
*/
public Bitmap getBitmapFromMemCache(String data) {
if (mMemoryCache != null) {
final Bitmap memBitmap = mMemoryCache.get(data);
if (memBitmap != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache hit");
}
return memBitmap;
}
}
return null;
}
/**
* Get from disk cache.
*
* #param data Unique identifier for which item to get
* #return The bitmap if found in cache, null otherwise
*/
public Bitmap getBitmapFromDiskCache(String data) {
final String key = hashKeyForDisk(data);
synchronized (mDiskCacheLock) {
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
InputStream inputStream = null;
try {
final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache hit");
}
inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
if (inputStream != null) {
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
return bitmap;
}
}
} catch (final IOException e) {
Log.e(TAG, "getBitmapFromDiskCache - " + e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {}
}
}
return null;
}
}
/**
* Clears both the memory and disk cache associated with this ImageCache object. Note that
* this includes disk access so this should not be executed on the main/UI thread.
*/
public void clearCache() {
if (mMemoryCache != null) {
mMemoryCache.evictAll();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache cleared");
}
}
synchronized (mDiskCacheLock) {
mDiskCacheStarting = true;
if (mDiskLruCache != null && !mDiskLruCache.isClosed()) {
try {
mDiskLruCache.delete();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache cleared");
}
} catch (IOException e) {
Log.e(TAG, "clearCache - " + e);
}
mDiskLruCache = null;
initDiskCache();
}
}
}
/**
* Flushes the disk cache associated with this ImageCache object. Note that this includes
* disk access so this should not be executed on the main/UI thread.
*/
public void flush() {
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null) {
try {
mDiskLruCache.flush();
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache flushed");
}
} catch (IOException e) {
Log.e(TAG, "flush - " + e);
}
}
}
}
/**
* Closes the disk cache associated with this ImageCache object. Note that this includes
* disk access so this should not be executed on the main/UI thread.
*/
public void close() {
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null) {
try {
if (!mDiskLruCache.isClosed()) {
mDiskLruCache.close();
mDiskLruCache = null;
if (BuildConfig.DEBUG) {
Log.d(TAG, "Disk cache closed");
}
}
} catch (IOException e) {
Log.e(TAG, "close - " + e);
}
}
}
}
/**
* A holder class that contains cache parameters.
*/
public static class ImageCacheParams {
public int memCacheSize = DEFAULT_MEM_CACHE_SIZE;
public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE;
public File diskCacheDir;
public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
public int compressQuality = DEFAULT_COMPRESS_QUALITY;
public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED;
public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED;
public boolean clearDiskCacheOnStart = DEFAULT_CLEAR_DISK_CACHE_ON_START;
public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE;
public ImageCacheParams(Context context, String uniqueName) {
diskCacheDir = getDiskCacheDir(context, uniqueName);
}
public ImageCacheParams(File diskCacheDir) {
this.diskCacheDir = diskCacheDir;
}
/**
* Sets the memory cache size based on a percentage of the device memory class.
* Eg. setting percent to 0.2 would set the memory cache to one fifth of the device memory
* class. Throws {#link IllegalArgumentException} if percent is < 0.05 or > .8.
*
* This value should be chosen carefully based on a number of factors
* Refer to the corresponding Android Training class for more discussion:
* http://developer.android.com/training/displaying-bitmaps/
*
* #param context Context to use to fetch memory class
* #param percent Percent of memory class to use to size memory cache
*/
public void setMemCacheSizePercent(Context context, float percent) {
if (percent < 0.05f || percent > 0.8f) {
throw new IllegalArgumentException("setMemCacheSizePercent - percent must be "
+ "between 0.05 and 0.8 (inclusive)");
}
memCacheSize = Math.round(percent * getMemoryClass(context) * 1024 * 1024);
}
private static int getMemoryClass(Context context) {
return ((ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE)).getMemoryClass();
}
}
/**
* Get a usable cache directory (external if available, internal otherwise).
*
* #param context The context to use
* #param uniqueName A unique directory name to append to the cache dir
* #return The cache dir
*/
public static File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
/**
* A hashing method that changes a string (like a URL) into a hash suitable for using as a
* disk filename.
*/
public static String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
private static String bytesToHexString(byte[] bytes) {
// https://stackoverflow.com/questions/332079
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
/**
* Get the size in bytes of a bitmap.
* #param bitmap
* #return size in bytes
*/
#TargetApi(12)
public static int getBitmapSize(Bitmap bitmap) {
if (Utils.hasHoneycombMR1()) {
return bitmap.getByteCount();
}
// Pre HC-MR1
return bitmap.getRowBytes() * bitmap.getHeight();
}
/**
* Check if external storage is built-in or removable.
*
* #return True if external storage is removable (like an SD card), false
* otherwise.
*/
#TargetApi(9)
public static boolean isExternalStorageRemovable() {
if (Utils.hasGingerbread()) {
return Environment.isExternalStorageRemovable();
}
return true;
}
/**
* Get the external app cache directory.
*
* #param context The context to use
* #return The external cache dir
*/
#TargetApi(8)
public static File getExternalCacheDir(Context context) {
if (Utils.hasFroyo()) {
return context.getExternalCacheDir();
}
// Before Froyo we need to construct the external cache dir ourselves
final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
}
/**
* Check how much usable space is available at a given path.
*
* #param path The path to check
* #return The space available in bytes
*/
#TargetApi(9)
public static long getUsableSpace(File path) {
if (Utils.hasGingerbread()) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
/**
* Locate an existing instance of this Fragment or if not found, create and
* add it using FragmentManager.
*
* #param fm The FragmentManager manager to use.
* #return The existing instance of the Fragment or the new instance if just
* created.
*/
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
// Check to see if we have retained the worker fragment.
RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG);
// If not retained (or first time running), we need to create and add it.
if (mRetainFragment == null) {
mRetainFragment = new RetainFragment();
fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss();
}
return mRetainFragment;
}
/**
* A simple non-UI Fragment that stores a single Object and is retained over configuration
* changes. It will be used to retain the ImageCache object.
*/
public static class RetainFragment extends Fragment {
private Object mObject;
/**
* Empty constructor as per the Fragment documentation
*/
public RetainFragment() {}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Make sure this Fragment is retained over a configuration change
setRetainInstance(true);
}
/**
* Store a single object in this Fragment.
*
* #param object The object to store
*/
public void setObject(Object object) {
mObject = object;
}
/**
* Get the stored object.
*
* #return The stored object
*/
public Object getObject() {
return mObject;
}
}
}
There is an example in the docs
http://developer.android.com/training/displaying-bitmaps/index.html
Download the zip file DisplayingBitmaps.zip. The example has a implementation of cache for images.
http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
Using Universal Image Loader
Caching images and displaying
Edit:
BitmapFun sample snap shots.
Images are downloaded and cached
Detailed View
Now loading other images when scrolled. Images are not yet cached
With all the three its easy to understand.

Performance issue with Volley's DiskBasedCache

In my Photo Collage app for Android I'm using Volley for loading images.
I'm using the DiskBasedCache (included with volley) with 50 mb storage to prevent re-downloading the same images multiple times.
Last time I checked the DiskBasedCache contained about 1000 cache entries.
When my app starts Volley calls mCache.initialize() and it will spend about 10 seconds (!) on my Galaxy S4 to do the following:
List all files in cache folder
Open each and every file and read the header section.
I find that reading 1000+ files at startup is not a very efficient way to load the cache index! :-)
From volley/toolbox/DiskBasedCache.java:
#Override
public synchronized void initialize() {
if (!mRootDirectory.exists()) {
if (!mRootDirectory.mkdirs()) {
VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
}
return;
}
File[] files = mRootDirectory.listFiles();
if (files == null) {
return;
}
for (File file : files) {
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
CacheHeader entry = CacheHeader.readHeader(fis);
entry.size = file.length();
putEntry(entry.key, entry);
} catch (IOException e) {
if (file != null) {
file.delete();
}
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException ignored) { }
}
}
}
I'm looking for a fast and scalable solution. Perhaps an alternative DiskBasedCache implementation or suggestions on how to improve the volley library.
Update: (2014-01-06)
Noticing that the Volley cache used a lot of small (1 byte) IO read/writes. I cloned DiskBasedCache.java and encapsulating all FileInputStreams and FileOutputStreams with BufferedInputStream and BufferedOutputStreams. I found that that this optimization gave me a 3-10 times speed up.
This modification has a low risks of bugs compared to writing a new disk cache with a central index file.
Update: (2014-01-10)
Here is new class BufferedDiskBasedCache.java that I'm using now.
package no.ludde.android.ds.android.volley;
/*
* 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 android.os.SystemClock;
import com.android.volley.Cache;
import com.android.volley.VolleyLog;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Cache implementation that caches files directly onto the hard disk in the specified
* directory. The default disk usage size is 5MB, but is configurable.
*/
public class BufferedDiskBasedCache implements Cache {
/** Map of the Key, CacheHeader pairs */
private final Map<String, CacheHeader> mEntries =
new LinkedHashMap<String, CacheHeader>(16, .75f, true);
/** Total amount of space currently used by the cache in bytes. */
private long mTotalSize = 0;
/** The root directory to use for the cache. */
private final File mRootDirectory;
/** The maximum size of the cache in bytes. */
private final int mMaxCacheSizeInBytes;
/** Default maximum disk usage in bytes. */
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
/** High water mark percentage for the cache */
private static final float HYSTERESIS_FACTOR = 0.9f;
/** Magic number for current version of cache file format. */
private static final int CACHE_MAGIC = 0x20120504;
/**
* Constructs an instance of the DiskBasedCache at the specified directory.
* #param rootDirectory The root directory of the cache.
* #param maxCacheSizeInBytes The maximum size of the cache in bytes.
*/
public BufferedDiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}
/**
* Constructs an instance of the DiskBasedCache at the specified directory using
* the default maximum cache size of 5MB.
* #param rootDirectory The root directory of the cache.
*/
public BufferedDiskBasedCache(File rootDirectory) {
this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}
/**
* Clears the cache. Deletes all cached files from disk.
*/
#Override
public synchronized void clear() {
File[] files = mRootDirectory.listFiles();
if (files != null) {
for (File file : files) {
file.delete();
}
}
mEntries.clear();
mTotalSize = 0;
VolleyLog.d("Cache cleared.");
}
/**
* Returns the cache entry with the specified key if it exists, null otherwise.
*/
#Override
public synchronized Entry get(String key) {
CacheHeader entry = mEntries.get(key);
// if the entry does not exist, return.
if (entry == null) {
return null;
}
File file = getFileForKey(key);
CountingInputStream cis = null;
try {
cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
CacheHeader.readHeader(cis); // eat header
byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
return entry.toCacheEntry(data);
} catch (IOException e) {
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
remove(key);
return null;
} finally {
if (cis != null) {
try {
cis.close();
} catch (IOException ioe) {
return null;
}
}
}
}
/**
* Initializes the DiskBasedCache by scanning for all files currently in the
* specified root directory. Creates the root directory if necessary.
*/
#Override
public synchronized void initialize() {
if (!mRootDirectory.exists()) {
if (!mRootDirectory.mkdirs()) {
VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
}
return;
}
File[] files = mRootDirectory.listFiles();
if (files == null) {
return;
}
for (File file : files) {
BufferedInputStream fis = null;
try {
fis = new BufferedInputStream(new FileInputStream(file));
CacheHeader entry = CacheHeader.readHeader(fis);
entry.size = file.length();
putEntry(entry.key, entry);
} catch (IOException e) {
if (file != null) {
file.delete();
}
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException ignored) { }
}
}
}
/**
* Invalidates an entry in the cache.
* #param key Cache key
* #param fullExpire True to fully expire the entry, false to soft expire
*/
#Override
public synchronized void invalidate(String key, boolean fullExpire) {
Entry entry = get(key);
if (entry != null) {
entry.softTtl = 0;
if (fullExpire) {
entry.ttl = 0;
}
put(key, entry);
}
}
/**
* Puts the entry with the specified key into the cache.
*/
#Override
public synchronized void put(String key, Entry entry) {
pruneIfNeeded(entry.data.length);
File file = getFileForKey(key);
try {
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
CacheHeader e = new CacheHeader(key, entry);
e.writeHeader(fos);
fos.write(entry.data);
fos.close();
putEntry(key, e);
return;
} catch (IOException e) {
}
boolean deleted = file.delete();
if (!deleted) {
VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
}
}
/**
* Removes the specified key from the cache if it exists.
*/
#Override
public synchronized void remove(String key) {
boolean deleted = getFileForKey(key).delete();
removeEntry(key);
if (!deleted) {
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
key, getFilenameForKey(key));
}
}
/**
* Creates a pseudo-unique filename for the specified cache key.
* #param key The key to generate a file name for.
* #return A pseudo-unique filename.
*/
private String getFilenameForKey(String key) {
int firstHalfLength = key.length() / 2;
String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
return localFilename;
}
/**
* Returns a file object for the given cache key.
*/
public File getFileForKey(String key) {
return new File(mRootDirectory, getFilenameForKey(key));
}
/**
* Prunes the cache to fit the amount of bytes specified.
* #param neededSpace The amount of bytes we are trying to fit into the cache.
*/
private void pruneIfNeeded(int neededSpace) {
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
return;
}
if (VolleyLog.DEBUG) {
VolleyLog.v("Pruning old cache entries.");
}
long before = mTotalSize;
int prunedFiles = 0;
long startTime = SystemClock.elapsedRealtime();
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, CacheHeader> entry = iterator.next();
CacheHeader e = entry.getValue();
boolean deleted = getFileForKey(e.key).delete();
if (deleted) {
mTotalSize -= e.size;
} else {
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
e.key, getFilenameForKey(e.key));
}
iterator.remove();
prunedFiles++;
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
break;
}
}
if (VolleyLog.DEBUG) {
VolleyLog.v("pruned %d files, %d bytes, %d ms",
prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
}
}
/**
* Puts the entry with the specified key into the cache.
* #param key The key to identify the entry by.
* #param entry The entry to cache.
*/
private void putEntry(String key, CacheHeader entry) {
if (!mEntries.containsKey(key)) {
mTotalSize += entry.size;
} else {
CacheHeader oldEntry = mEntries.get(key);
mTotalSize += (entry.size - oldEntry.size);
}
mEntries.put(key, entry);
}
/**
* Removes the entry identified by 'key' from the cache.
*/
private void removeEntry(String key) {
CacheHeader entry = mEntries.get(key);
if (entry != null) {
mTotalSize -= entry.size;
mEntries.remove(key);
}
}
/**
* Reads the contents of an InputStream into a byte[].
* */
private static byte[] streamToBytes(InputStream in, int length) throws IOException {
byte[] bytes = new byte[length];
int count;
int pos = 0;
while (pos < length && ((count = in.read(bytes, pos, length - pos)) != -1)) {
pos += count;
}
if (pos != length) {
throw new IOException("Expected " + length + " bytes, read " + pos + " bytes");
}
return bytes;
}
/**
* Handles holding onto the cache headers for an entry.
*/
// Visible for testing.
static class CacheHeader {
/** The size of the data identified by this CacheHeader. (This is not
* serialized to disk. */
public long size;
/** The key that identifies the cache entry. */
public String key;
/** ETag for cache coherence. */
public String etag;
/** Date of this response as reported by the server. */
public long serverDate;
/** TTL for this record. */
public long ttl;
/** Soft TTL for this record. */
public long softTtl;
/** Headers from the response resulting in this cache entry. */
public Map<String, String> responseHeaders;
private CacheHeader() { }
/**
* Instantiates a new CacheHeader object
* #param key The key that identifies the cache entry
* #param entry The cache entry.
*/
public CacheHeader(String key, Entry entry) {
this.key = key;
this.size = entry.data.length;
this.etag = entry.etag;
this.serverDate = entry.serverDate;
this.ttl = entry.ttl;
this.softTtl = entry.softTtl;
this.responseHeaders = entry.responseHeaders;
}
/**
* Reads the header off of an InputStream and returns a CacheHeader object.
* #param is The InputStream to read from.
* #throws IOException
*/
public static CacheHeader readHeader(InputStream is) throws IOException {
CacheHeader entry = new CacheHeader();
int magic = readInt(is);
if (magic != CACHE_MAGIC) {
// don't bother deleting, it'll get pruned eventually
throw new IOException();
}
entry.key = readString(is);
entry.etag = readString(is);
if (entry.etag.equals("")) {
entry.etag = null;
}
entry.serverDate = readLong(is);
entry.ttl = readLong(is);
entry.softTtl = readLong(is);
entry.responseHeaders = readStringStringMap(is);
return entry;
}
/**
* Creates a cache entry for the specified data.
*/
public Entry toCacheEntry(byte[] data) {
Entry e = new Entry();
e.data = data;
e.etag = etag;
e.serverDate = serverDate;
e.ttl = ttl;
e.softTtl = softTtl;
e.responseHeaders = responseHeaders;
return e;
}
/**
* Writes the contents of this CacheHeader to the specified OutputStream.
*/
public boolean writeHeader(OutputStream os) {
try {
writeInt(os, CACHE_MAGIC);
writeString(os, key);
writeString(os, etag == null ? "" : etag);
writeLong(os, serverDate);
writeLong(os, ttl);
writeLong(os, softTtl);
writeStringStringMap(responseHeaders, os);
os.flush();
return true;
} catch (IOException e) {
VolleyLog.d("%s", e.toString());
return false;
}
}
}
private static class CountingInputStream extends FilterInputStream {
private int bytesRead = 0;
private CountingInputStream(InputStream in) {
super(in);
}
#Override
public int read() throws IOException {
int result = super.read();
if (result != -1) {
bytesRead++;
}
return result;
}
#Override
public int read(byte[] buffer, int offset, int count) throws IOException {
int result = super.read(buffer, offset, count);
if (result != -1) {
bytesRead += result;
}
return result;
}
}
/*
* Homebrewed simple serialization system used for reading and writing cache
* headers on disk. Once upon a time, this used the standard Java
* Object{Input,Output}Stream, but the default implementation relies heavily
* on reflection (even for standard types) and generates a ton of garbage.
*/
/**
* Simple wrapper around {#link InputStream#read()} that throws EOFException
* instead of returning -1.
*/
private static int read(InputStream is) throws IOException {
int b = is.read();
if (b == -1) {
throw new EOFException();
}
return b;
}
static void writeInt(OutputStream os, int n) throws IOException {
os.write((n >> 0) & 0xff);
os.write((n >> 8) & 0xff);
os.write((n >> 16) & 0xff);
os.write((n >> 24) & 0xff);
}
static int readInt(InputStream is) throws IOException {
int n = 0;
n |= (read(is) << 0);
n |= (read(is) << 8);
n |= (read(is) << 16);
n |= (read(is) << 24);
return n;
}
static void writeLong(OutputStream os, long n) throws IOException {
os.write((byte)(n >>> 0));
os.write((byte)(n >>> 8));
os.write((byte)(n >>> 16));
os.write((byte)(n >>> 24));
os.write((byte)(n >>> 32));
os.write((byte)(n >>> 40));
os.write((byte)(n >>> 48));
os.write((byte)(n >>> 56));
}
static long readLong(InputStream is) throws IOException {
long n = 0;
n |= ((read(is) & 0xFFL) << 0);
n |= ((read(is) & 0xFFL) << 8);
n |= ((read(is) & 0xFFL) << 16);
n |= ((read(is) & 0xFFL) << 24);
n |= ((read(is) & 0xFFL) << 32);
n |= ((read(is) & 0xFFL) << 40);
n |= ((read(is) & 0xFFL) << 48);
n |= ((read(is) & 0xFFL) << 56);
return n;
}
static void writeString(OutputStream os, String s) throws IOException {
byte[] b = s.getBytes("UTF-8");
writeLong(os, b.length);
os.write(b, 0, b.length);
}
static String readString(InputStream is) throws IOException {
int n = (int) readLong(is);
byte[] b = streamToBytes(is, n);
return new String(b, "UTF-8");
}
static void writeStringStringMap(Map<String, String> map, OutputStream os) throws IOException {
if (map != null) {
writeInt(os, map.size());
for (Map.Entry<String, String> entry : map.entrySet()) {
writeString(os, entry.getKey());
writeString(os, entry.getValue());
}
} else {
writeInt(os, 0);
}
}
static Map<String, String> readStringStringMap(InputStream is) throws IOException {
int size = readInt(is);
Map<String, String> result = (size == 0)
? Collections.<String, String>emptyMap()
: new HashMap<String, String>(size);
for (int i = 0; i < size; i++) {
String key = readString(is).intern();
String value = readString(is).intern();
result.put(key, value);
}
return result;
}
}
Yes, the way DiskBasedCache works it needs to open all the files in initialize(). Which is simply.... not a good idea :-(
You need to make a different implementation that doesent open all the files at startup.
Take a copy of DiskBasedCache and change initialize() to
#Override
public synchronized void initialize() {
if (!mRootDirectory.exists()) {
if (!mRootDirectory.mkdirs()) {
VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
}
}
}
And change get() so it makes an additional check for if the file exists on the file system, like
#Override
public synchronized Entry get(String key) {
CacheHeader entry = mEntries.get(key);
File file = getFileForKey(key);
if (entry == null && !file.exists()) { // EXTRA CHECK
// if the entry does not exist, return.
VolleyLog.d("DrVolleyDiskBasedCache miss for " + key);
return null;
}
...
I use this approach in https://play.google.com/store/apps/details?id=dk.dr.radio and it works fine - its robustness have been tested by ~300000 users :-)
You can download a full version of the file from https://code.google.com/p/dr-radio-android/source/browse/trunk/DRRadiov3/src/dk/dr/radio/net/volley/DrDiskBasedCache.java (you'll have to delete some DR Radio specific stuff)
In the streamToBytes(), first it will new bytes by the cache file length, does your cache file was too large than application maximum heap size ?
private static byte[] streamToBytes(InputStream in, int length) throws IOException {
byte[] bytes = new byte[length];
...
}
public synchronized Entry get(String key) {
CacheHeader entry = mEntries.get(key);
File file = getFileForKey(key);
byte[] data = streamToBytes(..., file.length());
}
If you want to clear the cache, you could keep the DiskBasedCache reference, after clear time's came, use ClearCacheRequest and pass that cache instance in :
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
DiskBasedCache cache = new DiskBasedCache(cacheDir);
RequestQueue queue = new RequestQueue(cache, network);
queue.start();
// clear all volley caches.
queue.add(new ClearCacheRequest(cache, null));
this way will clear all caches, so I suggest you use it carefully. of course, you can doing conditional check, just iterating the cacheDir files, estimate which was too large then remove it.
for (File cacheFile : cacheDir.listFiles()) {
if (cacheFile.isFile() && cacheFile.length() > 10000000) cacheFile.delete();
}
Volley wasn't design as a big data cache solution, it's common request cache, don't storing large data anytime.
------------- Update at 2014-07-17 -------------
In fact, clear all caches is final way, also isn't wise way, we should suppressing these large request use cache when we sure it would be, and if not sure? we still can determine the response data size whether large or not, then call setShouldCache(false) to disable it.
public class TheRequest extends Request {
#Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
// if response data was too large, disable caching is still time.
if (response.data.length > 10000) setShouldCache(false);
...
}
}
My initial thought was to use the DiskLruCache written by Jake Wharton by writing a com.android.volley.Cache wrapper over it.
But I finally implemented a singleton pattern for the Volley, combined with the cache creation in an AsyncTask called from the Application context
public static synchronized VolleyClient getInstance(Context context)
{
if (mInstance == null)
{
mInstance = new VolleyClient(context);
}
return mInstance;
}
private VolleyClient(Context context)
{
this.context = context;
VolleyCacheInitializer volleyCacheInitializer = new VolleyCacheInitializer();
volleyCacheInitializer.execute();
}
private class VolleyCacheInitializer extends AsyncTask<Void, Void, Boolean>
{
#Override
protected Boolean doInBackground(Void... params)
{
// Instantiate the cache with 50MB Cache Size
Cache diskBasedCache = new DiskBasedCache(context.getCacheDir(), 50 * 1024 * 1024);
// Instantiate the RequestQueue with the cache and network.
mRequestQueue = new RequestQueue(diskBasedCache, network);
// Start the queue which calls the DiskBasedCache.initialize()
mRequestQueue.start();
return true;
}
#Override
protected void onPostExecute(Boolean aBoolean)
{
super.onPostExecute(aBoolean);
if(aBoolean)
Log.d(TAG, "Volley request queue initialized");
else
Log.d(TAG, "Volley request queue initialization failed");
}
}
Inside MyApplication class
#Override
public void onCreate()
{
super.onCreate();
// Initialize an application level volley request queue
VolleyClient volleyHttpClient = VolleyClient.getInstance(this);
}

How to convert the WAV/OGG file to FLAC file in Android?

EDIT: Incorporated the changed uv001's answer.
I can only find that ICS 4.0 support decoding of FLAC, but encode. I need some encoder to convert wav to flac, but currenty I can't find it. I find there is a jFlac avaible , but I don't know how to use this library, just simply convert the files.
Could anyone give me a hand on it?
Today, I just some idea by myself, with using the JavaFlacEncoder.
and it works for certain bitrates of WAV.
I changed the value into a hard coding value in which it is working now.
/*
* Copyright (C) 2010 Preston Lacey http://javaflacencoder.sourceforge.net/
* All Rights Reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package javaFlacEncoder;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* FLAC_FileEncoder is a class to encode an input wav File to an output Flac
* file. It allows the EncodingConfiguration to be set only once, prior to
* encoding the entire File.
*
* #author Preston Lacey
* #author Bo Tan (Temple)
*/
public class FLAC_FileEncoder {
/** Maximum number of bytes to read from file at once */
private static final int MAX_READ = 16384;
/** Status enum for encode result */
public enum Status {
/** Unknown State. */
UNKNOWN,
/** Everything went well */
FULL_ENCODE,
/** Something unspecified went wrong...*/
GENERAL_ERROR,
/** internal error is something that went haywire that was discovered
* due to internal sanity checks. A problem in API. */
INTERNAL_ERROR,
/** File given was not able to be read */
UNSUPPORTED_FILE,
/** Generic file IO Error */
FILE_IO_ERROR,
/** Sample size unsupported */
UNSUPPORTED_SAMPLE_SIZE,
/** Error with output file */
OUTPUT_FILE_ERROR,
/** No errors found. */
OK
}
FLACEncoder flac = null;
StreamConfiguration sc = null;
EncodingConfiguration ec = null;
File outFile = null;
int lastTotalSamples = 0;
boolean useThreads;
/**
* Constructor creates a FLAC_FileEncoder object with default
* StreamConfiguration configuration and default EncodingConfiguration.
* Thread use defaults to true.
*/
public FLAC_FileEncoder() {
flac = new FLACEncoder();
sc = new StreamConfiguration();
ec = new EncodingConfiguration();
useThreads = true;
}
/**
* Specify whether to use multiple threads or not.
* #param val true to use threads, false otherwise.
*/
public void useThreads(boolean val) {
useThreads = val;
}
private void adjustConfigurations(){//(AudioFormat format) {
int sampleRate = 16000;//(int)format.getSampleRate();
int sampleSize = 16; //(int)format.getSampleSizeInBits();
int channels =1;// (int)format.getChannels();
//int blockSize = sc.getMaxBlockSize();
/*sc = new StreamConfiguration(channels, blockSize, blockSize,
sampleRate, sampleSize);*/
sc.setSampleRate(sampleRate);
sc.setBitsPerSample(sampleSize);
sc.setChannelCount(channels);
}
/**
* Set the stream configuration for this encoder to use. Note that the audio
* characteristics(number of channels, sample rate, and sample size), will
* be set to match the input file at encode time, so needn't be set in the
* given StreamConfiguration object.
*
* #param config StreamConfiguration to use for encoding.
*/
public void setStreamConfig(StreamConfiguration config) {sc = config; }
/**
* Set the EncodingConfiguration to use for encoding.
* #param config EncodingConfiguration to use.
*/
public void setEncodingConfig(EncodingConfiguration config){ec = config;}
private Status openStream() {
Status status = Status.OK;
boolean result = flac.setStreamConfiguration(sc);
result = result & flac.setEncodingConfiguration(ec);
if( !result)
status = Status.INTERNAL_ERROR;
else {
FLACFileOutputStream fout = null;
try {
fout = new FLACFileOutputStream(outFile.getPath());
} catch(IOException e) {
status = Status.OUTPUT_FILE_ERROR;
e.printStackTrace();
}
if( status == Status.OK) {
flac.setOutputStream(fout);
try {
flac.openFLACStream();
}catch(IOException e) {
status = Status.INTERNAL_ERROR;
}
}
else
status = Status.OUTPUT_FILE_ERROR;
}
return status;
}
/**
* Encode the given input wav file to an output file.
*
* #param inputFile Input wav file to encode.
* #param outputFile Output file to write FLAC stream to. If file exists, it
* will be overwritten without prompting.
*
* #return Status flag for encode
*/
public Status encode(File inputFile, File outputFile) {
Status status = Status.FULL_ENCODE;
this.outFile = outputFile;
//take file and initial configuration.
//open file
// AudioInputStream sin = null;
// AudioFormat format = null;
// //File inputFile = new File("encoderTest.wav");
// try {
// sin = AudioSystem.getAudioInputStream(inputFile);
// }catch(IOException e) {
// status = Status.FILE_IO_ERROR;
// }catch (UnsupportedAudioFileException e) {
// status = Status.UNSUPPORTED_FILE;
// }finally {
// if(status != Status.FULL_ENCODE)
// return status;
// }
FileInputStream sin=null;
try {
sin = new FileInputStream(inputFile);
} catch (FileNotFoundException e1) {
status = Status.FILE_IO_ERROR;
e1.printStackTrace();
}finally {
if (status != Status.FULL_ENCODE)
return status;
}
try {
// format = sin.getFormat();
//sanitize and optimize configurations
adjustConfigurations(); //adjustConfigurations(format);
//open stream
openStream();
int frameSize = 2;//format.getFrameSize();
int sampleSize = 16;//format.getSampleSizeInBits();
int bytesPerSample = sampleSize/8;
if(sampleSize %8 != 0) {
//end processing now
Exception newEx = new Exception(Status.UNSUPPORTED_SAMPLE_SIZE.name());
throw newEx;
}
int channels =1;// format.getChannels();
boolean bigEndian =false;// format.isBigEndian();
byte[] samplesIn = new byte[(int)MAX_READ];
int samplesRead;
int framesRead;
int[] sampleData = new int[MAX_READ*channels/frameSize];
int blockSize = sc.getMaxBlockSize();
int unencodedSamples = 0;
int totalSamples = 0;
while((samplesRead = sin.read(samplesIn, 0, MAX_READ)) != -1) {
//System.err.println("Read: " + read);
framesRead = samplesRead/(frameSize);
if(bigEndian) {
for(int i = 0; i < framesRead*channels; i++) {
int lower8Mask = 255;
int temp = 0;
int totalTemp = 0;
for(int x = bytesPerSample-1; x >= 0; x++) {
int upShift = 8*x;
if(x == 0)//don't mask...we want sign
temp = ((samplesIn[bytesPerSample*i+x]) << upShift);
else
temp = ((samplesIn[bytesPerSample*i+x] & lower8Mask) << upShift);
totalTemp = totalTemp | temp;
}
sampleData[i] = totalTemp;
}
}
else {
for(int i = 0; i < framesRead*channels; i++) {
int lower8Mask = 255;
int temp = 0;
int totalTemp = 0;
for(int x = 0; x < bytesPerSample; x++) {
int upShift = 8*x;
if(x == bytesPerSample-1)//don't mask...we want sign
temp = ((samplesIn[bytesPerSample*i+x]) << upShift);
else
temp = ((samplesIn[bytesPerSample*i+x] & lower8Mask) << upShift);
totalTemp = totalTemp | temp;
}
sampleData[i] = totalTemp;
}
}
if(framesRead > 0) {
flac.addSamples(sampleData, framesRead);
unencodedSamples += framesRead;
}
//if(unencodedSamples > blockSize*100) {
if(useThreads)//Thread.yield();//
unencodedSamples -= flac.t_encodeSamples(unencodedSamples, false, flac.getThreadCount());
else
unencodedSamples -= flac.encodeSamples(unencodedSamples, false);
totalSamples += unencodedSamples;
//unencodedSamples = 0;
//}
//System.err.println("read : "+ samplesRead);
}
totalSamples += unencodedSamples;
if(useThreads)
unencodedSamples -= flac.t_encodeSamples(unencodedSamples, true, flac.getThreadCount());
else
unencodedSamples -= flac.encodeSamples(unencodedSamples, true);
//unencodedSamples = 0;
lastTotalSamples = totalSamples;
}
catch(IOException e) {
status = Status.FILE_IO_ERROR;
}
catch(Exception e) {
status = Status.GENERAL_ERROR;
String message = e.getMessage();
if(message == null) {
e.printStackTrace();
}
else if(message.equals(Status.UNSUPPORTED_SAMPLE_SIZE.name()))
status = Status.UNSUPPORTED_SAMPLE_SIZE;
}
//System.err.print("LastTotalSamples: "+lastTotalSamples);
return status;
}
/**
* Get the total number of samples encoded in last encode. This is here
* primarily for use as a sanity check during debugging.
*
* #return Total number of samples encoded in last encode attempt.
*/
public int getLastTotalSamplesEncoded() {
return this.lastTotalSamples;
}
}
Temple, Thanks for the post and the response with the attached code for FLAC conversion. It works as you described with one minor issue.
The FLACFileOutputStream (fout) and the FileInputStream (sin) are not closed at the end of the function. Please update your post/code so others can benefit as well.
finally {
try {
if (sin != null) sin.close();
if (fout != null) fout.close();
} catch (IOException e) {}
}
PS: FLACFileOutputStream fout will need to be promoted to a class variable for this to work.
Change the following line in your code
unencodedSamples -= flac.t_encodeSamples(unencodedSamples, true);
to
unencodedSamples -= flac.t_encodeSamples(unencodedSamples, true,flac.getThreadCount());

Categories

Resources