/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* This file is part of FileExplorer.
*
* FileExplorer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FileExplorer 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with SwiFTP. If not, see <http://www.gnu.org/licenses/>.
*/
package net.micode.fileexplorer;
import java.lang.ref.SoftReference;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import net.micode.fileexplorer.FileCategoryHelper.FileCategory;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.Handler.Callback;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
import android.provider.MediaStore.Files.FileColumns;
import android.util.Log;
import android.widget.ImageView;
/**
* Asynchronously loads file icons and thumbnail, mostly single-threaded.
*/
public class FileIconLoader implements Callback {
private static final String LOADER_THREAD_NAME = "FileIconLoader";
/**
* Type of message sent by the UI thread to itself to indicate that some
* photos need to be loaded.
*/
private static final int MESSAGE_REQUEST_LOADING = 1;
/**
* Type of message sent by the loader thread to indicate that some photos
* have been loaded.
*/
private static final int MESSAGE_ICON_LOADED = 2;
private static abstract class ImageHolder {
public static final int NEEDED = 0;
public static final int LOADING = 1;
public static final int LOADED = 2;
int state;
public static ImageHolder create(FileCategory cate) {
switch (cate) {
case Apk:
return new DrawableHolder();
case Picture:
case Video:
return new BitmapHolder();
}
return null;
};
public abstract boolean setImageView(ImageView v);
public abstract boolean isNull();
public abstract void setImage(Object image);
}
private static class BitmapHolder extends ImageHolder {
SoftReference<Bitmap> bitmapRef;
@Override
public boolean setImageView(ImageView v) {
if (bitmapRef.get() == null)
return false;
v.setImageBitmap(bitmapRef.get());
return true;
}
@Override
public boolean isNull() {
return bitmapRef == null;
}
@Override
public void setImage(Object image) {
bitmapRef = image == null ? null : new SoftReference<Bitmap>((Bitmap) image);
}
}
private static class DrawableHolder extends ImageHolder {
SoftReference<Drawable> drawableRef;
@Override
public boolean setImageView(ImageView v) {
if (drawableRef.get() == null)
return false;
v.setImageDrawable(drawableRef.get());
return true;
}
@Override
public boolean isNull() {
return drawableRef == null;
}
@Override
public void setImage(Object image) {
drawableRef = image == null ? null : new SoftReference<Drawable>((Drawable) image);
}
}
/**
* A soft cache for image thumbnails. the key is file path
*/
private final static ConcurrentHashMap<String, ImageHolder> mImageCache = new ConcurrentHashMap<String, ImageHolder>();
/**
* A map from ImageView to the corresponding photo ID. Please note that this
* photo ID may change before the photo loading request is started.
*/
private final ConcurrentHashMap<ImageView, FileId> mPendingRequests = new ConcurrentHashMap<ImageView, FileId>();
/**
* Handler for messages sent to the UI thread.
*/
private final Handler mMainThreadHandler = new Handler(this);
/**
* Thread responsible for loading photos from the database. Created upon the
* first request.
*/
private LoaderThread mLoaderThread;
/**
* A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at
* a time.
*/
private boolean mLoadingRequested;
/**
* Flag indicating if the image loading is paused.
*/
private boolean mPaused;
private final Context mContext;
private IconLoadFinishListener iconLoadListener;
/**
* Constructor.
*
* @param context content context
*/
public FileIconLoader(Context context, IconLoadFinishListener l) {
mContext = context;
iconLoadListener = l;
}
public static class FileId {
public String mPath;
public long mId; // database id
public FileCategory mCategory;
public FileId(String path, long id, FileCategory cate) {
mPath = path;
mId = id;
mCategory = cate;
}
}
public abstract static interface IconLoadFinishListener {
void onIconLoadFinished(ImageView view);
}
/**
* Load photo into the supplied image view. If the photo is already cached,
* it is displayed immediately. Otherwise a request is sent to load the
* photo from the database.
*
* @param id, database id
*/
public boolean loadIcon(ImageView view, String path, long id, FileCategory cate) {
boolean loaded = loadCachedIcon(view, path, cate);
if (loaded) {
mPendingRequests.remove(view);
} else {
FileId p = new FileId(path, id, cate);
mPendingRequests.put(view, p);
if (!mPaused) {
// Send a request to start loading photos
requestLoading();
}
}
return loaded;
}
public void cancelRequest(ImageView view) {
mPendingRequests.remove(view);
}
/**
* Checks if the photo is present in cache. If so, sets the photo on the
* view, otherwise sets the state of the photo to
* {@link net.micode.fileexplorer.FileIconLoader.BitmapHolder#NEEDED}
*/
private boolean loadCachedIcon(ImageView view, String path, FileCategory cate) {
ImageHolder holder = mImageCache.get(path);
if (holder == null) {
holder = ImageHolder.create(cate);
if (holder == null)
return false;
mImageCache.put(path, holder);
} else if (holder.state == ImageHolder.LOADED) {
if (holder.isNull()) {
return true;
}
// failing to set imageview means that the soft reference was
// released by the GC, we need to reload the photo.
if (holder.setImageView(view)) {
return true;
}
}
holder.state = ImageHolder.NEEDED;
return false;
}
public long getDbId(String path, boolean isVideo) {
String volumeName = "external";
Uri uri = isVideo ? Video.Media.getContentUri(volumeName) : Images.Media.getContentUri(volumeName);
String selection = FileColumns.DATA + "=?";
;
String[] selectionArgs = new String[] {
path
};
String[] columns = new String[] {
FileColumns._ID, FileColumns.DATA
};
Cursor c = mContext.getContentResolver()
.query(uri, columns, selection, selectionArgs, null);
if (c == null) {
return 0;
}
long id = 0;
if (c.moveToNext()) {
id = c.getLong(0);
}
c.close();
return id;
}
/**
* Stops loading images, kills the image loader thread and clears all
* caches.
*/
public void stop() {
pause();
if (mLoaderThread != null) {
mLoaderThread.quit();
mLoaderThread = null;
}
clear();
}
public void clear() {
mPendingRequests.clear();
mImageCache.clear();
}
/**
* Temporarily stops loading
*/
public void pause() {
mPaused = true;
}
/**
* Resumes loading
*/
public void resume() {
mPaused = false;
if (!mPendingRequests.isEmpty()) {
requestLoading();
}
}
/**
* Sends a message to this thread itself to start loading images. If the
* current view contains multiple image views, all of those image views will
* get a chance to request their respective photos before any of those
* requests are executed. This allows us to load images in bulk.
*/
private void requestLoading() {
if (!mLoadingRequested) {
mLoadingRequested = true;
mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
}
}
/**
* Processes requests on the main thread.
*/
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_REQUEST_LOADING: {
mLoadingRequested = false;
if (!mPaused) {
if (mLoaderThread == null) {
mLoaderThread = new LoaderThread();
mLoaderThread.start();
}
mLoaderThread.requestLoading();
}
return true;
}
case MESSAGE_ICON_LOADED: {
if (!mPaused) {
processLoadedIcons();
}
return true;
}
}
return false;
}
/**
* Goes over pending loading requests and displays loaded photos. If some of
* the photos still haven't been loaded, sends another request for image
* loading.
*/
private void processLoadedIcons() {
Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
while (iterator.hasNext()) {
ImageView view = iterator.next();
FileId fileId = mPendingRequests.get(view);
boolean loaded = loadCachedIcon(view, fileId.mPath, fileId.mCategory);
if (loaded) {
iterator.remove();
iconLoadListener.onIconLoadFinished(view);
}
}
if (!mPendingRequests.isEmpty()) {
requestLoading();
}
}
/**
* The thread that performs loading of photos from the database.
*/
private class LoaderThread extends HandlerThread implements Callback {
private Handler mLoaderThreadHandler;
public LoaderThread() {
super(LOADER_THREAD_NAME);
}
/**
* Sends a message to this thread to load requested photos.
*/
public void requestLoading() {
if (mLoaderThreadHandler == null) {
mLoaderThreadHandler = new Handler(getLooper(), this);
}
mLoaderThreadHandler.sendEmptyMessage(0);
}
/**
* Receives the above message, loads photos and then sends a message to
* the main thread to process them.
*/
public boolean handleMessage(Message msg) {
Iterator<FileId> iterator = mPendingRequests.values().iterator();
while (iterator.hasNext()) {
FileId id = iterator.next();
ImageHolder holder = mImageCache.get(id.mPath);
if (holder != null && holder.state == ImageHolder.NEEDED) {
// Assuming atomic behavior
holder.state = ImageHolder.LOADING;
switch (id.mCategory) {
case Apk:
Drawable icon = Util.getApkIcon(mContext, id.mPath);
holder.setImage(icon);
break;
case Picture:
case Video:
boolean isVideo = id.mCategory == FileCategory.Video;
if (id.mId == 0)
id.mId = getDbId(id.mPath, isVideo);
if (id.mId == 0) {
Log.e("FileIconLoader", "Fail to get dababase id for:" + id.mPath);
}
holder.setImage(isVideo ? getVideoThumbnail(id.mId) : getImageThumbnail(id.mId));
break;
}
holder.state = BitmapHolder.LOADED;
mImageCache.put(id.mPath, holder);
}
}
mMainThreadHandler.sendEmptyMessage(MESSAGE_ICON_LOADED);
return true;
}
private static final int MICRO_KIND = 3;
private Bitmap getImageThumbnail(long id) {
return Images.Thumbnails.getThumbnail(mContext.getContentResolver(), id, MICRO_KIND, null);
}
private Bitmap getVideoThumbnail(long id) {
return Video.Thumbnails.getThumbnail(mContext.getContentResolver(), id, MICRO_KIND, null);
}
}
}