/*
* @copyright 2011 Philip Warner
* @license GNU General Public License
*
* This file is part of Book Catalogue.
*
* Book Catalogue 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.
*
* Book Catalogue 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 Book Catalogue. If not, see <http://www.gnu.org/licenses/>.
*/
package com.eleybourn.bookcatalogue;
import java.io.File;
import java.util.ArrayList;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.content.res.TypedArray;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Gallery;
import android.widget.ImageSwitcher;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewSwitcher.ViewFactory;
import com.eleybourn.bookcatalogue.LibraryThingManager.ImageSizes;
import com.eleybourn.bookcatalogue.dialogs.StandardDialogs;
import com.eleybourn.bookcatalogue.utils.Logger;
import com.eleybourn.bookcatalogue.utils.SimpleTaskQueue;
import com.eleybourn.bookcatalogue.utils.SimpleTaskQueue.SimpleTask;
import com.eleybourn.bookcatalogue.utils.SimpleTaskQueue.SimpleTaskContext;
import com.eleybourn.bookcatalogue.utils.Utils;
import com.eleybourn.bookcatalogue.utils.ViewTagger;
/**
* Class to display and manage a cover image browser in a dialog.
*
* ENHANCE: For each ISBN returned by LT, add TWO images and get the second from GoodReads
* ENHANCE: (Somehow) remove non-existent images from ImageSelector. Probably start with 1 image and GROW it.
*
* @author Philip Warner
*/
public class CoverBrowser {
// used in setting images sizes
private android.util.DisplayMetrics mMetrics;
// Task queue for images
private SimpleTaskQueue mImageFetcher = null;
// Handler when an image is finally selected.
private OnImageSelectedListener mOnImageSelectedListener;
// ISBN of book to lookup
private String mIsbn;
// Calling context
private Context mContext;
// Libary Thing
private LibraryThingManager mLibraryThing;
// Calculated size for preview images
private final int mPreviewSize;
// List of all editions for the given ISBN
private ArrayList<String> mEditions;
// Object to ensure files are cleaned up.
private FileManager mFileManager;
// The Dialog
private Dialog mDialog = null;
// Adapted to queue/display images
private CoverImageAdapter mAdapter = null;
/** Indicates a 'shutdown()' has been requested */
private boolean mShutdown = false;
/**
* Interface called when image is selected.
*
* @author Philip Warner
*/
public interface OnImageSelectedListener {
void onImageSelected(String fileSpec);
}
/**
* Constructor
*
* @param context Calling context
* @param metrics Display metrics uses in sizing images
* @param isbn ISBN of book
* @param onImageSelectedListener Handler to call when book selected
*/
CoverBrowser(Context context, android.util.DisplayMetrics metrics, String isbn, OnImageSelectedListener onImageSelectedListener) {
mIsbn = isbn;
mContext = context;
mMetrics = metrics;
mOnImageSelectedListener = onImageSelectedListener;
// Calculate some image sizes to display
mPreviewSize = Math.max(mMetrics.widthPixels, mMetrics.heightPixels)/5;
// Create an object to manage the downloaded files
mFileManager = new FileManager();
}
/**
* Close and abort everything
*/
public void dismiss() {
shutdown();
}
/**
* Close down everything
*/
private void shutdown() {
mShutdown = true;
if (mDialog != null) {
// Dismiss will call shutdown();
mDialog.dismiss();
mDialog = null;
}
if (mImageFetcher != null) {
mImageFetcher.finish();
mImageFetcher = null;
}
if (mFileManager != null) {
mFileManager.purge();
mFileManager = null;
}
}
/**
* SimpleTask to fetch a thumbnail image and apply it to an ImageView
*
* @author Philip Warner
*/
private class GetEditionsTask implements SimpleTask {
String isbn;
/**
* Constructor
*
* @param position Position on requested cover.
* @param v View to update
*/
GetEditionsTask(String isbn){
this.isbn = isbn;
}
@Override
public void run(SimpleTaskContext taskContext) {
// Get some editions
// ENHANCE: the list of editions should be expanded to somehow include Amazon and Google. As well
// as the alternate user-contributed images from LibraryThing. The latter are often the best
// source but at present could only be obtained by HTML scraping.
try {
mEditions = LibraryThingManager.searchEditions(isbn);
} catch (Exception e) {
mEditions = null;
}
}
@Override
public void onFinish(Exception e) {
if (mEditions.size() == 0) {
Toast.makeText(mContext, R.string.no_editions, Toast.LENGTH_LONG).show();
shutdown();
return;
}
showDialogDetails();
}
}
/**
* SimpleTask to fetch a thumbnail image and apply it to an ImageView
*
* @author Philip Warner
*/
@SuppressWarnings("unused")
private class GetThumbnailTask implements SimpleTask {
ImageView v;
int position;
String isbn;
String fileSpec;
/**
* Constructor
*
* @param position Position on requested cover.
* @param v View to update
*/
GetThumbnailTask(int position, ImageView v){
this.position = position;
this.v = v;
isbn = mEditions.get(position);
}
@Override
public void run(SimpleTaskContext taskContext) {
// Start the download
fileSpec = mFileManager.download(isbn, ImageSizes.SMALL);
File file = new File(fileSpec);
if (file.length() < 50) {
fileSpec = mFileManager.download(isbn, ImageSizes.LARGE);
}
}
@Override
public void onFinish(Exception e) {
// Load the file and apply to view
File file = new File(fileSpec);
file.deleteOnExit();
//CoverImageAdapter cia = (CoverImageAdapter) gallery.getAdapter();
//cia.notifyDataSetChanged();
Utils.fetchFileIntoImageView(file, v, mPreviewSize, mPreviewSize, true );
}
}
/**
* SimpleTask to download an image and apply it to the ImageSwitcher.
*
* @author Philip Warner
*/
@SuppressWarnings("unused")
private class GetFullImageTask implements SimpleTask {
// Switcher to use
private ImageSwitcher switcher;
// Position of edition
private int position;
// ISBN
private String isbn;
// Resulting file spec
private String fileSpec;
/**
* Constructor
*
* @param position Position f ISBN
* @param switcher ImageSwicther to update
*/
GetFullImageTask(int position, ImageSwitcher switcher){
this.position = position;
this.switcher = switcher;
// Get the ISBN
isbn = mEditions.get(position);
}
@Override
public void run(SimpleTaskContext taskContext) {
// If we are shutdown, just return
if (mShutdown) {
taskContext.setRequiresFinish(false);
return;
}
// Download the file
fileSpec = mFileManager.download(isbn, ImageSizes.LARGE);
File file = new File(fileSpec);
if (file.length() < 50) {
fileSpec = mFileManager.download(isbn, ImageSizes.SMALL);
}
}
@Override
public void onFinish(Exception e) {
if (mShutdown)
return;
// Update the ImageSwitcher
File file = new File(fileSpec);
TextView msgVw = (TextView)mDialog.findViewById(R.id.switcherStatus);
if (file.exists() && file.length() > 100) {
Drawable d = new BitmapDrawable(Utils.fetchFileIntoImageView(file, null, mPreviewSize*4, mPreviewSize*4, true ));
switcher.setImageDrawable(d);
ViewTagger.setTag(switcher, file.getAbsolutePath());
msgVw.setVisibility(View.GONE);
switcher.setVisibility(View.VISIBLE);
} else {
msgVw.setVisibility(View.VISIBLE);
switcher.setVisibility(View.GONE);
msgVw.setText(R.string.image_not_found);
}
}
}
/**
* Show the user a selection of other covers and allow selection of a replacement.
*/
public void showEditionCovers() {
mLibraryThing = new LibraryThingManager(mContext);
if (!mLibraryThing.isAvailable()) {
StandardDialogs.needLibraryThingAlert(mContext, true, "cover_browser");
return;
}
if (mIsbn == null || mIsbn.trim().length() == 0) {
Toast.makeText(mContext, R.string.no_isbn_no_editions, Toast.LENGTH_LONG).show();
shutdown();
return;
}
// Setup the background fetcher
if (mImageFetcher == null)
mImageFetcher = new SimpleTaskQueue("cover-browser");
SimpleTask edTask = new GetEditionsTask(mIsbn);
mImageFetcher.enqueue(edTask);
// Setup the basic dialog
mDialog = new Dialog(mContext);
mDialog.setContentView(R.layout.select_edition_cover);
mDialog.setTitle(R.string.finding_editions);
//TextView msgVw = (TextView)mDialog.findViewById(R.id.status);
//msgVw.setText(R.string.finding_editions);
mDialog.show();
}
private void showDialogDetails() {
mDialog.setTitle(R.string.select_cover);
// The switcher will be used to display larger versions; needed for onItemClick().
final ImageSwitcher switcher = (ImageSwitcher) mDialog.findViewById(R.id.switcher);
// Setup the Gallery.
final Gallery gallery = (Gallery) mDialog.findViewById(R.id.gallery);
gallery.setVisibility(View.VISIBLE);
// Show help message
TextView msgVw = (TextView)mDialog.findViewById(R.id.switcherStatus);
msgVw.setText(R.string.click_on_thumb);
msgVw.setVisibility(View.VISIBLE);
//gallery.setHorizontalScrollBarEnabled(true);
gallery.setMinimumWidth(mMetrics.widthPixels);
gallery.setMinimumHeight(mPreviewSize);
gallery.setSpacing(mPreviewSize/10);
// Use our custom adapter to load images
mAdapter = new CoverImageAdapter();
gallery.setAdapter(mAdapter);
// When the gallery is clicked, load the switcher
gallery.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
// Show status message
TextView msgVw = (TextView)mDialog.findViewById(R.id.switcherStatus);
switcher.setVisibility(View.GONE);
msgVw.setText(R.string.loading);
msgVw.setVisibility(View.VISIBLE);
GetFullImageTask task = new GetFullImageTask(position, switcher);
mImageFetcher.enqueue(task);
}
});
// When the dialog is closed, delete the files and terminated the SimpleTaskQueue.
mDialog.setOnDismissListener(new OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
shutdown();
}});
// When the large image is clicked, send it back to the caller and terminate.
switcher.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Object newSpec = ViewTagger.getTag(switcher);
if (newSpec != null) {
if (mOnImageSelectedListener != null)
mOnImageSelectedListener.onImageSelected((String)newSpec);
}
if (mDialog != null)
mDialog.dismiss();
}});
// Required object. Just create an ImageView
switcher.setFactory(new ViewFactory() {
@Override
public View makeView() {
ImageView i = new ImageView(mContext);
i.setBackgroundColor(0xFF000000);
i.setScaleType(ImageView.ScaleType.FIT_CENTER);
i.setLayoutParams(new ImageSwitcher.LayoutParams(ImageSwitcher.LayoutParams.WRAP_CONTENT,
ImageSwitcher.LayoutParams.WRAP_CONTENT));
i.setImageResource(android.R.drawable.ic_menu_help);
return i;
}});
}
/**
* Simple utility class to (try) to cleanup and prevent files from accumulating.
*
* @author Philip Warner
*/
private class FileManager {
private Bundle mFiles = new Bundle();
LibraryThingManager mLibraryThing = new LibraryThingManager(mContext);
private boolean isGood(File f) {
boolean ok = true;
if (!f.exists() || f.length() == 0) {
ok = false;
} else {
try {
// Just read the image files to get file size
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inJustDecodeBounds = true;
BitmapFactory.decodeFile( f.getAbsolutePath(), opt );
// If too small, it's no good
if ( opt.outHeight < 8 || opt.outWidth < 8 ) {
ok = false;
}
} catch (Exception e) {
// Failed to decode; probably not an image
ok = false;
Logger.logError(e, "Unable to decode thumbnail");
}
}
try {
if (!ok && f.exists()) {
f.delete();
}
} catch(Exception e) {
Logger.logError(e, "Unable to delete bad thumbnail");
}
return ok;
}
/**
* Download a file if not present and keep a record of it.
*
* @param isbn ISBN of file
* @param size Size of image required.
* @return
*/
public String download(String isbn, ImageSizes size) {
String filespec;
String key = isbn + "_" + size;
boolean isPresent;
synchronized(mFiles) {
isPresent = mFiles.containsKey(key);
}
// Do some checks on the actual file to see if a re-download may help
if (isPresent) {
synchronized(mFiles) {
filespec = mFiles.getString(key);
}
File f = new File(filespec);
if (!isGood(f)) {
mFiles.remove(key);
isPresent = false;
}
}
if (!isPresent) {
filespec = mLibraryThing.getCoverImage(isbn, null, size);
File f = new File(filespec);
if (isGood(f)) {
synchronized(mFiles) {
mFiles.putString(key, filespec);
}
} else {
// Try google
f = GoogleBooksManager.getThumbnailFromIsbn(isbn);
if (f != null && isGood(f)) {
filespec = f.getAbsolutePath();
synchronized(mFiles) {
mFiles.putString(key, filespec);
}
} else {
filespec = "";
mFiles.putString(key, filespec);
}
}
} else {
synchronized(mFiles) {
filespec = mFiles.getString(key);
}
}
return filespec;
}
// Get the requested file, if available, otherwise return null.
public File getFile(String isbn, ImageSizes size) {
String filespec;
String key = isbn + "_" + size;
boolean isPresent;
synchronized(mFiles) {
isPresent = mFiles.containsKey(key);
}
if (!isPresent)
return null;
synchronized(mFiles) {
filespec = mFiles.getString(key);
}
return new File(filespec);
}
/**
* Clean up all files.
*/
public void purge() {
try {
for(String k : mFiles.keySet()) {
String filespec = mFiles.getString(k);
File file = new File(filespec);
if (file.exists())
file.delete();
}
mFiles.clear();
} catch (Exception e) {
Logger.logError(e);
}
}
}
/**
* ImageAdapter for gallery. Queues image requests.
*
* @author Philip Warner
*/
public class CoverImageAdapter extends BaseAdapter {
private int mGalleryItemBackground;
/**
* Constructor
*/
public CoverImageAdapter() {
// Setup the background
TypedArray a = mContext.obtainStyledAttributes(R.styleable.CoverGallery);
mGalleryItemBackground = a.getResourceId(
R.styleable.CoverGallery_android_galleryItemBackground, 0);
a.recycle();
}
@Override
public Object getItem(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// Create or reuse view
ImageView i;
if (convertView == null)
i = new ImageView(mContext);
else
i = (ImageView)convertView;
// If we are shutdown, just return a view
if (mShutdown)
return i;
// Initialize the view
i.setScaleType(ImageView.ScaleType.FIT_XY);
//i.setAdjustViewBounds(true);
i.setMaxHeight(mPreviewSize);
i.setMaxWidth(mPreviewSize);
i.setBackgroundResource(mGalleryItemBackground);
// See if file is present
File f = null;
try {
f = mFileManager.getFile(mEditions.get(position), ImageSizes.SMALL);
} catch (NullPointerException e) {
//file did not exist. Dealt with later.
}
if (f == null) {
// Not present; request it and use a placeholder.
GetThumbnailTask task = new GetThumbnailTask(position, i);
mImageFetcher.enqueue(task);
i.setImageResource(android.R.drawable.ic_menu_help);
} else {
// Present, so use it.
Utils.fetchFileIntoImageView(f, i, mPreviewSize, mPreviewSize, true );
}
return i;
}
@Override
public int getCount() {
return mEditions.size();
}
}
}