/*
* @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 java.util.Timer;
import java.util.TimerTask;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.eleybourn.bookcatalogue.compat.BookCatalogueActivity;
import com.eleybourn.bookcatalogue.utils.Utils;
/**
* Catalogue search based on the SQLite FTS engine. Due to the speed of FTS it updates the
* number of hits more or less in real time. The user can choose to see a full list at any
* time.
*
* ENHANCE: Finish or DELETE FTS activity.
*
* @author Philip Warner
*/
public class SearchCatalogue extends BookCatalogueActivity {
private CatalogueDBAdapter mDbHelper;
/** Indicates user has changed something since the last search. */
private boolean mSearchDirty = false;
/** Timer reset each time the user clicks, in order to detect an idle time */
private long mIdleStart = 0;
/** Timer object for background idle searches */
private Timer mTimer;
/** Handle inter-thread messages */
Handler m_handler = new Handler();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get the DB and setup the layout.
mDbHelper = new CatalogueDBAdapter(this);
mDbHelper.open();
setContentView(R.layout.search_catalogue_criteria);
View layout = this.findViewById(R.id.layout_root);
EditText criteria = (EditText) this.findViewById(R.id.criteria);
EditText author = (EditText) this.findViewById(R.id.author);
EditText title = (EditText) this.findViewById(R.id.title);
Button showResults = (Button) this.findViewById(R.id.search);
Button ftsRebuild = (Button) this.findViewById(R.id.rebuild);
// If the user touches anything, it's not idle
layout.setOnTouchListener(mOnTouchListener);
// If the user changes any text, it's not idle
author.addTextChangedListener(mTextWatcher);
title.addTextChangedListener(mTextWatcher);
criteria.addTextChangedListener(mTextWatcher);
// Handle button presses
showResults.setOnClickListener(mShowResultsListener);
ftsRebuild.setOnClickListener(mFtsRebuildListener);
// Note: Timer will be started in OnResume().
Utils.initBackground(R.drawable.bc_background_gradient_dim, this, R.id.layout_root, false);
}
/** start the idle timer */
public void startIdleTimer()
{
// Synchronize since this is relevant to more than 1 thread.
synchronized(SearchCatalogue.this) {
if (mTimer != null)
return;
mTimer = new Timer();
mIdleStart = System.currentTimeMillis();
}
//create timer to tick every 200ms
mTimer.schedule(new SearchUpdateTimer(), 0, 250);
}
/**
* Stop the timer.
*/
private void stopIdleTimer() {
Timer tmr;
// Synchronize since this is relevant to more than 1 thread.
synchronized(SearchCatalogue.this) {
tmr = mTimer;
mTimer = null;
}
if (tmr != null)
tmr.cancel();
}
/**
* Class to implement a timer task and do a search when necessary, if idle.
*
* If a search happens, we stop the idle timer.
*
* @author Philip Warner
*
*/
private class SearchUpdateTimer extends TimerTask {
@Override
public void run() {
boolean doSearch = false;
// Synchronize since this is relevant to more than 1 thread.
synchronized(SearchCatalogue.this) {
long timeNow = System.currentTimeMillis();
boolean idle = (timeNow - mIdleStart) > 1000;
if (idle) {
// Stop the timer, it will be restarted if the user changes something
stopIdleTimer();
if (mSearchDirty) {
doSearch = true;
mSearchDirty = false;
}
}
}
if (doSearch)
doSearch();
}
};
/**
* Handle the 'Search' button.
*/
private OnClickListener mShowResultsListener = new OnClickListener() {
@Override
public void onClick(View v) {
doSearch();
}
};
/**
* Handle the 'FTS Rebuild' button.
*/
private OnClickListener mFtsRebuildListener = new OnClickListener() {
@Override
public void onClick(View v) {
mDbHelper.rebuildFts();
}
};
/**
* Called in the timer thread, this code will run the search then queue the UI
* updates to the main thread.
*/
private void doSearch() {
// Get search criteria
String author = ((EditText) this.findViewById(R.id.author)).getText().toString();
String title = ((EditText) this.findViewById(R.id.title)).getText().toString();
String criteria = ((EditText) this.findViewById(R.id.criteria)).getText().toString();
String tmpMsg;
// Save time to log how long query takes.
long t0 = System.currentTimeMillis();
//BooksCursor c = mDbHelper.fetchAllBooks(""/*order*/, ""/*bookshelf*/,
// "(" + CatalogueDBAdapter.KEY_FAMILY_NAME + " like '%" + author + "%' " + CatalogueDBAdapter.COLLATION + " or " + CatalogueDBAdapter.KEY_GIVEN_NAMES + " like '%" + author + "%' " + CatalogueDBAdapter.COLLATION + ")",
// "b." + CatalogueDBAdapter.KEY_TITLE + " like '%" + title + "%' " + CatalogueDBAdapter.COLLATION + ",
// ""/*searchText*/, ""/*loaned_to*/, ""/*seriesName*/);
// Get the cursor
Cursor c = null;
try {
c = mDbHelper.searchFts(author, title, criteria);
if (c == null) {
// Null return means searchFts thought parameters were effectively blank
tmpMsg = "(enter search criteria)";
} else {
int count = c.getCount();
c.close();
t0 = System.currentTimeMillis() - t0;
tmpMsg = "(" + count + " books found in " + t0 + "ms)";
}
} catch (Exception e) {
tmpMsg = e.getMessage();
} finally {
// Cleanup cursor
try {
if (c != null)
c.close();
} catch (Exception e) {}
}
final String message = tmpMsg;
// Update the UI in main thread.
m_handler.post(new Runnable(){
@Override
public void run() {
TextView booksFound = (TextView) SearchCatalogue.this.findViewById(R.id.books_found);
booksFound.setText(message);
}
});
}
/**
* Called when a UI element detects the user doing something
*
* @param dirty Indicates the user action made the last search invalid
*/
private void userIsActive(boolean dirty) {
synchronized(SearchCatalogue.this) {
// Mark search dirty if necessary
mSearchDirty = mSearchDirty || dirty;
// Reset the idle timer since the user did something
mIdleStart = System.currentTimeMillis();
// If the search is dirty, make sure idle timer is running and update UI
if (mSearchDirty) {
TextView booksFound = (TextView) SearchCatalogue.this.findViewById(R.id.books_found);
booksFound.setText("(waiting for idle)");
startIdleTimer(); // (if not started)
}
}
}
/**
* Detect when user touches something, just so we know they are 'busy'.
*/
private OnTouchListener mOnTouchListener = new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
userIsActive(false);
return false;
}
};
/**
* Detect text changes and call userIsActive(...).
*/
private TextWatcher mTextWatcher = new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
userIsActive(true);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
};
/**
* When activity pauses, stop timer.
*/
@Override
protected void onPause() {
super.onPause();
stopIdleTimer();
}
/**
* When activity resumes, mark search as dirty
*/
@Override
protected void onResume() {
super.onResume();
userIsActive(true);
Utils.initBackground(R.drawable.bc_background_gradient_dim, this, false);
}
/**
* Cleanup
*/
@Override
public void onDestroy() {
super.onDestroy();
try {
if (mDbHelper != null)
mDbHelper.close();
} catch (Exception e) {}
try {
stopIdleTimer();
} catch (Exception e) {}
}
}