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);
}
Related
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
I have made Android application that download zip file from server using Android Download Manager class and then uncompress the file and store that into SD card on pictures folder. On some of the phones.
The zip file is not downloading and download manager progress bar never show progress even if I keep it for hours. Whereas on other phones this works perfectly.
The file size is 40 MB. Is there any known limitation of Android Download Manager or in case of .zip files?
I have been using a variation of (using another class for unzipping, but since the issue here is related to downloading, i am suggesting it) this class for purposes of downloading (the file name retains specific implementation, but it is just a matter of renaming accordingly...). The work() method of this class can be called from within the run method of a Runnable object for parallel threading as inferred from the initial comment:
package com.package;
/*
This class is intended to download file filtering purpose and suffix from the server.
IMPORTANT:This is intended to be instantiated within a separate thread (i.e., != UI Thread)
*/
import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;
final class FileDownloader
{
// Declaring a the maximum buffer size
private static final int MAXIMUM_BUFFER_SIZE = 1024;
// Declaring static final byte fields for coding the status
protected static final byte ISDOWNLOADING = 0;
protected static final byte ERROROCCURRED = 1;
protected static final byte DOWNLOADISCOMPLETE = 2;
// Declaring a private URL field for storing the file for downloading
private java.net.URL url = null;
// Declaring a private int field for storing the file size in bytes
private int filesize;
// Declaring a private int field for storing the amount of downloaded bytes
private int bytesDownloaded;
// Declaring a private byte field for storing the current status of the download
private byte currentStatus;
// A private static final string for storing the server contents location
private static final String SERVER = "https://server.com/zipfiles/";
// Declaring a private field for storing the caller context, used for defining
// the path for saving files
private android.content.Context callerContext = null;
// The following rule is going to be applied for distributing purpose and their contents:
// 'purpose.x.zip' zip file to store the folders of the the x purpose_id and its inherent
// structure
private static final String PURPOSE= "purpose";
private String x = null;
private static final String SUFFIX = "zip";
// The remote file to be downloaded is going to be [stringed as]:
// SERVER + PURPOSE + "." + ((String.valueOf(x)).trim()) + "." + suffix
private String remoteFile = null;
// Defining a private static final File field for storing the purposes' contents within it.
// Specifically, this is being designed to be:
// java.io.File seekingRegisteredUserFolder =
// new java.io.File(callerContext.getFilesDir(), "RegisteredUser");
private final java.io.File seekingRegisteredUserFolder;
// The class constructor. The constructor depends on constructing elements for downloading
// the remoteFile respective to the element_ [cf. constructor parameter] under consideration,
// viz.:
protected FileDownloader(final String x_, final android.content.Context callerContext_)
throws
java.net.MalformedURLException,
java.io.FileNotFoundException,
java.lang.SecurityException
{
this.x = x_;
this.remoteFile = SERVER + PURPOSE + "." + ((String.valueOf(this.x)).trim()) + "." + SUFFIX;
int parsedW = 0;
try
{
parsedW = Integer.parseInt(x_);
}
catch (Exception throwableThrownParsingW)
{
throw new java.net.MalformedURLException();
}
// Implementation specific
if (parsedW < 1)
{
throw new java.net.MalformedURLException();
}
this.callerContext = callerContext_;
this.seekingRegisteredUserFolder = new java.io.File((this.callerContext).getFilesDir(), "RegisteredUser");
if (!((this.seekingRegisteredUserFolder).exists()))
{
throw new java.io.FileNotFoundException();
}
this.url = new java.net.URL(this.remoteFile);
this.filesize = -1;
this.bytesDownloaded = 0;
this.currentStatus = ISDOWNLOADING;
}
// Begins the file download. This is to be called under an object of this class instantiation
boolean work()
{
final java.io.RandomAccessFile[] randomAccessFile = {null};
final java.io.InputStream[] inputStream = {null};
final java.io.File[] purpose = {null};
try
{
purpose[0] = new java.io.File(seekingRegisteredUserFolder, (PURPOSE + "." + x + "." + SUFFIX));
// Opens a connection to the URL via ssl
final javax.net.ssl.HttpsURLConnection[] connection = {null};
connection[0] = (javax.net.ssl.HttpsURLConnection) url.openConnection();
// Defines the file part to download
connection[0].setRequestProperty("Range", "bytes=" + bytesDownloaded + "-");
// Connects to the server
connection[0].connect();
// The response code must be within the 200 range
if ((connection[0].getResponseCode() / 100) != 2)
{
currentStatus = ERROROCCURRED;
}
// Inferring the validity of the content size
final int[] contentLength = {0};
contentLength[0] = connection[0].getContentLength();
if (contentLength[0] < 1)
{
currentStatus = ERROROCCURRED;
}
// Configuring the download size, case not yet configured
if (filesize == -1)
{
filesize = contentLength[0];
}
// Opens the file, seeking its final
randomAccessFile[0] = new java.io.RandomAccessFile(purpose[0], "rw");
randomAccessFile[0].seek(bytesDownloaded);
inputStream[0] = connection[0].getInputStream();
while (currentStatus == ISDOWNLOADING)
{
// Defines the buffer according to the left amount of file to complete
byte[] byteBuffer = null;
if ((filesize - bytesDownloaded) > MAXIMUM_BUFFER_SIZE)
{
byteBuffer = new byte[MAXIMUM_BUFFER_SIZE];
}
else
{
byteBuffer = new byte[filesize - bytesDownloaded];
}
// Reads from server to the buffer
int read = inputStream[0].read(byteBuffer);
if (read == -1)
{
break;
}
// Writes from buffer to file
randomAccessFile[0].write(byteBuffer, 0, read);
bytesDownloaded += read;
}
// Changing the status for complete since this point of code has been reached
if (currentStatus == ISDOWNLOADING)
{
currentStatus = DOWNLOADISCOMPLETE;
}
}
catch (java.lang.Exception connectionException)
{
currentStatus = ERROROCCURRED;
}
finally
{
// Closes the [RandomAccessFile] file
if (randomAccessFile[0] != null)
{
try
{
randomAccessFile[0].close();
}
catch (java.lang.Exception closingFileException)
{
currentStatus = ERROROCCURRED;
}
}
if (inputStream[0] != null)
{
try
{
inputStream[0].close();
}
catch (java.lang.Exception closingConnectionException)
{
currentStatus = ERROROCCURRED;
}
}
}
if ((currentStatus == DOWNLOADISCOMPLETE) && (purpose[0] != null) &&
(purpose[0]).isFile() && (purpose[0].length() > 0) && (purpose[0].length() == filesize))
{
((AppCompatActivity) callerContext).runOnUiThread
(
new Runnable()
{
#Override
public final void run()
{
Toast.makeText(callerContext, "Downloaded: " + remoteFile.substring(remoteFile.indexOf(SERVER) + SERVER.length()), Toast.LENGTH_LONG).show();
}
}
);
return true;
}
return false;
}
}
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.
I am using phonegap with socket programming.
When I check on emulator I am getting VM aborting error.
I am just calling my server and checking it to my emulator.
Here is my code.
package com.example.test;
import org.apache.cordova.DroidGap;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
public class MainActivity extends DroidGap {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_main);
super.loadUrl("file:///android_asset/www/index.html");
appView.addJavascriptInterface(new WebSocketFactory(appView), "WebSocketFactory");
}
}
---------------------------------------------------------------------
package com.example.test;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.NotYetConnectedException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import android.util.Log;
import android.webkit.WebView;
public class WebSocket implements Runnable {
/**
* Enum for WebSocket Draft
*/
public enum Draft {
DRAFT75, DRAFT76
}
// //////////////// CONSTANT
/**
* The connection has not yet been established.
*/
public final static int WEBSOCKET_STATE_CONNECTING = 0;
/**
* The WebSocket connection is established and communication is possible.
*/
public final static int WEBSOCKET_STATE_OPEN = 1;
/**
* The connection is going through the closing handshake.
*/
public final static int WEBSOCKET_STATE_CLOSING = 2;
/**
* The connection has been closed or could not be opened.
*/
public final static int WEBSOCKET_STATE_CLOSED = 3;
/**
* An empty string
*/
private static String BLANK_MESSAGE = "";
/**
* The javascript method name for onOpen event.
*/
private static String EVENT_ON_OPEN = "onopen";
/**
* The javascript method name for onMessage event.
*/
private static String EVENT_ON_MESSAGE = "onmessage";
/**
* The javascript method name for onClose event.
*/
private static String EVENT_ON_CLOSE = "onclose";
/**
* The javascript method name for onError event.
*/
private static String EVENT_ON_ERROR = "onerror";
/**
* The default port of WebSockets, as defined in the spec.
*/
public static final int DEFAULT_PORT = 80;
/**
* The WebSocket protocol expects UTF-8 encoded bytes.
*/
public static final String UTF8_CHARSET = "UTF-8";
/**
* The byte representing Carriage Return, or \r
*/
public static final byte DATA_CR = (byte) 0x0D;
/**
* The byte representing Line Feed, or \n
*/
public static final byte DATA_LF = (byte) 0x0A;
/**
* The byte representing the beginning of a WebSocket text frame.
*/
public static final byte DATA_START_OF_FRAME = (byte) 0x00;
/**
* The byte representing the end of a WebSocket text frame.
*/
public static final byte DATA_END_OF_FRAME = (byte) 0xFF;
// //////////////// INSTANCE Variables
/**
* The WebView instance from Phonegap DroidGap
*/
private final WebView appView;
/**
* The unique id for this instance (helps to bind this to javascript events)
*/
private String id;
/**
* The URI this client is supposed to connect to.
*/
private URI uri;
/**
* The port of the websocket server
*/
private int port;
/**
* The Draft of the WebSocket protocol the Client is adhering to.
*/
private Draft draft;
/**
* The <tt>SocketChannel</tt> instance to use for this server connection.
* This is used to read and write data to.
*/
private SocketChannel socketChannel;
/**
* The 'Selector' used to get event keys from the underlying socket.
*/
private Selector selector;
/**
* Keeps track of whether or not the client thread should continue running.
*/
private boolean running;
/**
* Internally used to determine whether to recieve data as part of the
* remote handshake, or as part of a text frame.
*/
private boolean handshakeComplete;
/**
* The 1-byte buffer reused throughout the WebSocket connection to read
* data.
*/
private ByteBuffer buffer;
/**
* The bytes that make up the remote handshake.
*/
private ByteBuffer remoteHandshake;
/**
* The bytes that make up the current text frame being read.
*/
private ByteBuffer currentFrame;
/**
* Queue of buffers that need to be sent to the client.
*/
private BlockingQueue<ByteBuffer> bufferQueue;
/**
* Lock object to ensure that data is sent from the bufferQueue in the
* proper order
*/
private Object bufferQueueMutex = new Object();
/**
* Number 1 used in handshake
*/
private int number1 = 0;
/**
* Number 2 used in handshake
*/
private int number2 = 0;
/**
* Key3 used in handshake
*/
private byte[] key3 = null;
/**
* The readyState attribute represents the state of the connection.
*/
private int readyState = WEBSOCKET_STATE_CONNECTING;
private final WebSocket instance;
/**
* Constructor.
*
* Note: this is protected because it's supposed to be instantiated from {#link WebSocketFactory} only.
*
* #param appView
* {#link android.webkit.WebView}
* #param uri
* websocket server {#link URI}
* #param draft
* websocket server {#link Draft} implementation (75/76)
* #param id
* unique id for this instance
*/
protected WebSocket(WebView appView, URI uri, Draft draft, String id) {
this.appView = appView;
this.uri = uri;
this.draft = draft;
// port
port = uri.getPort();
if (port == -1) {
port = DEFAULT_PORT;
}
// Id
this.id = id;
this.bufferQueue = new LinkedBlockingQueue<ByteBuffer>();
this.handshakeComplete = false;
this.remoteHandshake = this.currentFrame = null;
this.buffer = ByteBuffer.allocate(1);
this.instance = this;
}
// //////////////////////////////////////////////////////////////////////////////////////
// /////////////////////////// WEB SOCKET API Methods
// ///////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////
/**
* Starts a new Thread and connects to server
*
* #throws IOException
*/
public Thread connect() throws IOException {
this.running = true;
this.readyState = WEBSOCKET_STATE_CONNECTING;
// open socket
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
// set address
socketChannel.connect(new InetSocketAddress(uri.getHost(), port));
// start a thread to make connection
// More info:
// http://groups.google.com/group/android-developers/browse_thread/thread/45a8b53e9bf60d82
// http://stackoverflow.com/questions/2879455/android-2-2-and-bad-address-family-on-socket-connect
System.setProperty("java.net.preferIPv4Stack", "true");
System.setProperty("java.net.preferIPv6Addresses", "false");
selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT);
Log.v("websocket", "Starting a new thread to manage data reading/writing");
Thread th = new Thread(this);
th.start();
// return thread object for explicit closing, if needed
return th;
}
public void run() {
while (this.running) {
try {
_connect();
} catch (IOException e) {
this.onError(e);
}
}
}
/**
* Closes connection with server
*/
public void close() {
this.readyState = WebSocket.WEBSOCKET_STATE_CLOSING;
// close socket channel
try {
this.socketChannel.close();
} catch (IOException e) {
this.onError(e);
}
this.running = false;
selector.wakeup();
// fire onClose method
this.onClose();
this.readyState = WebSocket.WEBSOCKET_STATE_CLOSED;
}
/**
* Sends <var>text</var> to server
*
* #param text
* String to send to server
*/
public void send(final String text) {
new Thread(new Runnable() {
#Override
public void run() {
if (instance.readyState == WEBSOCKET_STATE_OPEN) {
try {
instance._send(text);
} catch (IOException e) {
instance.onError(e);
}
} else {
instance.onError(new NotYetConnectedException());
}
}
}).start();
}
/**
* Called when an entire text frame has been received.
*
* #param msg
* Message from websocket server
*/
public void onMessage(final String msg) {
appView.post(new Runnable() {
#Override
public void run() {
appView.loadUrl(buildJavaScriptData(EVENT_ON_MESSAGE, msg));
}
});
}
public void onOpen() {
appView.post(new Runnable() {
#Override
public void run() {
appView.loadUrl(buildJavaScriptData(EVENT_ON_OPEN, BLANK_MESSAGE));
}
});
}
public void onClose() {
appView.post(new Runnable() {
#Override
public void run() {
appView.loadUrl(buildJavaScriptData(EVENT_ON_CLOSE, BLANK_MESSAGE));
}
});
}
public void onError(final Throwable t) {
appView.post(new Runnable() {
#Override
public void run() {
appView.loadUrl(buildJavaScriptData(EVENT_ON_ERROR, t.getMessage()));
}
});
}
public String getId() {
return id;
}
/**
* #return the readyState
*/
public int getReadyState() {
return readyState;
}
/**
* Builds text for javascript engine to invoke proper event method with
* proper data.
*
* #param event
* websocket event (onOpen, onMessage etc.)
* #param msg
* Text message received from websocket server
* #return
*/
private String buildJavaScriptData(String event, String msg) {
String _d = "javascript:WebSocket." + event + "(" + "{" + "\"_target\":\"" + id + "\"," + "\"data\":'" + msg.replaceAll("'", "\\\\'")
+ "'" + "}" + ")";
return _d;
}
// //////////////////////////////////////////////////////////////////////////////////////
// /////////////////////////// WEB SOCKET Internal Methods
// //////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////
private boolean _send(String text) throws IOException {
if (!this.handshakeComplete) {
throw new NotYetConnectedException();
}
if (text == null) {
throw new NullPointerException("Cannot send 'null' data to a WebSocket.");
}
// Get 'text' into a WebSocket "frame" of bytes
byte[] textBytes = text.getBytes(UTF8_CHARSET.toString());
ByteBuffer b = ByteBuffer.allocate(textBytes.length + 2);
b.put(DATA_START_OF_FRAME);
b.put(textBytes);
b.put(DATA_END_OF_FRAME);
b.rewind();
// See if we have any backlog that needs to be sent first
if (_write()) {
// Write the ByteBuffer to the socket
this.socketChannel.write(b);
}
// If we didn't get it all sent, add it to the buffer of buffers
if (b.remaining() > 0) {
if (!this.bufferQueue.offer(b)) {
throw new IOException("Buffers are full, message could not be sent to"
+ this.socketChannel.socket().getRemoteSocketAddress());
}
return false;
}
return true;
}
// actual connection logic
private void _connect() throws IOException {
// Continuous loop that is only supposed to end when "close" is called.
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> i = keys.iterator();
while (i.hasNext()) {
SelectionKey key = i.next();
i.remove();
if (key.isConnectable()) {
if (socketChannel.isConnectionPending()) {
socketChannel.finishConnect();
}
socketChannel.register(selector, SelectionKey.OP_READ);
_writeHandshake();
}
if (key.isReadable()) {
try {
_read();
} catch (NoSuchAlgorithmException nsa) {
this.onError(nsa);
}
}
}
}
private void _writeHandshake() throws IOException {
String path = this.uri.getPath();
if (path.indexOf("/") != 0) {
path = "/" + path;
}
String host = uri.getHost() + (port != DEFAULT_PORT ? ":" + port : "");
String origin = "*"; // TODO: Make 'origin' configurable
String request = "GET " + path + " HTTP/1.1\r\n" + "Upgrade: WebSocket\r\n" + "Connection: Upgrade\r\n"
+ "Host: " + host + "\r\n" + "Origin: " + origin + "\r\n";
// Add random keys for Draft76
if (this.draft == Draft.DRAFT76) {
request += "Sec-WebSocket-Key1: " + this._randomKey() + "\r\n";
request += "Sec-WebSocket-Key2: " + this._randomKey() + "\r\n";
request += "\r\n";
this.key3 = new byte[8];
(new Random()).nextBytes(this.key3);
// Convert to bytes early so last eight bytes don't get jacked
byte[] bRequest = request.getBytes(UTF8_CHARSET);
byte[] bToSend = new byte[bRequest.length + 8];
// Copy in the Request bytes
System.arraycopy(bRequest, 0, bToSend, 0, bRequest.length);
// Now tack on key3 bytes
System.arraycopy(this.key3, 0, bToSend, bRequest.length, this.key3.length);
// Now we can send all keys as a single frame
_write(bToSend);
return;
}
request += "\r\n";
_write(request.getBytes(UTF8_CHARSET));
}
private boolean _write() throws IOException {
synchronized (this.bufferQueueMutex) {
ByteBuffer buffer = this.bufferQueue.peek();
while (buffer != null) {
this.socketChannel.write(buffer);
if (buffer.remaining() > 0) {
return false; // Didn't finish this buffer. There's more to
// send.
} else {
this.bufferQueue.poll(); // Buffer finished. Remove it.
buffer = this.bufferQueue.peek();
}
}
return true;
}
}
private void _write(byte[] bytes) throws IOException {
this.socketChannel.write(ByteBuffer.wrap(bytes));
}
private void _read() throws IOException, NoSuchAlgorithmException {
this.buffer.rewind();
int bytesRead = -1;
try {
bytesRead = this.socketChannel.read(this.buffer);
} catch (Exception ex) {
}
if (bytesRead == -1) {
close();
} else if (bytesRead > 0) {
this.buffer.rewind();
if (!this.handshakeComplete) {
_readHandshake();
} else {
_readFrame();
}
}
}
private void _readFrame() throws UnsupportedEncodingException {
byte newestByte = this.buffer.get();
if (newestByte == DATA_START_OF_FRAME) { // Beginning of Frame
this.currentFrame = null;
} else if (newestByte == DATA_END_OF_FRAME) { // End of Frame
String textFrame = null;
// currentFrame will be null if END_OF_FRAME was send directly after
// START_OF_FRAME, thus we will send 'null' as the sent message.
if (this.currentFrame != null) {
textFrame = new String(this.currentFrame.array(), "ISO-8859-1");
}
// fire onMessage method
this.onMessage(textFrame);
} else { // Regular frame data, add to current frame buffer
ByteBuffer frame = ByteBuffer.allocate((this.currentFrame != null ? this.currentFrame.capacity() : 0)
+ this.buffer.capacity());
if (this.currentFrame != null) {
this.currentFrame.rewind();
frame.put(this.currentFrame);
}
frame.put(newestByte);
this.currentFrame = frame;
String textFrame = new String(this.currentFrame.array(),"ISO-8859-1");
this.onMessage(textFrame);
}
}
private void _readHandshake() throws IOException, NoSuchAlgorithmException {
ByteBuffer ch = ByteBuffer.allocate((this.remoteHandshake != null ? this.remoteHandshake.capacity() : 0)
+ this.buffer.capacity());
if (this.remoteHandshake != null) {
this.remoteHandshake.rewind();
ch.put(this.remoteHandshake);
}
ch.put(this.buffer);
this.remoteHandshake = ch;
byte[] h = this.remoteHandshake.array();
// If the ByteBuffer contains 16 random bytes, and ends with
// 0x0D 0x0A 0x0D 0x0A (or two CRLFs), then the client
// handshake is complete for Draft 76 Client.
if ((h.length >= 20 && h[h.length - 20] == DATA_CR && h[h.length - 19] == DATA_LF
&& h[h.length - 18] == DATA_CR && h[h.length - 17] == DATA_LF)) {
_readHandshake(new byte[] { h[h.length - 16], h[h.length - 15], h[h.length - 14], h[h.length - 13],
h[h.length - 12], h[h.length - 11], h[h.length - 10], h[h.length - 9], h[h.length - 8],
h[h.length - 7], h[h.length - 6], h[h.length - 5], h[h.length - 4], h[h.length - 3],
h[h.length - 2], h[h.length - 1] });
// If the ByteBuffer contains 8 random bytes,ends with
// 0x0D 0x0A 0x0D 0x0A (or two CRLFs), and the response
// contains Sec-WebSocket-Key1 then the client
// handshake is complete for Draft 76 Server.
} else if ((h.length >= 12 && h[h.length - 12] == DATA_CR && h[h.length - 11] == DATA_LF
&& h[h.length - 10] == DATA_CR && h[h.length - 9] == DATA_LF)
&& new String(this.remoteHandshake.array(), UTF8_CHARSET).contains("Sec-WebSocket-Key1")) {// ************************
_readHandshake(new byte[] { h[h.length - 8], h[h.length - 7], h[h.length - 6], h[h.length - 5],
h[h.length - 4], h[h.length - 3], h[h.length - 2], h[h.length - 1] });
// Consider Draft 75, and the Flash Security Policy
// Request edge-case.
} else if ((h.length >= 4 && h[h.length - 4] == DATA_CR && h[h.length - 3] == DATA_LF
&& h[h.length - 2] == DATA_CR && h[h.length - 1] == DATA_LF)
&& !(new String(this.remoteHandshake.array(), UTF8_CHARSET).contains("Sec"))
|| (h.length == 23 && h[h.length - 1] == 0)) {
_readHandshake(null);
}else{
_readHandshake(null);
}
}
private void _readHandshake(byte[] handShakeBody) throws IOException, NoSuchAlgorithmException {
// byte[] handshakeBytes = this.remoteHandshake.array();
// String handshake = new String(handshakeBytes, UTF8_CHARSET);
// TODO: Do some parsing of the returned handshake, and close connection
// in received anything unexpected!
this.handshakeComplete = true;
boolean isConnectionReady = true;
if (this.draft == WebSocket.Draft.DRAFT76) {
if (handShakeBody == null) {
isConnectionReady = true;
} else{
byte[] challenge = new byte[] { (byte) (this.number1 >> 24), (byte) ((this.number1 << 8) >> 24),
(byte) ((this.number1 << 16) >> 24), (byte) ((this.number1 << 24) >> 24),
(byte) (this.number2 >> 24), (byte) ((this.number2 << 8) >> 24),
(byte) ((this.number2 << 16) >> 24), (byte) ((this.number2 << 24) >> 24), this.key3[0],
this.key3[1], this.key3[2], this.key3[3], this.key3[4], this.key3[5], this.key3[6], this.key3[7] };
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] expected = md5.digest(challenge);
for (int i = 0; i < handShakeBody.length; i++) {
if (expected[i] != handShakeBody[i]) {
isConnectionReady = true;
}
}
}
}
if (isConnectionReady) {
this.readyState = WEBSOCKET_STATE_OPEN;
// fire onOpen method
this.onOpen();
} else {
close();
}
}
private String _randomKey() {
Random r = new Random();
long maxNumber = 4294967295L;
long spaces = r.nextInt(12) + 1;
int max = new Long(maxNumber / spaces).intValue();
max = Math.abs(max);
int number = r.nextInt(max) + 1;
if (this.number1 == 0) {
this.number1 = number;
} else {
this.number2 = number;
}
long product = number * spaces;
String key = Long.toString(product);
int numChars = r.nextInt(12);
for (int i = 0; i < numChars; i++) {
int position = r.nextInt(key.length());
position = Math.abs(position);
char randChar = (char) (r.nextInt(95) + 33);
// exclude numbers here
if (randChar >= 48 && randChar <= 57) {
randChar -= 15;
}
key = new StringBuilder(key).insert(position, randChar).toString();
}
for (int i = 0; i < spaces; i++) {
int position = r.nextInt(key.length() - 1) + 1;
position = Math.abs(position);
key = new StringBuilder(key).insert(position, "\u0020").toString();
}
return key;
}
}
----------------------------------------------------------------------------
package com.example.test;
import java.net.URI;
import java.util.Random;
import android.webkit.WebView;
/**
* The <tt>WebSocketFactory</tt> is like a helper class to instantiate new
* WebSocket instaces especially from Javascript side. It expects a valid
* "ws://" URI.
*
* #author Animesh Kumar
*/
public class WebSocketFactory {
/** The app view. */
WebView appView;
/**
* Instantiates a new web socket factory.
*
* #param appView
* the app view
*/
public WebSocketFactory(WebView appView) {
this.appView = appView;
}
public WebSocket getInstance(String url) {
// use Draft75 by default
return getInstance(url, WebSocket.Draft.DRAFT76);
}
public WebSocket getInstance(String url, WebSocket.Draft draft) {
WebSocket socket = null;
Thread th = null;
try {
socket = new WebSocket(appView, new URI(url), draft, getRandonUniqueId());
th = socket.connect();
return socket;
} catch (Exception e) {
//Log.v("websocket", e.toString());
if(th != null) {
th.interrupt();
}
}
return null;
}
/**
* Generates random unique ids for WebSocket instances
*
* #return String
*/
private String getRandonUniqueId() {
return "WEBSOCKET." + new Random().nextInt(100);
}
}
-------------------------------------------------------------
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="format-detection" content="telephone=no" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />
<link rel="stylesheet" type="text/css" href="css/index.css" />
<title>Hello World</title>
</head>
<body>
<script type="text/javascript" src="cordova-2.7.0.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript" src="js/websocket.js"></script>
<script type="text/javascript">
var socket = new WebSocket('ws://192.168.1.3:8101/');
// push a message after the connection is established.
socket.onopen = function() {
socket.send('Hello World')
};
// alerts message pushed from server
socket.onmessage = function(msg) {
console.log("TTTTT"+JSON.stringify(msg));
};
// alert close event
socket.onclose = function() {
alert('closed');
};
</script>
</body>
</html>
From the script I am calling my functions.
But when I check it on emulator it is showing that error.
Please help me removing this error.
Thanks.
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());