/*
* @copyright 2012 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 static com.eleybourn.bookcatalogue.booklist.DatabaseDefinitions.DOM_READ;
import static com.eleybourn.bookcatalogue.booklist.DatabaseDefinitions.DOM_TITLE;
import static com.eleybourn.bookcatalogue.booklist.DatabaseDefinitions.TBL_BOOKS;
import java.util.ArrayList;
import java.util.Iterator;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.app.SearchManager;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
import com.eleybourn.bookcatalogue.BooksMultitypeListHandler.BooklistChangeListener;
import com.eleybourn.bookcatalogue.booklist.BooklistBuilder;
import com.eleybourn.bookcatalogue.booklist.BooklistBuilder.BookRowInfo;
import com.eleybourn.bookcatalogue.booklist.BooklistGroup.RowKinds;
import com.eleybourn.bookcatalogue.booklist.BooklistPreferencesActivity;
import com.eleybourn.bookcatalogue.booklist.BooklistPseudoCursor;
import com.eleybourn.bookcatalogue.booklist.BooklistStyle;
import com.eleybourn.bookcatalogue.booklist.BooklistStylePropertiesActivity;
import com.eleybourn.bookcatalogue.booklist.BooklistStyles;
import com.eleybourn.bookcatalogue.compat.BookCatalogueActivity;
import com.eleybourn.bookcatalogue.debug.Tracker;
import com.eleybourn.bookcatalogue.dialogs.StandardDialogs;
import com.eleybourn.bookcatalogue.dialogs.StandardDialogs.SimpleDialogItem;
import com.eleybourn.bookcatalogue.dialogs.StandardDialogs.SimpleDialogMenuItem;
import com.eleybourn.bookcatalogue.dialogs.StandardDialogs.SimpleDialogOnClickListener;
import com.eleybourn.bookcatalogue.goodreads.GoodreadsManager;
import com.eleybourn.bookcatalogue.goodreads.GoodreadsUtils;
import com.eleybourn.bookcatalogue.utils.HintManager;
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.TrackedCursor;
import com.eleybourn.bookcatalogue.utils.Utils;
import com.eleybourn.bookcatalogue.utils.ViewTagger;
/**
* Activity that displays a flattened book hierarchy based on the Booklist* classes.
*
* @author Philip Warner
*/
public class BooksOnBookshelf extends BookCatalogueActivity implements BooklistChangeListener {
/** Counter for com.eleybourn.bookcatalogue.debug purposes */
private static Integer mInstanceCount = 0;
/** Prefix used in preferences for this activity */
private final static String TAG = "BooksOnBookshelf";
/** Preference name */
public final static String PREF_BOOKSHELF = TAG + ".BOOKSHELF";
/** Preference name */
private final static String PREF_TOP_ROW = TAG + ".TOP_ROW";
/** Preference name */
private final static String PREF_TOP_ROW_TOP = TAG + ".TOP_ROW_TOP";
/** Preference name */
private final static String PREF_LIST_STYLE = TAG + ".LIST_STYLE";
/** Currently selected bookshelf */
private String mCurrentBookshelf = ""; //getString(R.string.all_books);
/** Currently selected list style */
BooklistStyle mCurrentStyle = null;
/** Flag indicating activity has been destroyed. Used for background tasks */
private boolean mIsDead = false;
/** Flag to indicate that a list has been successfully loaded -- affects the way we save state */
private boolean mListHasBeenLoaded = false;
/** Used by onScroll to detect when the top row has actuallt changed. */
private int mLastTop = -1;
/** ProgressDialog used to display "Getting books...". Needed here so we can dismiss it on close. */
private ProgressDialog mListDialog = null;
/** A book ID used for keeping/updating current list position, eg. when a book is edited. */
private long mMarkBookId = 0;
/** Text to use in search query */
private String mSearchText = "";
/** Saved position of last top row */
private int mTopRow = 0;
/** Saved position of last top row offset from view top */
private int mTopRowTop = 0;
/** Database connection */
private CatalogueDBAdapter mDb;
/** Handler to manage all Views on the list */
private BooksMultitypeListHandler mListHandler;
/** Current displayed list cursor */
private BooklistPseudoCursor mList;
/** Multi-type adapter to manage list connection to cursor */
private MultitypeListAdapter mAdapter;
/** Task queue to get book lists in background */
private SimpleTaskQueue mTaskQueue = new SimpleTaskQueue("BoB-List", 1);
/** Preferred booklist state in next rebuild */
private int mRebuildState;
/** Total number of books in current list */
private int mTotalBooks = 0;
/** Total number of unique books in current list */
private int mUniqueBooks = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
Tracker.enterOnCreate(this);
try {
super.onCreate(savedInstanceState);
setTitle(R.string.my_books);
if (savedInstanceState == null)
// Get preferred booklist state to use from preferences; default to always expanded (MUCH faster than 'preserve' with lots of books)
mRebuildState = BooklistPreferencesActivity.getRebuildState();
else
// Always preserve state when rebuilding/recreating etc
mRebuildState = BooklistPreferencesActivity.BOOKLISTS_STATE_PRESERVED;
mDb = new CatalogueDBAdapter(this);
mDb.open();
// Extract the sort type from the bundle. getInt will return 0 if there is no attribute
// sort (which is exactly what we want)
try {
BookCataloguePreferences prefs = BookCatalogueApp.getAppPreferences();
// Restore bookshelf and position
mCurrentBookshelf = prefs.getString(PREF_BOOKSHELF, mCurrentBookshelf);
mTopRow = prefs.getInt(PREF_TOP_ROW, 0);
mTopRowTop = prefs.getInt(PREF_TOP_ROW_TOP, 0);
} catch (Exception e) {
Logger.logError(e);
}
// Restore view style
refreshStyle();
// This sets the search capability to local (application) search
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
// This sets the search capability to local (application) search
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
setContentView(R.layout.booksonbookshelf);
Intent intent = getIntent();
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
// Return the search results instead of all books (for the bookshelf)
mSearchText = intent.getStringExtra(SearchManager.QUERY).trim();
} else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
// Handle a suggestions click (because the suggestions all use ACTION_VIEW)
mSearchText = intent.getDataString();
}
if (mSearchText == null || mSearchText.equals(".")) {
mSearchText = "";
}
TextView searchTextView = (TextView) findViewById(R.id.search_text);
if (mSearchText.equals("")) {
searchTextView.setVisibility(View.GONE);
} else {
searchTextView.setVisibility(View.VISIBLE);
searchTextView.setText(getString(R.string.search) + ": " + mSearchText);
}
// We want context menus to be available
getListView().setOnItemLongClickListener(new OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, final int position, long id) {
mList.moveToPosition(position);
ArrayList<SimpleDialogItem> menu = new ArrayList<SimpleDialogItem>();
mListHandler.buildContextMenu(mList.getRowView(), menu);
if (menu.size() > 0) {
StandardDialogs.selectItemDialog(getLayoutInflater(), null, menu, null, new SimpleDialogOnClickListener() {
@Override
public void onClick(SimpleDialogItem item) {
mList.moveToPosition(position);
int id = ((SimpleDialogMenuItem)item).getItemId();
mListHandler.onContextItemSelected(mDb, mList.getRowView(), BooksOnBookshelf.this, mDb, id);
}});
}
return true;
}
});
// use the custom fast scroller (the ListView in the XML is our custome version).
getListView().setFastScrollEnabled(true);
// Handle item click events
getListView().setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View view, int position, long rowId) {
handleItemClick(arg0, view, position, rowId);
}});
// Debug; makes list structures vary across calls to ensure code is correct...
mMarkBookId = -1;
// This will cause the list to be generated.
initBookshelfSpinner();
setupList(true);
if (savedInstanceState == null) {
HintManager.displayHint(this, R.string.hint_view_only_book_details, null);
HintManager.displayHint(this, R.string.hint_book_list, null);
if (StartupActivity.getShowAmazonHint() && HintManager.shouldBeShown(R.string.hint_amazon_links_blurb) ) {
HintManager.displayHint(this, R.string.hint_amazon_links_blurb, null,
getString(R.string.amazon_books_by_author),
getString(R.string.amazon_books_in_series),
getString(R.string.amazon_books_by_author_in_series),
getString(R.string.app_name));
}
}
} finally {
Tracker.exitOnCreate(this);
}
}
/**
* Support routine now that this activity is no longer a ListActivity
*/
private ListView getListView() {
return (ListView)findViewById(android.R.id.list);
}
/**
* Handle a list item being clicked.
*
* @param arg0 Parent adapter
* @param view Row View that was clicked
* @param position Position of view in listView
* @param rowId _id field from cursor
*/
private void handleItemClick(AdapterView<?> arg0, View view, int position, long rowId) {
// Move the cursor to the position
mList.moveToPosition(position);
// If it's a book, edit it.
if (mList.getRowView().getKind() == RowKinds.ROW_KIND_BOOK) {
BookEdit.openBook(this, mList.getRowView().getBookId(), mList.getBuilder(), position);
// boolean isReadOnly = BookCatalogueApp.getAppPreferences()
// .getBoolean(BookCataloguePreferences.PREF_OPEN_BOOK_READ_ONLY, false);
// if (isReadOnly){
// BookEdit.viewBook(this, mList.getRowView().getBookId());
// } else {
// BookEdit.editBook(this, mList.getRowView().getBookId(), BookEdit.TAB_EDIT);
// }
} else {
// If it's leve1, expand/collapse. Technically, we could expand/collapse any level
// but storing and recovering the view becomes unmanageable.
if (mList.getRowView().getLevel() == 1) {
mList.getBuilder().toggleExpandNode(mList.getRowView().getAbsolutePosition());
mList.requery();
mAdapter.notifyDataSetChanged();
}
}
}
// /**
// * Build the context menu.
// */
// @Override
// public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
// super.onCreateContextMenu(menu, v, menuInfo);
// AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
//
// try {
// // Just move the cursor and call the handler to do the work.
// mList.moveToPosition(info.position);
// mListHandler.onCreateContextMenu(mList.getRowView(), menu);
// } catch (NullPointerException e) {
// Logger.logError(e);
// }
// }
/**
* Handle selections from context menu
*/
@Override
public boolean onContextItemSelected(android.view.MenuItem item) {
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
mList.moveToPosition(info.position);
if (mListHandler.onContextItemSelected(mDb, mList.getRowView(), this, mDb, item.getItemId()))
return true;
else
return super.onContextItemSelected(item);
}
/**
* Handle the style that a user has selected.
*
* @param name Name of the selected style
*/
private void handleSelectedStyle(String name) {
// Find the style, if no match warn user and exit
BooklistStyles styles = BooklistStyles.getAllStyles(mDb);
BooklistStyle style = styles.findCanonical(name);
if (style == null) {
Toast.makeText(this, "Could not find appropriate list", Toast.LENGTH_LONG).show();
return;
}
// Set the rebuild state like this is the first time in, which it sort of is, given we are changing style.
// There is very little ability to preserve position when going from a list sorted by author/series to
// on sorted by unread/addedDate/publisher. Keeping the current row/pos is probably the most useful
// thing we can do since we *may* come back to a similar list.
try {
ListView lv = getListView();
mTopRow = lv.getFirstVisiblePosition();
View v = lv.getChildAt(0);
mTopRowTop = v == null ? 0 : v.getTop();
} catch (Exception e) {};
// New style, so use user-pref for rebuild
mRebuildState = BooklistPreferencesActivity.getRebuildState();
// Do a rebuild
mCurrentStyle = style;
setupList(true);
}
/**
* Background task to build and retrieve the list of books based on current settings.
*
* @author Philip Warner
*/
private class GetListTask implements SimpleTask {
/** Indicates whole table structure needs rebuild, vs. just do a reselect of underlying data */
private final boolean mIsFullRebuild;
/** Resulting Cursor */
BooklistPseudoCursor mTempList = null;
/** used to determine new cursor position */
ArrayList<BookRowInfo> mTargetRows = null;
/**
* Constructor.
*
* @param isFullRebuild Indicates whole table structure needs rebuild, vs. just do a reselect of underlying data
*/
public GetListTask(boolean isFullRebuild) {
mIsFullRebuild = isFullRebuild;
}
@Override
public void run(SimpleTaskContext taskContext) {
try {
long t0 = System.currentTimeMillis();
// Build the underlying data
BooklistBuilder b = buildBooklist(mIsFullRebuild);
long t1 = System.currentTimeMillis();
// Try to sync the previously selected book ID
if (mMarkBookId != 0) {
// get all positions of the book
mTargetRows = b.getBookAbsolutePositions(mMarkBookId);
if (mTargetRows != null && mTargetRows.size() > 0) {
// First, get the ones that are currently visible...
ArrayList<BookRowInfo> visRows = new ArrayList<BookRowInfo>();
for(BookRowInfo i: mTargetRows) {
if (i.visible) {
visRows.add(i);
}
}
// If we have any visible rows, only consider them for the new position
if (visRows.size() > 0)
mTargetRows = visRows;
else {
// Make them ALL visible
for(BookRowInfo i: mTargetRows) {
if (!i.visible) {
b.ensureAbsolutePositionVisible(i.absolutePosition);
}
}
// Recalculate all positions
for(BookRowInfo i: mTargetRows) {
i.listPosition = b.getPosition(i.absolutePosition);
}
}
// // Find the nearest row to the recorded 'top' row.
// int targetRow = bookRows[0];
// int minDist = Math.abs(mTopRow - b.getPosition(targetRow));
// for(int i=1; i < bookRows.length; i++) {
// int pos = b.getPosition(bookRows[i]);
// int dist = Math.abs(mTopRow - pos);
// if (dist < minDist)
// targetRow = bookRows[i];
// }
// // Make sure the target row is visible/expanded.
// b.ensureAbsolutePositionVisible(targetRow);
// // Now find the position it will occupy in the view
// mTargetPos = b.getPosition(targetRow);
}
} else
mTargetRows = null;
long t2 = System.currentTimeMillis();
// Now we have expanded groups as needed, get the list cursor
mTempList = b.getList();
// Clear it so it wont be reused.
mMarkBookId = 0;
// get a count() from the cursor in background task because the setAdapter() call
// will do a count() and potentially block the UI thread while it pages through the
// entire cursor. If we do it here, subsequent calls will be fast.
long t3 = System.currentTimeMillis();
int count = mTempList.getCount();
long t4 = System.currentTimeMillis();
mUniqueBooks = mTempList.getUniqueBookCount();
long t5 = System.currentTimeMillis();
mTotalBooks = mTempList.getBookCount();
long t6 = System.currentTimeMillis();
System.out.println("Build: " + (t1-t0));
System.out.println("Position: " + (t2-t1));
System.out.println("Select: " + (t3-t2));
System.out.println("Count(" + count + "): " + (t4-t3) + "/" + (t5-t4) + "/" + (t6-t5));
System.out.println("====== " );
System.out.println("Total: " + (t6-t0));
// Save a flag to say list was loaded at least once successfully
mListHasBeenLoaded = true;
} finally {
if (taskContext.isTerminating()) {
// onFinish() will not be called, and we can discard our
// work...
if (mTempList != null && mTempList != mList) {
if (mList == null || mTempList.getBuilder() != mList.getBuilder())
try { mTempList.getBuilder().close(); } catch (Exception e) { /* Ignore */ };
try { mTempList.close(); } catch (Exception e) { /* Ignore */ };
}
}
}
}
@Override
public void onFinish(Exception e) {
// If activity dead, just do a local cleanup and exit.
if (mIsDead) {
mTempList.close();
return;
}
// Dismiss the progress dialog, if present
if (mListDialog != null && !mTaskQueue.hasActiveTasks()) {
mListDialog.dismiss();
mListDialog = null;
}
// Update the data
if (mTempList != null) {
displayList(mTempList, mTargetRows);
}
mTempList = null;
}
}
/**
* Queue a rebuild of the underlying cursor and data.
*
* @param isFullRebuild Indicates whole table structure needs rebuild, vs. just do a reselect of underlying data
*/
private void setupList(boolean isFullRebuild) {
isFullRebuild = true;
mTaskQueue.enqueue(new GetListTask(isFullRebuild));
if (mListDialog == null) {
mListDialog = ProgressDialog.show(this, "", getString(R.string.getting_books_ellipsis), true, true, new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
// Cancelling the list cancels the activity.
BooksOnBookshelf.this.finish();
dialog.dismiss();
mListDialog = null;
}});
}
}
/**
* Set the listview background based on user preferences
*/
private void initBackground() {
ListView lv = getListView();
View root = findViewById(R.id.root);
View header = findViewById(R.id.header);
//
// Sanity checks as a result of user bug report that was caused by either:
// (a) root being null
// or
// (b) getResources() returning null
//
if (root == null)
throw new RuntimeException("Sanity Check Fail: Root view not found; isFinishing() = " + isFinishing());
if (header == null)
throw new RuntimeException("Sanity Check Fail: Header view not found; isFinishing() = " + isFinishing());
if (getResources() == null)
throw new RuntimeException("Sanity Check Fail: getResources() returned null; isFinishing() = " + isFinishing());
if (BooklistPreferencesActivity.isBackgroundFlat() || BookCatalogueApp.isBackgroundImageDisabled()) {
final int backgroundColor = getResources().getColor(R.color.background_grey);
lv.setBackgroundColor(backgroundColor);
Utils.setCacheColorHintSafely(lv, backgroundColor);
if (BookCatalogueApp.isBackgroundImageDisabled()) {
root.setBackgroundColor(backgroundColor);
header.setBackgroundColor(backgroundColor);
} else {
Drawable d = Utils.makeTiledBackground(false);
root.setBackgroundDrawable(d);
header.setBackgroundDrawable(d);
// root.setBackgroundDrawable(Utils.cleanupTiledBackground(getResources().getDrawable(R.drawable.bc_background_gradient)));
// header.setBackgroundDrawable(Utils.cleanupTiledBackground(getResources().getDrawable(R.drawable.bc_vertical_gradient)));
}
} else {
Utils.setCacheColorHintSafely(lv, 0x00000000);
// ICS does not cope well with transparent ListView backgrounds with a 0 cache hint, but it does
// seem to cope with a background image on the ListView itself.
Drawable d = Utils.makeTiledBackground(false);
if (Build.VERSION.SDK_INT >= 11) {
// Honeycomb
lv.setBackgroundDrawable(d);
// lv.setBackgroundDrawable(Utils.cleanupTiledBackground(getResources().getDrawable(R.drawable.bc_background_gradient_dim)));
} else {
lv.setBackgroundColor(0x00000000);
}
root.setBackgroundDrawable(d);
// root.setBackgroundDrawable(Utils.cleanupTiledBackground(getResources().getDrawable(R.drawable.bc_background_gradient_dim)));
header.setBackgroundColor(0x00000000);
}
root.invalidate();
}
/**
* Fix background
*/
@Override
public void onResume() {
Tracker.enterOnResume(this);
super.onResume();
// Try to prevent null-pointer errors for rapidly pressing 'back'; this
// is in response to errors reporting NullPointerException when, most likely,
// a null is returned by getResources(). The most likely explanation for that
// is the call occurs after Activity is destroyed.
if (mIsDead)
return;
initBackground();
Tracker.exitOnResume(this);
}
/**
* Display the passed cursor in the ListView, and change the position to targetRow.
*
* @param newList New cursor to use
* @param targetPos
*/
private void displayList(BooklistPseudoCursor newList, final ArrayList<BookRowInfo> targetRows) {
if (newList == null) {
throw new RuntimeException("Unexpected empty list");
}
final int showHeaderFlags = (mCurrentStyle == null ? BooklistStyle.SUMMARY_SHOW_ALL : mCurrentStyle.getShowHeaderInfo());
initBackground();
TextView bookCounts = (TextView)findViewById(R.id.bookshelf_count);
if ( (showHeaderFlags & BooklistStyle.SUMMARY_SHOW_COUNT) != 0) {
if (mUniqueBooks != mTotalBooks)
bookCounts.setText("(" + this.getString(R.string.displaying_n_books_in_m_entries, mUniqueBooks, mTotalBooks) + ")");
else
bookCounts.setText("(" + this.getString(R.string.displaying_n_books, mUniqueBooks) + ")");
bookCounts.setVisibility(View.VISIBLE);
} else {
bookCounts.setVisibility(View.GONE);
}
long t0 = System.currentTimeMillis();
// Save the old list so we can close it later, and set the new list locally
BooklistPseudoCursor oldList = mList;
mList = newList;
// Get new handler and adapter since list may be radically different structure
mListHandler = new BooksMultitypeListHandler();
mAdapter = new MultitypeListAdapter(this, mList, mListHandler);
// Get the ListView and set it up
final ListView lv = (ListView)getListView();
final ListViewHolder lvHolder = new ListViewHolder();
ViewTagger.setTag(lv, R.id.TAG_HOLDER, lvHolder);
lv.setAdapter(mAdapter);
mAdapter.notifyDataSetChanged();
// Force a rebuild of FastScroller
lv.setFastScrollEnabled(false);
lv.setFastScrollEnabled(true);
// Restore saved position
final int count = mList.getCount();
try {
if (mTopRow >= count) {
mTopRow = count-1;
lv.setSelection(mTopRow);
} else {
lv.setSelectionFromTop(mTopRow, mTopRowTop);
}
} catch (Exception e) {}; // Don't really care
// If a target position array is set, then queue a runnable to set the position
// once we know how many items appear in a typical view and once we can tell
// if it is already in the view.
if (targetRows != null) {
// post a runnable to fix the position once the control is drawn
getListView().post(new Runnable() {
@Override
public void run() {
// Find the actual extend of the current view and get centre.
int first = lv.getFirstVisiblePosition();
int last = lv.getLastVisiblePosition();
int centre = (last+first)/2;
System.out.println("New List: (" + first + ", " + last + ")<-" + centre );
// Get the first 'target' and make it 'best candidate'
BookRowInfo best = targetRows.get(0);
int dist = Math.abs(best.listPosition - centre);
// Scan all other rows, looking for a nearer one
for (int i = 1; i < targetRows.size(); i++) {
BookRowInfo ri = targetRows.get(i);
int newDist = Math.abs(ri.listPosition - centre);
if (newDist < dist) {
dist = newDist;
best = ri;
}
}
System.out.println("Best @" + best.listPosition );
// Try to put at top if not already visible, or only partially visible
if (first >= best.listPosition || last <= best.listPosition) {
System.out.println("Adjusting position");
//
// setSelectionfromTop does not seem to always do what is expected.
// But adding smoothScrollToPosition seems to get the job done reasonably well.
//
// Specific problem occurs if:
// - put phone in portrait mode
// - edit a book near bottom of list
// - turn phone to landscape
// - save the book (don't cancel)
// Book will be off bottom of screen without the smoothScroll in the second Runnable.
//
lv.setSelectionFromTop(best.listPosition, 0);
// Code below does not behave as expected. Results in items often being near bottom.
//lv.setSelectionFromTop(best.listPosition, lv.getHeight() / 2);
// smoothScrollToPosition is only available at API level 8.
// Without this call some positioning may be off by one row (see above).
if (android.os.Build.VERSION.SDK_INT >= 8) {
final int newPos = best.listPosition;
getListView().post(new Runnable() {
@TargetApi(8)
@Override
public void run() {
lv.smoothScrollToPosition(newPos);
}});
}
//int newTop = best.listPosition - (last-first)/2;
//System.out.println("New Top @" + newTop );
//lv.setSelection(newTop);
}
}});
//}
}
final boolean hasLevel1 = (mList.numLevels() > 1);
final boolean hasLevel2 = (mList.numLevels() > 2);
if ( hasLevel2 && (showHeaderFlags & BooklistStyle.SUMMARY_SHOW_LEVEL_2) != 0 ) {
lvHolder.level2Text.setVisibility(View.VISIBLE);
lvHolder.level2Text.setText("");
} else {
lvHolder.level2Text.setVisibility(View.GONE);
}
if (hasLevel1 && (showHeaderFlags & BooklistStyle.SUMMARY_SHOW_LEVEL_1) != 0) {
lvHolder.level1Text.setVisibility(View.VISIBLE);
lvHolder.level1Text.setText("");
} else
lvHolder.level1Text.setVisibility(View.GONE);
// Update the header details
if (count > 0 && (showHeaderFlags & (BooklistStyle.SUMMARY_SHOW_LEVEL_1 ^ BooklistStyle.SUMMARY_SHOW_LEVEL_2)) != 0)
updateListHeader(lvHolder, mTopRow, hasLevel1, hasLevel2, showHeaderFlags);
// Define a scroller to update header detail when top row changes
lv.setOnScrollListener(new OnScrollListener() {
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// TODO: Investigate why BooklistPseudoCursor causes a scroll even when it is closed!
// Need to check isDead because BooklistPseudoCursor misbehaves when activity terminates and closes cursor
if (mLastTop != firstVisibleItem && !mIsDead && (showHeaderFlags != 0) ) {
ListViewHolder holder = (ListViewHolder)ViewTagger.getTag(view, R.id.TAG_HOLDER);
updateListHeader(holder, firstVisibleItem, hasLevel1, hasLevel2, showHeaderFlags);
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}}
);
if (mCurrentStyle == null)
this.getSupportActionBar().setSubtitle("");
else
this.getSupportActionBar().setSubtitle(mCurrentStyle.getDisplayName());
// Close old list
if (oldList != null) {
if (mList.getBuilder() != oldList.getBuilder())
oldList.getBuilder().close();
oldList.close();
}
long t1 = System.currentTimeMillis();
System.out.println("displayList: " + (t1 - t0));
}
/**
* Update the list header to match the current top item.
*
* @param holder Holder object for header
* @param topItem Top row
* @param hasLevel1 flag indicating level 1 is present
* @param hasLevel2 flag indicating level 2 is present
*/
private void updateListHeader(ListViewHolder holder, int topItem, boolean hasLevel1, boolean hasLevel2, int flags) {
if (topItem < 0)
topItem = 0;
mLastTop = topItem;
if (hasLevel1 && ( flags & BooklistStyle.SUMMARY_SHOW_LEVEL_1) != 0) {
if ( mList.moveToPosition(topItem) ) {
holder.level1Text.setText(mList.getRowView().getLevel1Data());
String s = null;
if (hasLevel2 && ( flags & BooklistStyle.SUMMARY_SHOW_LEVEL_2) != 0 ) {
s = mList.getRowView().getLevel2Data();
holder.level2Text.setText(s);
}
}
}
}
/**
* Build the underlying flattened list of books.
*
* @param isFullRebuild Indicates a complete structural rebuild is required
*
* @return The BooklistBuilder object used to build the data
*/
private BooklistBuilder buildBooklist(boolean isFullRebuild) {
// If not a full rebuild then just use the current builder to requery the underlying data
if (mList != null && !isFullRebuild) {
System.out.println("Doing rebuild()");
BooklistBuilder b = mList.getBuilder();
b.rebuild();
return b;
} else {
System.out.println("Doing full reconstruct");
// Make sure we have a style chosen
BooklistStyles styles = BooklistStyles.getAllStyles(mDb);
if (mCurrentStyle == null) {
String prefStyle = BookCatalogueApp.getAppPreferences().getString(BookCataloguePreferences.PREF_BOOKLIST_STYLE, getString(R.string.sort_author_series));
mCurrentStyle = styles.findCanonical(prefStyle);
if (mCurrentStyle == null)
mCurrentStyle = styles.get(0);
BookCatalogueApp.getAppPreferences().setString(BookCataloguePreferences.PREF_BOOKLIST_STYLE, mCurrentStyle.getCanonicalName());
}
// get a new builder and add the required extra domains
BooklistBuilder builder = new BooklistBuilder(mDb, mCurrentStyle);
builder.requireDomain(DOM_TITLE, TBL_BOOKS.dot(DOM_TITLE), true);
builder.requireDomain(DOM_READ, TBL_BOOKS.dot(DOM_READ), false);
// Build based on our current criteria and return
builder.build(mRebuildState, mMarkBookId, mCurrentBookshelf, "", "", "", "", mSearchText);
// After first build, always preserve this object state
mRebuildState = BooklistPreferencesActivity.BOOKLISTS_STATE_PRESERVED;
return builder;
}
}
/**
* record to hold the current ListView header details.
*
* @author Philip Warner
*/
private class ListViewHolder {
TextView level1Text;
TextView level2Text;
public ListViewHolder() {
level1Text = (TextView)findViewById(R.id.level_1_text);
level2Text = (TextView)findViewById(R.id.level_2_text);
}
}
/**
* Save current position information, including view nodes that are expanded.
*
* ENHANCE: Handle positions a little better when books are deleted.
*
* Deleting a book by 'n' authors from the last author in list results
* in the list decreasing in length by, potentially, n*2 items. The
* current 'savePosition()' code will return to the old position in the
* list after such an operation...which will be too far down.
*/
private void savePosition() {
if (mIsDead)
return;
final Editor ed = BookCatalogueApp.getAppPreferences().edit();
// Save position in list
if (mListHasBeenLoaded) {
final ListView lv = getListView();
mTopRow = lv.getFirstVisiblePosition();
ed.putInt(PREF_TOP_ROW, mTopRow);
View v = lv.getChildAt(0);
mTopRowTop = v == null ? 0 : v.getTop();
ed.putInt(PREF_TOP_ROW_TOP, mTopRowTop);
}
if (mCurrentStyle != null)
ed.putString(PREF_LIST_STYLE, mCurrentStyle.getCanonicalName());
ed.commit();
}
/**
* Save position when paused
*/
@Override
public void onPause() {
Tracker.enterOnPause(this);
super.onPause();
System.out.println("onPause");
if (mSearchText == null || mSearchText.equals(""))
savePosition();
if (isFinishing())
mTaskQueue.finish();
if (mListDialog != null)
mListDialog.dismiss();
Tracker.exitOnPause(this);
}
/**
* Cleanup
*/
@Override
public void onDestroy() {
Tracker.enterOnDestroy(this);
super.onDestroy();
System.out.println("onDestroy");
mIsDead = true;
mTaskQueue.finish();
try {
if (mList != null) {
try {
if ( mList.getBuilder() != null)
mList.getBuilder().close();
} catch (Exception e) {
Logger.logError(e);
}
mList.close();
}
mDb.close();
} catch (Exception e) {
Logger.logError(e);
};
mListHandler = null;
mAdapter = null;
mBookshelfSpinner = null;
mBookshelfAdapter = null;
synchronized(mInstanceCount) {
mInstanceCount--;
System.out.println("BoB instances: " + mInstanceCount);
}
TrackedCursor.dumpCursors();
Tracker.exitOnDestroy(this);
}
/**
* Setup the bookshelf spinner. This function will also call fillData when
* complete having loaded the appropriate bookshelf.
*/
private Spinner mBookshelfSpinner;
private ArrayAdapter<String> mBookshelfAdapter;
private void initBookshelfSpinner() {
// Setup the Bookshelf Spinner
mBookshelfSpinner = (Spinner) findViewById(R.id.bookshelf_name);
mBookshelfAdapter = new ArrayAdapter<String>(this, R.layout.spinner_frontpage);
mBookshelfAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mBookshelfSpinner.setAdapter(mBookshelfAdapter);
// Add the default All Books bookshelf
int pos = 0;
int bspos = pos;
mBookshelfAdapter.add(getString(R.string.all_books));
pos++;
Cursor bookshelves = mDb.fetchAllBookshelves();
if (bookshelves.moveToFirst()) {
do {
String this_bookshelf = bookshelves.getString(1);
if (this_bookshelf.equals(mCurrentBookshelf)) {
bspos = pos;
}
pos++;
mBookshelfAdapter.add(this_bookshelf);
}
while (bookshelves.moveToNext());
}
bookshelves.close(); // close the cursor
// Set the current bookshelf. We use this to force the correct bookshelf after
// the state has been restored.
mBookshelfSpinner.setSelection(bspos);
/**
* This is fired whenever a bookshelf is selected. It is also fired when the
* page is loaded with the default (or current) bookshelf.
*/
mBookshelfSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
public void onItemSelected(AdapterView<?> parentView, View view, int position, long id) {
// Check to see if mBookshelfAdapter is null, which should only occur if
// the activity is being torn down: see Issue 370.
if (mBookshelfAdapter == null)
return;
String new_bookshelf = mBookshelfAdapter.getItem(position);
if (position == 0) {
new_bookshelf = "";
}
if (!new_bookshelf.equalsIgnoreCase(mCurrentBookshelf)) {
mCurrentBookshelf = new_bookshelf;
// save the current bookshelf into the preferences
BookCataloguePreferences prefs = BookCatalogueApp.getAppPreferences();
SharedPreferences.Editor ed = prefs.edit();
ed.putString(PREF_BOOKSHELF, mCurrentBookshelf);
ed.commit();
setupList(true);
}
}
public void onNothingSelected(AdapterView<?> parentView) {
// Do Nothing
}
});
ImageView bookshelfDown = (ImageView) findViewById(R.id.bookshelf_down);
bookshelfDown.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mBookshelfSpinner.performClick();
return;
}
});
TextView bookshelfNum = (TextView) findViewById(R.id.bookshelf_num);
if (bookshelfNum != null) {
bookshelfNum.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mBookshelfSpinner.performClick();
return;
}
});
}
}
private MenuHandler mMenuHandler;
private static final int MNU_SORT = MenuHandler.FIRST+1;
private static final int MNU_EXPAND = MenuHandler.FIRST+2;
private static final int MNU_COLLAPSE = MenuHandler.FIRST+3;
private static final int MNU_EDIT_STYLE = MenuHandler.FIRST+4;
private static final int MNU_GOODREADS = MenuHandler.FIRST+5;
/**
* Run each time the menu button is pressed. This will setup the options menu
*/
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem i;
mMenuHandler = new MenuHandler();
mMenuHandler.init(menu);
mMenuHandler.addCreateBookItems(menu);
i = mMenuHandler.addItem(menu, MNU_SORT, R.string.sort_and_style_ellipsis, android.R.drawable.ic_menu_sort_alphabetically);
i.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
;
//mMenuHandler.addItem(menu, MNU_EDIT_STYLE, R.string.edit_style, android.R.drawable.ic_menu_manage);
mMenuHandler.addItem(menu, MNU_EXPAND, R.string.menu_sort_by_author_expanded, R.drawable.ic_menu_expand);
mMenuHandler.addItem(menu, MNU_COLLAPSE, R.string.menu_sort_by_author_collapsed, R.drawable.ic_menu_collapse);
mMenuHandler.addSearchItem(menu)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
final boolean showGr = GoodreadsManager.hasCredentials();
if (showGr) {
mMenuHandler.addItem(menu, MNU_GOODREADS, R.string.goodreads, R.drawable.ic_menu_gr_logo);
}
mMenuHandler.addCreateHelpAndAdminItems(menu);
return super.onPrepareOptionsMenu(menu);
}
/**
* This will be called when a menu item is selected. A large switch statement to
* call the appropriate functions (or other activities)
*/
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
if (mMenuHandler != null && !mMenuHandler.onMenuItemSelected(this, featureId, item)) {
switch(item.getItemId()) {
case MNU_SORT:
HintManager.displayHint(this, R.string.hint_booklist_style_menu, new Runnable() {
@Override
public void run() {
doSortMenu(false);
}});
return true;
case MNU_EDIT_STYLE:
doEditStyle();
return true;
case MNU_EXPAND:
{
// It is possible that the list will be empty, if so, ignore
if (getListView().getChildCount() != 0) {
int oldAbsPos = mListHandler.getAbsolutePosition(getListView().getChildAt(0));
savePosition();
mList.getBuilder().expandAll(true);
mTopRow = mList.getBuilder().getPosition(oldAbsPos);
BooklistPseudoCursor newList = mList.getBuilder().getList();
displayList(newList, null);
}
break;
}
case MNU_COLLAPSE:
{
// It is possible that the list will be empty, if so, ignore
if (getListView().getChildCount() != 0) {
int oldAbsPos = mListHandler.getAbsolutePosition(getListView().getChildAt(0));
savePosition();
mList.getBuilder().expandAll(false);
mTopRow = mList.getBuilder().getPosition(oldAbsPos);
displayList(mList.getBuilder().getList(), null);
}
break;
}
case MNU_GOODREADS:
{
GoodreadsUtils.showGoodreadsOptions(this);
break;
}
/*
case INSERT_ID:
createBook();
return true;
case INSERT_ISBN_ID:
createBookISBN("isbn");
return true;
case INSERT_BARCODE_ID:
createBookScan();
return true;
case ADMIN:
// Start the Main Menu, not just the Admin page
mainMenuPage();
return true;
case SEARCH:
onSearchRequested();
return true;
case INSERT_NAME_ID:
createBookISBN("name");
return true;
*/
}
}
return super.onMenuItemSelected(featureId, item);
}
/**
* Called when an activity launched exits, giving you the requestCode you started it with,
* the resultCode it returned, and any additional data from it.
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
System.out.println("In onActivityResult for BooksOnBookshelf for request " + requestCode);
mMarkBookId = 0;
switch(requestCode) {
case UniqueId.ACTIVITY_CREATE_BOOK_SCAN:
try {
if (intent != null && intent.hasExtra(CatalogueDBAdapter.KEY_ROWID)) {
long newId = intent.getLongExtra(CatalogueDBAdapter.KEY_ROWID, 0);
if (newId != 0) {
mMarkBookId = newId;
}
}
// Always rebuild, even after a cancelled edit because the series may have had global edits
// ENHANCE: Allow detection of global changes to avoid unnecessary rebuilds
this.setupList(false);
} catch (NullPointerException e) {
// This is not a scan result, but a normal return
//fillData();
}
break;
case UniqueId.ACTIVITY_CREATE_BOOK_ISBN:
case UniqueId.ACTIVITY_CREATE_BOOK_MANUALLY:
case UniqueId.ACTIVITY_VIEW_BOOK:
case UniqueId.ACTIVITY_EDIT_BOOK:
try {
if (intent != null && intent.hasExtra(CatalogueDBAdapter.KEY_ROWID)) {
long id = intent.getLongExtra(CatalogueDBAdapter.KEY_ROWID, 0);
if (id != 0) {
mMarkBookId = id;
}
}
// Always rebuild, even after a cancelled edit because the series may have had global edits
// ENHANCE: Allow detection of global changes to avoid unnecessary rebuilds
this.setupList(false);
} catch (Exception e) {
Logger.logError(e);
}
break;
case UniqueId.ACTIVITY_BOOKLIST_STYLE_PROPERTIES:
try {
if (intent != null && intent.hasExtra(BooklistStylePropertiesActivity.KEY_STYLE)) {
BooklistStyle style = (BooklistStyle)intent.getSerializableExtra(BooklistStylePropertiesActivity.KEY_STYLE);
if (style != null)
mCurrentStyle = style;
}
} catch (Exception e) {
Logger.logError(e);
}
this.savePosition();
this.setupList(true);
break;
case UniqueId.ACTIVITY_BOOKLIST_STYLES:
case UniqueId.ACTIVITY_ADMIN:
case UniqueId.ACTIVITY_PREFERENCES:
// Refresh the style because prefs may have changed
refreshStyle();
this.savePosition();
this.setupList(true);
break;
//case ACTIVITY_SORT:
//case ACTIVITY_ADMIN:
/*
try {
// Use the ADDED_* fields if present.
if (intent != null && intent.hasExtra(BookEditFields.ADDED_HAS_INFO)) {
if (sort == SORT_TITLE) {
justAdded = intent.getStringExtra(BookEditFields.ADDED_TITLE);
int position = mDbHelper.fetchBookPositionByTitle(justAdded, bookshelf);
adjustCurrentGroup(position, 1, true, false);
} else if (sort == SORT_AUTHOR) {
justAdded = intent.getStringExtra(BookEditFields.ADDED_AUTHOR);
int position = mDbHelper.fetchAuthorPositionByName(justAdded, bookshelf);
adjustCurrentGroup(position, 1, true, false);
} else if (sort == SORT_AUTHOR_GIVEN) {
justAdded = intent.getStringExtra(BookEditFields.ADDED_AUTHOR);
int position = mDbHelper.fetchAuthorPositionByGivenName(justAdded, bookshelf);
adjustCurrentGroup(position, 1, true, false);
} else if (sort == SORT_SERIES) {
justAdded = intent.getStringExtra(BookEditFields.ADDED_SERIES);
int position = mDbHelper.fetchSeriesPositionBySeries(justAdded, bookshelf);
adjustCurrentGroup(position, 1, true, false);
} else if (sort == SORT_GENRE) {
justAdded = intent.getStringExtra(BookEditFields.ADDED_GENRE);
int position = mDbHelper.fetchGenrePositionByGenre(justAdded, bookshelf);
adjustCurrentGroup(position, 1, true, false);
}
}
} catch (Exception e) {
Logger.logError(e);
}
*/
// We call bookshelf not fillData in case the bookshelves have been updated.
}
}
/**
* Update and/or create the current style definition.
*/
private void refreshStyle() {
BooklistStyles styles = BooklistStyles.getAllStyles(mDb);
String styleName;
if (mCurrentStyle == null) {
BookCataloguePreferences prefs = BookCatalogueApp.getAppPreferences();
styleName = prefs.getString(PREF_LIST_STYLE, "");
} else {
styleName = mCurrentStyle.getCanonicalName();
}
BooklistStyle style = styles.findCanonical(styleName);
if (style != null)
mCurrentStyle = style;
if (mCurrentStyle == null)
mCurrentStyle = styles.get(0);
}
/**
* Setup the sort options. This function will also call fillData when
* complete having loaded the appropriate view.
*/
private void doSortMenu(final boolean showAll) {
LayoutInflater inf = this.getLayoutInflater();
View root = inf.inflate(R.layout.booklist_style_menu, null);
RadioGroup group = (RadioGroup)root.findViewById(R.id.radio_buttons);
LinearLayout main = (LinearLayout)root.findViewById(R.id.menu);
final AlertDialog sortDialog = new AlertDialog.Builder(this).setView(root).create();
sortDialog.setTitle(R.string.select_style);
sortDialog.show();
Iterator<BooklistStyle> i;
if (!showAll)
i = BooklistStyles.getPreferredStyles(mDb).iterator();
else
i = BooklistStyles.getAllStyles(mDb).iterator();
while(i.hasNext()) {
BooklistStyle style = i.next();
makeRadio(sortDialog, inf, group, style);
}
int moreLess;
if (showAll)
moreLess = R.string.show_fewer_ellipsis;
else
moreLess = R.string.show_more_ellipsis;
makeText(main, inf, moreLess, new OnClickListener() {
@Override
public void onClick(View v) {
sortDialog.dismiss();
doSortMenu(!showAll);
}});
makeText(main, inf, R.string.customize_ellipsis, new OnClickListener() {
@Override
public void onClick(View v) {
sortDialog.dismiss();
BooklistStyles.startEditActivity(BooksOnBookshelf.this);
}});
}
/**
* Add a radio box to the sort options dialogue.
*
* @param sortDialog
* @param group
* @param style
*/
private void makeRadio (final AlertDialog sortDialog, final LayoutInflater inf, RadioGroup group, final BooklistStyle style) {
View v = inf.inflate(R.layout.booklist_style_menu_radio, null);
RadioButton btn = (RadioButton)v;
btn.setText(style.getDisplayName());
if (mCurrentStyle.getCanonicalName().equalsIgnoreCase(style.getCanonicalName())) {
btn.setChecked(true);
} else {
btn.setChecked(false);
}
group.addView(btn);
btn.setOnClickListener( new OnClickListener() {
@Override
public void onClick(View v) {
handleSelectedStyle(style.getCanonicalName());
sortDialog.dismiss();
return;
}
});
}
/**
* Add a text box to the sort options dialogue.
*
* @param sortDialog
* @param stringId
* @param listener
*/
private void makeText (final LinearLayout parent, final LayoutInflater inf, final int stringId, OnClickListener listener) {
TextView view = (TextView)inf.inflate(R.layout.booklist_style_menu_text, null);
Typeface tf = view.getTypeface();
view.setTypeface(tf, Typeface.ITALIC);
view.setText(stringId);
view.setOnClickListener( listener );
parent.addView(view);
}
/**
* Start the BooklistPreferences Activity
*/
public void doEditStyle() {
Intent i = new Intent(this, BooklistStylePropertiesActivity.class);
i.putExtra(BooklistStylePropertiesActivity.KEY_STYLE, mCurrentStyle);
i.putExtra(BooklistStylePropertiesActivity.KEY_SAVE_TO_DATABASE, false);
startActivityForResult(i, UniqueId.ACTIVITY_BOOKLIST_STYLE_PROPERTIES);
}
@Override
public void onBooklistChange(int flags) {
if (flags != 0) {
// Author or series changed. Just regenerate.
savePosition();
this.setupList(true);
}
}
/**
* TODO DEBUG ONLY. Count instances
*/
public BooksOnBookshelf() {
super();
synchronized(mInstanceCount) {
mInstanceCount++;
System.out.println("BoB instances: " + mInstanceCount);
}
}
}