/*
* Copyright (C) 2012 Alex Kuiper
*
* This file is part of PageTurner
*
* PageTurner 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.
*
* PageTurner 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 PageTurner. If not, see <http://www.gnu.org/licenses/>.*
*/
package net.nightwhistler.pageturner.fragment;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
import com.actionbarsherlock.widget.SearchView;
import com.github.rtyley.android.sherlock.roboguice.fragment.RoboSherlockFragment;
import com.google.inject.Inject;
import jedi.functional.FunctionalPrimitives;
import jedi.option.Option;
import net.nightwhistler.htmlspanner.HtmlSpanner;
import net.nightwhistler.pageturner.Configuration;
import net.nightwhistler.pageturner.Configuration.ColourProfile;
import net.nightwhistler.pageturner.Configuration.LibrarySelection;
import net.nightwhistler.pageturner.Configuration.LibraryView;
import net.nightwhistler.pageturner.PlatformUtil;
import net.nightwhistler.pageturner.R;
import net.nightwhistler.pageturner.activity.*;
import net.nightwhistler.ui.DialogFactory;
import net.nightwhistler.ui.UiUtils;
import net.nightwhistler.pageturner.library.*;
import net.nightwhistler.pageturner.scheduling.QueueableAsyncTask;
import net.nightwhistler.pageturner.scheduling.TaskQueue;
import net.nightwhistler.pageturner.view.BookCaseView;
import net.nightwhistler.pageturner.view.FastBitmapDrawable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import roboguice.inject.InjectView;
import java.io.File;
import java.text.DateFormat;
import java.util.*;
import static java.lang.Character.toUpperCase;
import static jedi.functional.FunctionalPrimitives.isEmpty;
import static jedi.option.Options.none;
import static jedi.option.Options.option;
import static jedi.option.Options.some;
import static net.nightwhistler.ui.UiUtils.onCollapse;
import static net.nightwhistler.ui.UiUtils.onMenuPress;
import static net.nightwhistler.pageturner.PlatformUtil.isIntentAvailable;
public class LibraryFragment extends RoboSherlockFragment implements ImportCallback {
protected static final int REQUEST_CODE_GET_CONTENT = 2;
@Inject
private LibraryService libraryService;
@Inject
private DialogFactory dialogFactory;
@InjectView(R.id.libraryList)
private ListView listView;
@InjectView(R.id.bookCaseView)
private BookCaseView bookCaseView;
@InjectView(R.id.alphabetList)
private ListView alphabetBar;
private AlphabetAdapter alphabetAdapter;
@InjectView(R.id.alphabetDivider)
private ImageView alphabetDivider;
@InjectView(R.id.libHolder)
private ViewSwitcher switcher;
@Inject
private Context context;
@Inject
private Configuration config;
@Inject
private TaskQueue taskQueue;
private Drawable backupCover;
private Handler handler;
private KeyedResultAdapter bookAdapter;
private static final DateFormat DATE_FORMAT = DateFormat.getDateInstance(DateFormat.LONG);
private static final int ALPHABET_THRESHOLD = 20;
private ProgressDialog waitDialog;
private ProgressDialog importDialog;
private AlertDialog importQuestion;
private boolean askedUserToImport;
private boolean oldKeepScreenOn;
private static final Logger LOG = LoggerFactory.getLogger("LibraryActivity");
private IntentCallBack intentCallBack;
private List<CoverCallback> callbacks = new ArrayList<>();
private Map<String, FastBitmapDrawable> coverCache = new HashMap<>();
private MenuItem searchMenuItem;
private interface IntentCallBack {
void onResult( int resultCode, Intent data );
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LOG.debug("onCreate()");
Bitmap backupBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.unknown_cover );
this.backupCover = new FastBitmapDrawable(backupBitmap);
this.handler = new Handler();
if ( savedInstanceState != null ) {
this.askedUserToImport = savedInstanceState.getBoolean("import_q", false);
}
this.taskQueue.setTaskQueueListener(this::onTaskQueueEmpty);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_library, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setHasOptionsMenu(true);
this.bookCaseView.setOnScrollListener( new CoverScrollListener() );
this.listView.setOnScrollListener( new CoverScrollListener() );
if ( config.getLibraryView() == LibraryView.BOOKCASE ) {
this.bookAdapter = new BookCaseAdapter();
this.bookCaseView.setAdapter(bookAdapter);
if ( switcher.getDisplayedChild() == 0 ) {
switcher.showNext();
}
} else {
this.bookAdapter = new BookListAdapter(context);
this.listView.setAdapter(bookAdapter);
}
this.waitDialog = new ProgressDialog(context);
this.waitDialog.setOwnerActivity(getActivity());
this.importDialog = new ProgressDialog(context);
this.importDialog.setOwnerActivity(getActivity());
importDialog.setTitle(R.string.importing_books);
importDialog.setMessage(getString(R.string.scanning_epub));
registerForContextMenu(this.listView);
this.listView.setOnItemClickListener( this::onItemClick );
this.listView.setOnItemLongClickListener(this::onItemLongClick );
setAlphabetBarVisible(false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ActionBar actionBar = getSherlockActivity().getSupportActionBar();
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
ArrayAdapter<String> adapter = new ArrayAdapter<>(actionBar.getThemedContext(),
android.R.layout.simple_list_item_1,
android.R.id.text1, getResources().getStringArray(R.array.libraryQueries));
actionBar.setListNavigationCallbacks(adapter, this::onNavigationItemSelected );
refreshView();
Option<File> libraryFolder = config.getLibraryFolder();
LOG.debug( "Got libraryFolder: " + libraryFolder );
libraryFolder.match( folder -> {
executeTask(new CleanFilesTask(libraryService, this::booksDeleted) );
executeTask(new ImportTask(getActivity(), libraryService, this, config, config.getCopyToLibraryOnScan(),
true), folder );
}, () -> {
LOG.error("No library folder present!");
Toast.makeText( context, R.string.library_failed, Toast.LENGTH_LONG ).show();
});
}
private <A,B,C> void executeTask( QueueableAsyncTask<A,B,C> task, A... parameters ) {
setSupportProgressBarIndeterminateVisibility(true);
this.taskQueue.executeTask(task, parameters);
}
/**
* Triggered by the TaskQueue when all tasks are finished.
*/
private void onTaskQueueEmpty() {
LOG.debug( "Got onTaskQueueEmpty()" );
setSupportProgressBarIndeterminateVisibility(false);
}
private void clearCoverCache() {
for ( Map.Entry<String, FastBitmapDrawable> draw: coverCache.entrySet() ) {
draw.getValue().destroy();
}
coverCache.clear();
}
private void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
if ( config.getLongShortPressBehaviour() == Configuration.LongShortPressBehaviour.NORMAL ) {
this.bookAdapter.getResultAt( position ).forEach( this::showBookDetails );
} else {
this.bookAdapter.getResultAt( position ).forEach( this::openBook );
}
}
private boolean onItemLongClick(AdapterView<?> parent, View view,
int position, long id) {
if ( config.getLongShortPressBehaviour() == Configuration.LongShortPressBehaviour.NORMAL ) {
this.bookAdapter.getResultAt( position ).forEach(this::openBook);
} else {
this.bookAdapter.getResultAt( position ).forEach( this::showBookDetails );
}
return true;
}
private Option<Drawable> getCover( LibraryBook book ) {
try {
if ( !coverCache.containsKey(book.getFileName() ) ) {
Bitmap bitmap = BitmapFactory.decodeByteArray(book.getCoverImage(), 0, book.getCoverImage().length );
FastBitmapDrawable drawable = new FastBitmapDrawable(bitmap);
coverCache.put( book.getFileName(), drawable );
}
return option(coverCache.get(book.getFileName()));
} catch ( OutOfMemoryError outOfMemoryError ) {
clearCoverCache();
return none();
}
}
private void showBookDetails( final LibraryBook libraryBook ) {
if ( ! isAdded() || libraryBook == null ) {
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.book_details);
LayoutInflater inflater = PlatformUtil.getLayoutInflater(getActivity());
View layout = inflater.inflate(R.layout.book_details, null);
builder.setView( layout );
ImageView coverView = (ImageView) layout.findViewById(R.id.coverImage );
if ( libraryBook.getCoverImage() != null ) {
Drawable coverDrawable = getCover(libraryBook).getOrElse(
getResources().getDrawable(R.drawable.unknown_cover) );
coverView.setImageDrawable(coverDrawable);
}
TextView titleView = (TextView) layout.findViewById(R.id.titleField);
TextView authorView = (TextView) layout.findViewById(R.id.authorField);
TextView lastRead = (TextView) layout.findViewById(R.id.lastRead);
TextView added = (TextView) layout.findViewById(R.id.addedToLibrary);
TextView descriptionView = (TextView) layout.findViewById(R.id.bookDescription);
TextView fileName = (TextView) layout.findViewById(R.id.fileName);
titleView.setText(libraryBook.getTitle());
String authorText = String.format( getString(R.string.book_by),
libraryBook.getAuthor().getFirstName() + " "
+ libraryBook.getAuthor().getLastName() );
authorView.setText( authorText );
fileName.setText( libraryBook.getFileName() );
if (libraryBook.getLastRead() != null && ! libraryBook.getLastRead().equals(new Date(0))) {
String lastReadText = String.format(getString(R.string.last_read),
DATE_FORMAT.format(libraryBook.getLastRead()));
lastRead.setText( lastReadText );
} else {
String lastReadText = String.format(getString(R.string.last_read), getString(R.string.never_read));
lastRead.setText( lastReadText );
}
String addedText = String.format( getString(R.string.added_to_lib),
DATE_FORMAT.format(libraryBook.getAddedToLibrary()));
added.setText( addedText );
HtmlSpanner spanner = new HtmlSpanner();
spanner.unregisterHandler("img" ); //We don't want to render images
descriptionView.setText(spanner.fromHtml( libraryBook.getDescription()));
builder.setNeutralButton(R.string.delete, (dialog, which) -> {
libraryService.deleteBook( libraryBook.getFileName() );
refreshView();
dialog.dismiss();
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.setPositiveButton(R.string.read, (dialog, which) -> openBook(libraryBook) );
builder.show();
}
private void openBook(LibraryBook libraryBook) {
Intent intent = new Intent(getActivity(), ReadingActivity.class);
config.setLastActivity( ReadingActivity.class );
intent.setData( Uri.parse(libraryBook.getFileName()));
getActivity().setResult(Activity.RESULT_OK, intent);
getActivity().startActivityIfNeeded(intent, 99);
}
private void startImport(File startFolder, boolean copy) {
ImportTask importTask = new ImportTask(context, libraryService, this, config, copy, false);
importDialog.setOnCancelListener(importTask);
importDialog.show();
this.oldKeepScreenOn = listView.getKeepScreenOn();
listView.setKeepScreenOn(true);
this.taskQueue.clear();
executeTask(importTask, startFolder);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if ( this.intentCallBack != null ) {
this.intentCallBack.onResult(resultCode, data);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.library_menu, menu);
UiUtils.Action toggleListener = () -> {
if ( switcher.getDisplayedChild() == 0 ) {
bookAdapter = new BookCaseAdapter();
bookCaseView.setAdapter(bookAdapter);
config.setLibraryView(LibraryView.BOOKCASE);
} else {
bookAdapter = new BookListAdapter(getActivity());
listView.setAdapter(bookAdapter);
config.setLibraryView(LibraryView.LIST);
}
switcher.showNext();
refreshView();
};
onMenuPress( menu, R.id.shelves_view ).thenDo( toggleListener );
onMenuPress( menu, R.id.list_view ).thenDo( toggleListener );
onMenuPress( menu, R.id.scan_books ).thenDo( this::showImportDialog );
onMenuPress( menu, R.id.about ).thenDo( dialogFactory.buildAboutDialog()::show );
onMenuPress( menu, R.id.profile_day ).thenDo(() -> switchToColourProfile(ColourProfile.DAY) );
onMenuPress( menu, R.id.profile_night ).thenDo(() -> switchToColourProfile(ColourProfile.NIGHT));
this.searchMenuItem = menu.findItem(R.id.menu_search);
if (searchMenuItem != null) {
final SearchView searchView = (SearchView) searchMenuItem.getActionView();
if (searchView != null) {
searchView.setOnQueryTextListener( UiUtils.onQuery( this::performSearch ));
searchMenuItem.setOnActionExpandListener( onCollapse(() -> performSearch("")));
} else {
searchMenuItem.setOnMenuItemClickListener( item -> {
dialogFactory.showSearchDialog(R.string.search_library, R.string.enter_query, this::performSearch);
return false;
});
}
}
// Only show open file item if we have a file manager installed
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("file/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
if (isIntentAvailable(getActivity(), intent)) {
onMenuPress( menu, R.id.open_file ).thenDo( this::launchFileManager );
} else {
menu.findItem(R.id.open_file).setVisible(false);
}
}
private void launchFileManager() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("file/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
this.intentCallBack = (int resultCode, Intent data) -> {
if ( resultCode == Activity.RESULT_OK && data != null ) {
Intent readingIntent = new Intent( getActivity(), ReadingActivity.class);
readingIntent.setData(data.getData());
getActivity().setResult(Activity.RESULT_OK, readingIntent);
getActivity().startActivityIfNeeded(readingIntent, 99);
}
};
try {
startActivityForResult(intent, REQUEST_CODE_GET_CONTENT);
} catch (ActivityNotFoundException e) {
// No compatible file manager was found.
Toast.makeText(getActivity(), getString(R.string.install_oi),
Toast.LENGTH_SHORT).show();
}
}
public void onSearchRequested() {
if ( this.searchMenuItem != null && searchMenuItem.getActionView() != null ) {
this.searchMenuItem.expandActionView();
this.searchMenuItem.getActionView().requestFocus();
} else {
dialogFactory.showSearchDialog(R.string.search_library, R.string.enter_query, this::performSearch);
}
}
private void performSearch(String query) {
if ( query != null ) {
setSupportProgressBarIndeterminateVisibility(true);
this.taskQueue.jumpQueueExecuteTask(new LoadBooksTask(config.getLastLibraryQuery(), query));
}
}
private void switchToColourProfile( ColourProfile profile ) {
config.setColourProfile(profile);
Intent intent = new Intent(getActivity(), LibraryActivity.class);
startActivity(intent);
onStop();
getActivity().finish();
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
boolean bookCaseActive = config.getLibraryView() == LibraryView.BOOKCASE;
menu.findItem(R.id.shelves_view).setVisible(! bookCaseActive);
menu.findItem(R.id.list_view).setVisible(bookCaseActive);
menu.findItem(R.id.profile_day).setVisible(config.getColourProfile() == ColourProfile.NIGHT);
menu.findItem(R.id.profile_night).setVisible(config.getColourProfile() == ColourProfile.DAY);
}
private void showImportDialog() {
AlertDialog.Builder builder;
LayoutInflater inflater = PlatformUtil.getLayoutInflater(getActivity());
final View layout = inflater.inflate(R.layout.import_dialog, null);
final RadioButton scanSpecific = (RadioButton) layout.findViewById(R.id.radioScanFolder);
final RadioGroup scanRadioGroup = (RadioGroup) layout.findViewById(R.id.radioScanGroup);
final TextView folder = (TextView) layout.findViewById(R.id.folderToScan);
final CheckBox copyToLibrary = (CheckBox) layout.findViewById(R.id.copyToLib);
final Button browseButton = (Button) layout.findViewById(R.id.browseButton);
Option<File> storageBase = config.getStorageBase();
if ( isEmpty(storageBase) ) {
return;
}
folder.setEnabled(false);
browseButton.setEnabled(false);
scanRadioGroup.setOnCheckedChangeListener((RadioGroup group, int checkedId) -> {
if (scanSpecific.isChecked()){
folder.setEnabled(true);
browseButton.setEnabled(true);
} else {
folder.setEnabled(false);
browseButton.setEnabled(false);
}
});
// Copy scan settings from the prefs
copyToLibrary.setChecked( config.getCopyToLibraryOnScan() );
scanSpecific.setChecked( config.getUseCustomScanFolder() );
folder.setText( config.getScanFolder() );
builder = new AlertDialog.Builder(getActivity());
builder.setView(layout);
this.intentCallBack = (int resultCode, Intent data) -> {
if ( resultCode == Activity.RESULT_OK && data != null ) {
folder.setText(data.getData().getPath());
}
};
browseButton.setOnClickListener(v -> {
Intent intent = new Intent(getActivity(), FileBrowseActivity.class);
intent.setData( Uri.parse(folder.getText().toString() ));
startActivityForResult(intent, 0);
});
builder.setTitle(R.string.import_books);
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
dialog.dismiss();
/* Update settings */
config.setUseCustomScanFolder( scanSpecific.isChecked() );
config.setCopyToLibraryOnScan( copyToLibrary.isChecked() );
File folderToScan;
if ( scanSpecific.isChecked() ) {
String path = folder.getText().toString();
folderToScan = new File(path);
config.setScanFolder(path); /* update custom path only if used */
} else {
File default_storage = storageBase.unsafeGet();
folderToScan = new File(default_storage.getAbsolutePath());
}
startImport(folderToScan, copyToLibrary.isChecked());
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean("import_q", askedUserToImport);
}
@Override
public void onStop() {
this.libraryService.close();
this.waitDialog.dismiss();
this.importDialog.dismiss();
super.onStop();
}
public void onBackPressed() {
getActivity().finish();
}
@Override
public void onPause() {
this.bookAdapter.clear();
//We clear the list to free up memory.
this.taskQueue.clear();
this.clearCoverCache();
super.onPause();
}
@Override
public void onResume() {
super.onResume();
LibrarySelection lastSelection = config.getLastLibraryQuery();
ActionBar actionBar = getSherlockActivity().getSupportActionBar();
if (actionBar.getSelectedNavigationIndex() != lastSelection.ordinal() ) {
actionBar.setSelectedNavigationItem(lastSelection.ordinal());
} else {
executeTask(new LoadBooksTask(lastSelection));
}
}
@Override
public void importCancelled(int booksImported, List<String> failures, boolean emptyLibrary, boolean silent) {
LOG.debug("Got importCancelled() ");
afterImport( booksImported, failures, emptyLibrary, silent, true );
}
@Override
public void importComplete(int booksImported, List<String> errors, boolean emptyLibrary, boolean silent) {
LOG.debug("Got importComplete() ");
afterImport(booksImported, errors, emptyLibrary, silent, false);
}
private void afterImport(int booksImported, List<String> errors, boolean emptyLibrary, boolean silent,
boolean cancelledByUser ) {
if ( !isAdded() || getActivity() == null ) {
return;
}
if ( silent ) {
if ( booksImported > 0 ) {
//Schedule refresh without clearing the queue
executeTask(new LoadBooksTask(config.getLastLibraryQuery()));
}
return;
}
importDialog.hide();
//If the user cancelled the import, don't bug him/her with alerts.
if ( (! errors.isEmpty()) ) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.import_errors);
builder.setItems( errors.toArray(new String[errors.size()]), null );
builder.setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss() );
builder.show();
}
listView.setKeepScreenOn(oldKeepScreenOn);
if ( booksImported > 0 ) {
//Switch to the "recently added" view.
if (getSherlockActivity().getSupportActionBar().getSelectedNavigationIndex() == LibrarySelection.LAST_ADDED.ordinal() ) {
loadView(LibrarySelection.LAST_ADDED, "importComplete()");
} else {
getSherlockActivity().getSupportActionBar().setSelectedNavigationItem(LibrarySelection.LAST_ADDED.ordinal());
}
} else if ( ! cancelledByUser ) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.no_books_found);
if ( emptyLibrary ) {
builder.setMessage( getString(R.string.no_bks_fnd_text2) );
builder.setPositiveButton( android.R.string.yes, (dialogInterface, i) ->
( (PageTurnerActivity) getSherlockActivity() ).launchActivity( CatalogActivity.class ));
builder.setNegativeButton( android.R.string.no, null );
} else {
builder.setMessage( getString(R.string.no_new_books_found));
builder.setNeutralButton(android.R.string.ok, ( dialog, which) -> dialog.dismiss() );
}
builder.show();
}
}
@Override
public void importFailed(String reason, boolean silent) {
LOG.debug("Got importFailed()");
if (silent || !isAdded() || getActivity() == null ) {
return;
}
importDialog.hide();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.import_failed);
builder.setMessage(reason);
builder.setNeutralButton(android.R.string.ok, null);
builder.show();
}
@Override
public void importStatusUpdate(String update, boolean silent) {
if (silent || !isAdded() || getActivity() == null ) {
return;
}
importDialog.setMessage(update);
}
public void onAlphabetBarClick( KeyedQueryResult<LibraryBook> result, Character c ) {
result.getOffsetFor(toUpperCase(c)).forEach( index -> {
if ( alphabetAdapter != null ) {
alphabetAdapter.setHighlightChar(c);
}
if ( config.getLibraryView() == LibraryView.BOOKCASE ) {
this.bookCaseView.setSelection(index);
} else {
this.listView.setSelection(index);
}
});
}
/**
* Based on example found here:
* http://www.vogella.de/articles/AndroidListView/article.html
*
* @author work
*
*/
private class BookListAdapter extends KeyedResultAdapter {
private Context context;
public BookListAdapter(Context context) {
this.context = context;
}
@Override
public View getView(int index, final LibraryBook book, View convertView,
ViewGroup parent) {
View rowView;
if ( convertView == null ) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
rowView = inflater.inflate(R.layout.book_row, parent, false);
} else {
rowView = convertView;
}
TextView titleView = (TextView) rowView.findViewById(R.id.bookTitle);
TextView authorView = (TextView) rowView.findViewById(R.id.bookAuthor);
TextView dateView = (TextView) rowView.findViewById(R.id.addedToLibrary);
TextView progressView = (TextView) rowView.findViewById(R.id.readingProgress);
final ImageView imageView = (ImageView) rowView.findViewById(R.id.bookCover);
String authorText = String.format(getString(R.string.book_by),
book.getAuthor().getFirstName() + " " + book.getAuthor().getLastName() );
authorView.setText(authorText);
titleView.setText(book.getTitle());
if ( book.getProgress() > 0 ) {
progressView.setText( "" + book.getProgress() + "%");
} else {
progressView.setText("");
}
String dateText = String.format(getString(R.string.added_to_lib),
DATE_FORMAT.format(book.getAddedToLibrary()));
dateView.setText( dateText );
loadCover(imageView, book, index);
return rowView;
}
}
private void loadView( LibrarySelection selection, String from ) {
LOG.debug("Loading view: " + selection + " from " + from);
this.taskQueue.clear();
executeTask(new LoadBooksTask(selection));
}
private void refreshView() {
LOG.debug("View refresh requested");
loadView(config.getLastLibraryQuery(), "refreshView()");
}
/**
* Called after books have been deleted.
* @param numberOfDeletedBooks
*/
private void booksDeleted(int numberOfDeletedBooks) {
LOG.debug("Got " + numberOfDeletedBooks + " deleted books.");
if ( numberOfDeletedBooks > 0 ) {
//Schedule a refresh without clearing the task queue
executeTask(new LoadBooksTask(config.getLastLibraryQuery()));
}
}
private void loadCover( ImageView imageView, LibraryBook book, int index ) {
Drawable draw = coverCache.get(book.getFileName());
if ( draw != null ) {
imageView.setImageDrawable(draw);
} else {
imageView.setImageDrawable(backupCover);
if ( book.getCoverImage() != null ) {
callbacks.add( new CoverCallback(book, index, imageView ) );
}
}
}
private class CoverScrollListener implements AbsListView.OnScrollListener {
private Runnable lastRunnable;
private Character lastCharacter;
private Drawable holoDrawable;
public CoverScrollListener() {
try {
this.holoDrawable = getResources().getDrawable(R.drawable.list_activated_holo);
} catch (IllegalStateException i) {
//leave it null
}
}
@Override
public void onScroll(AbsListView view, final int firstVisibleItem,
final int visibleItemCount, final int totalItemCount) {
if ( visibleItemCount == 0 ) {
return;
}
if ( this.lastRunnable != null ) {
handler.removeCallbacks(lastRunnable);
}
this.lastRunnable = () -> {
if ( bookAdapter.isKeyed() ) {
String key = bookAdapter.getKey(firstVisibleItem).getOrElse("");
if (key.length() > 0) {
Character keyChar = toUpperCase(key.charAt(0));
if (keyChar.equals(lastCharacter)) {
lastCharacter = keyChar;
List<Character> alphabet = bookAdapter.getAlphabet();
//If the highlight-char is already set, this means the
//user clicked the bar, so don't scroll it.
if (alphabetAdapter != null && !keyChar.equals(alphabetAdapter.getHighlightChar())) {
alphabetAdapter.setHighlightChar(keyChar);
alphabetBar.setSelection(alphabet.indexOf(keyChar));
}
for (int i = 0; i < alphabetBar.getChildCount(); i++) {
View child = alphabetBar.getChildAt(i);
if (child.getTag().equals(keyChar)) {
child.setBackgroundDrawable(holoDrawable);
} else {
child.setBackgroundDrawable(null);
}
}
}
}
}
List<CoverCallback> localList = new ArrayList<>( callbacks );
callbacks.clear();
int lastVisibleItem = firstVisibleItem + visibleItemCount - 1;
LOG.debug( "Loading items " + firstVisibleItem + " to " + lastVisibleItem + " of " + totalItemCount );
for ( CoverCallback callback: localList ) {
if ( callback.viewIndex >= firstVisibleItem && callback.viewIndex <= lastVisibleItem ) {
callback.run();
}
}
};
handler.postDelayed(lastRunnable, 550);
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
}
private class CoverCallback {
protected LibraryBook book;
protected int viewIndex;
protected ImageView view;
public CoverCallback(LibraryBook book, int viewIndex, ImageView view) {
this.book = book;
this.view = view;
this.viewIndex = viewIndex;
}
public void run() {
try {
getCover(book).forEach( view::setImageDrawable );
} catch (IllegalStateException i) {
//Do nothing, happens when we're no longer attached.
}
}
}
private class BookCaseAdapter extends KeyedResultAdapter {
@Override
public View getView(final int index, final LibraryBook object, View convertView,
ViewGroup parent) {
View result;
if ( convertView == null ) {
LayoutInflater inflater = PlatformUtil.getLayoutInflater(getActivity());
result = inflater.inflate(R.layout.bookcase_row, parent, false);
} else {
result = convertView;
}
result.setTag(index);
result.setOnClickListener( v -> LibraryFragment.this.onItemClick(null, null, index, 0) );
result.setOnLongClickListener( v -> LibraryFragment.this.onItemLongClick(null, null, index, 0));
final ImageView image = (ImageView) result.findViewById(R.id.bookCover);
image.setImageDrawable(backupCover);
TextView text = (TextView) result.findViewById(R.id.bookLabel);
text.setText( object.getTitle() );
text.setBackgroundResource(R.drawable.alphabet_bar_bg_dark);
loadCover(image, object, index);
return result;
}
}
private void buildImportQuestionDialog() {
if ( importQuestion != null || ! isAdded() ) {
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.no_books_found);
builder.setMessage( getString(R.string.scan_bks_question) );
builder.setPositiveButton(android.R.string.yes, (dialog, which) -> {
dialog.dismiss();
showImportDialog();
});
builder.setNegativeButton(android.R.string.no, (dialog, which ) -> {
dialog.dismiss();
importQuestion = null;
});
this.importQuestion = builder.create();
}
private void setAlphabetBarVisible( boolean visible ) {
int vis = visible ? View.VISIBLE : View.GONE;
alphabetBar.setVisibility(vis);
alphabetDivider.setVisibility(vis);
listView.setFastScrollEnabled(visible);
}
private void setSupportProgressBarIndeterminateVisibility(boolean enable) {
SherlockFragmentActivity activity = getSherlockActivity();
if ( activity != null) {
LOG.debug("Setting progress bar to " + enable );
activity.setSupportProgressBarIndeterminateVisibility(enable);
} else {
LOG.debug("Got null activity.");
}
}
private boolean onNavigationItemSelected(int pos, long arg1) {
LibrarySelection newSelections = LibrarySelection.values()[pos];
if ( newSelections != config.getLastLibraryQuery() ) {
config.setLastLibraryQuery(newSelections);
bookAdapter.clear();
loadView(newSelections, "onNavigationItemSelected()");
}
return false;
}
private class AlphabetAdapter extends ArrayAdapter<Character> {
private List<Character> data;
private Character highlightChar;
public AlphabetAdapter(Context context, int layout, int view, List<Character> input ) {
super(context, layout, view, input);
this.data = input;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = super.getView(position, convertView, parent);
Character tag = data.get(position);
view.setTag( tag );
if ( tag.equals(highlightChar) ) {
view.setBackgroundDrawable( getResources().getDrawable(R.drawable.list_activated_holo));
} else {
view.setBackgroundDrawable(null);
}
return view;
}
public void setHighlightChar(Character highlightChar) {
this.highlightChar = highlightChar;
}
public Character getHighlightChar() {
return highlightChar;
}
}
private void loadQueryData( QueryResult<LibraryBook> result ) {
if ( !isAdded() || getActivity() == null ) {
return;
}
bookAdapter.setResult(result);
if ( result instanceof KeyedQueryResult && result.getSize() >= ALPHABET_THRESHOLD ) {
final KeyedQueryResult<LibraryBook> keyedResult = (KeyedQueryResult<LibraryBook>) result;
alphabetAdapter = new AlphabetAdapter(getActivity(),
R.layout.alphabet_line, R.id.alphabetLabel, keyedResult.getAlphabet() );
alphabetBar.setAdapter(alphabetAdapter);
alphabetBar.setOnItemClickListener( (a, b, index, c) ->
onAlphabetBarClick(keyedResult, keyedResult.getAlphabet().get(index) ));
setAlphabetBarVisible(true);
} else {
alphabetAdapter = null;
setAlphabetBarVisible(false);
}
}
private class LoadBooksTask extends QueueableAsyncTask<String, Integer, QueryResult<LibraryBook>> {
private Configuration.LibrarySelection sel;
private String filter;
public LoadBooksTask(LibrarySelection selection) {
this.sel = selection;
}
public LoadBooksTask(LibrarySelection selection, String filter ) {
this(selection);
this.filter = filter;
}
@Override
public void doOnPreExecute() {
if ( this.filter == null ) {
coverCache.clear();
}
}
@Override
public Option<QueryResult<LibraryBook>> doInBackground(String... params) {
Exception storedException = null;
String query = this.filter;
for ( int i=0; i < 3; i++ ) {
try {
switch ( sel ) {
case LAST_ADDED:
return some(libraryService.findAllByLastAdded(query));
case UNREAD:
return some(libraryService.findUnread(query));
case BY_TITLE:
return some(libraryService.findAllByTitle(query));
case BY_AUTHOR:
return some(libraryService.findAllByAuthor(query));
default:
return some(libraryService.findAllByLastRead(query));
}
} catch (SQLiteException sql) {
storedException = sql;
try {
//Sometimes the database is still locked.
Thread.sleep(1000);
} catch (InterruptedException in) {}
}
}
LOG.error( "Failed after 3 attempts", storedException );
return none();
}
@Override
public void doOnPostExecute(Option<QueryResult<LibraryBook>> result) {
result.match(r -> {
loadQueryData(r);
if (filter == null && sel == Configuration.LibrarySelection.LAST_ADDED && r.getSize() == 0 && !askedUserToImport) {
askedUserToImport = true;
buildImportQuestionDialog();
importQuestion.show();
}
}, () -> Toast.makeText(context, R.string.library_failed, Toast.LENGTH_SHORT).show());
}
}
}