Back to project page Books.
The source code is released under:
Apache License
If you think the Android project Books listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
package com.contender.books; /* www .j a v a 2s. c om*/ import java.io.IOException; import java.util.Calendar; import java.util.TimeZone; import android.app.Activity; import android.app.AlertDialog; import android.app.DatePickerDialog; import android.app.DialogFragment; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.provider.CalendarContract.Events; import android.provider.ContactsContract; import android.text.format.DateFormat; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.CheckBox; import android.widget.DatePicker; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import com.google.api.client.extensions.android.http.AndroidHttp; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.services.books.Books; import com.google.api.services.books.Books.Volumes.List; import com.google.api.services.books.BooksRequestInitializer; import com.google.api.services.books.model.Volume; import com.google.api.services.books.model.Volumes; /** * Implements the adding and editing of books * <p> * When started with {@link com.contender.books.MainView.ACTION_ADD_ISBN ISBN intent}, * also retrieves the book title and author associated with this ISBN number. * Adds/deletes/updates Calendar events associated with book entries. * * @author Paul Klinkenberg <pklinken@gmail.com> * @version {@value com.contender.books.MainView#BOOKS_VERSION} */ public class BookActivity extends Activity implements DatePickerDialog.OnDateSetListener { private static final String STATE_DID_ISBN_LOOKUP = "stateDidIsbnLookup"; private static final String STATE_HAS_REMINDER = "stateHasReminder"; private static final String STATE_FETCHED_ROW = "stateFetchedRow"; private static final String STATE_INITIALIZED_DATE = "stateInitializedDate"; /** * Shows if the activity has previously set the Date * <p> * Used to prevent the activity from re-setting the date during its lifecycle */ private boolean initializedDate = false; /** * Shows if the activity has previously fetched a row from the DB * <p> * On starting the activity this value (and the Intent) determines if the passed * row should be fetched from the DB and loaded into the UI. */ private boolean fetchedRow = false; /** * Shows if the activity has previously succesfully retrieved the title and author * <p> * On starting the activity this value (and the Intent) is used to determine ifr * the BookResolver should be run again. */ private boolean didIsbnLookup = false; private static final String TAG = "BookActivity"; /** * Shows if an existing book entry already has a calendar event. * <p> * When editing an existing book entry, shows if it already has a calendar entry * associated with it, as to determine wether an existing calendar entry should * be updated/deleted, or a new one inserted. */ private boolean hasCalendarEvent = false; /** * Represents the current date shown in the DatePicker. Value also gets * changed by {@link DatePickerFragment} */ public static Calendar dueDate; /** * Retrieves a book name and author associated with a give ISBN number from * the google Books API. * <p> * Sets the Book title and author Views to the retrieved value on success * Shows a Toast message informing the user on failure. * @see AsyncTask * @see Toast * @author Paul Klinkenberg */ private class BookResolver extends AsyncTask<String, Void, Integer> { private static final String APPLICATION_NAME = "Contender-Books/0.1"; private JsonFactory json = JacksonFactory.getDefaultInstance(); private Books book; private String isbn, bookTitle, author; @Override protected void onPreExecute() { } @Override protected Integer doInBackground(String... args) { if(args.length < 1) { cancel(true); return null; } isbn = new String(args[0]); book = new Books.Builder(AndroidHttp.newCompatibleTransport(), json, null) .setApplicationName(APPLICATION_NAME) .setGoogleClientRequestInitializer(new BooksRequestInitializer(ClientCredentials.API_KEY)) .build(); List volumesList; try { volumesList = book.volumes().list("isbn:" + isbn); } catch (IOException e) { cancel(true); Log.e(TAG, "Error:" + e.getLocalizedMessage()); return null; } Volumes searchResult; try { searchResult = volumesList.execute(); } catch (IOException e) { cancel(true); Log.e(TAG, "Error:" + e.getLocalizedMessage()); return null; } if (searchResult.getTotalItems() == 0 || searchResult.getItems() == null) { System.out.println("No matches found."); cancel(true); return null; } // Not sure if ISBN query can return > 1 results, will just use the first. for(Volume volume : searchResult.getItems()) { Volume.VolumeInfo volumeInfo = volume.getVolumeInfo(); bookTitle = new String(volumeInfo.getTitle()); java.util.List<String> authors = volumeInfo.getAuthors(); if (authors != null && !authors.isEmpty()) { author = new String(authors.get(0)); } else author = new String("Unknown"); break; } return 0; } @Override protected void onCancelled(Integer result) { Context context = getApplicationContext(); Resources res = getResources(); CharSequence text = res.getString(R.string.bookresolver_cancelled); int duration = Toast.LENGTH_LONG; Toast toast = Toast.makeText(context, text, duration); toast.show(); } @Override protected void onPostExecute(Integer result) { if(result == 0) { // doInBackground() completed succesfully, set UI to reflect fetched data. TextView editTitle = (TextView) findViewById(R.id.editBookTitle); TextView editAuthor = (TextView) findViewById(R.id.editAuthor); editTitle.setText(bookTitle); editAuthor.setText(author); didIsbnLookup = true; } } } /** * Sets up autocompletion for the Contact name View * <p> * Retrieves a list of all primary contact names from the Contacts content provider * and links that to the AutoCompleteTextView.<br> * Set to start autocompleting at 3 characters or more. */ private void initContactAutocomplete() { String[] mProjection = { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME_PRIMARY }; Cursor mCursor = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, mProjection, null, null, null); AutoCompleteTextView editContact = (AutoCompleteTextView) findViewById(R.id.editContact); int columnIndex = mCursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY); String [] contacts = new String [mCursor.getCount()]; while(mCursor.moveToNext()) { contacts[mCursor.getPosition()] = mCursor.getString(columnIndex); } ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, contacts); editContact.setAdapter(adapter); editContact.setThreshold(3); mCursor.close(); } /** * Creates and opens a DatePickerFragment dialog. * @see DatePickerDialog * @param view view that called this functions. */ public void changeDate(View view) { DialogFragment newFragment = new DatePickerFragment(); newFragment.show(getFragmentManager(), "datePicker"); } /** * Implements {@link DatePickerDialog#OnDateSetListener} */ @Override public void onDateSet(DatePicker view, int year, int month, int day) { dueDate.set(Calendar.YEAR, year); dueDate.set(Calendar.MONTH, month); dueDate.set(Calendar.DAY_OF_MONTH, day); TextView editDueDate = (TextView) findViewById(R.id.editDuedate); java.text.DateFormat df = DateFormat.getMediumDateFormat(this); CharSequence format = df.format(dueDate.getTime()); //CharSequence format = DateFormat.format("MMM d, yyyy", dueDate); editDueDate.setText(format); } /** * Creates a new calendar event * <p> * Uses the Book title and dueDate information from the UI views to set up the event * @return Event ID of created event */ private long createCalendarEvent() { // Set up the event data and insert it Resources res = getResources(); ContentResolver cr = getContentResolver(); ContentValues values = new ContentValues(); TimeZone timeZone = TimeZone.getDefault(); values.put(Events.DTSTART, dueDate.getTimeInMillis()); values.put(Events.DTEND, dueDate.getTimeInMillis()); values.put(Events.EVENT_TIMEZONE, timeZone.getID()); values.put(Events.TITLE, res.getString(R.string.cal_event_title)); TextView view = (TextView) findViewById(R.id.editBookTitle); values.put(Events.DESCRIPTION, res.getString(R.string.cal_event_body, (view != null ? view.getText() : "(view object is null"))); values.put(Events.CALENDAR_ID, 3); // The calendar ID 3 is from the API examples itself but no explanation given. Uri uri = cr.insert(Events.CONTENT_URI, values); long eventID = Long.parseLong(uri.getLastPathSegment()); return eventID; } /** * Updates an existing calendar event * <p> * Finds the the event ID associated with the book where row IS _ID.<br> * Updates the date of the event at event ID. * * @param row row whose associated event is to be updated. */ private void updateCalendarEvent(int row) { int column; long eventID; Cursor cursor = MainView.BooksProvider.query(BooksStorage.BOOKS_TABLE_NAME, new String[] { BooksStorage.COLUMN_NAME_CALENDAREVENTID }, BooksStorage.COLUMN_NAME__ID + " IS ?", new String[] { new String(Integer.toString(row)) }, null); if(cursor == null || !cursor.moveToFirst()) { // Something went wrong Log.e(TAG, "Received empty cursor."); return; } column = cursor.getColumnIndex(BooksStorage.COLUMN_NAME_CALENDAREVENTID); eventID = cursor.getLong(column); cursor.close(); ContentResolver cr = getContentResolver(); ContentValues values = new ContentValues(); Uri updateUri = null; // FIXME: Should also update the book name if that is changed, little sense though it would make values.put(Events.DTSTART, dueDate.getTimeInMillis()); values.put(Events.DTEND, dueDate.getTimeInMillis()); updateUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventID); int rows; try { rows = cr.update(updateUri, values, null, null); } catch (NullPointerException e) { Log.e(TAG, "Error: " + e.getLocalizedMessage()); return; } Log.d(TAG, "Rows updated: " + rows); } /** * Deletes a calendar event * <p> * Finds the the event ID associated with the book where row IS _ID and * deletes this event. * @param row row whose associated event is to be deleted. */ private void deleteCalendarEvent(int row) { int column; long eventID; Cursor cursor = MainView.BooksProvider.query(BooksStorage.BOOKS_TABLE_NAME, new String[] { BooksStorage.COLUMN_NAME_CALENDAREVENTID }, BooksStorage.COLUMN_NAME__ID + " IS ?", new String[] { new String(Integer.toString(row)) }, null); if(cursor == null || !cursor.moveToFirst()) { // Something went wrong Log.e(TAG, "Received empty cursor."); return; } column = cursor.getColumnIndex(BooksStorage.COLUMN_NAME_CALENDAREVENTID); eventID = cursor.getLong(column); cursor.close(); ContentResolver cr = getContentResolver(); Uri deleteUri = null; deleteUri = ContentUris.withAppendedId(Events.CONTENT_URI, eventID); int rows = cr.delete(deleteUri, null, null); Log.d(TAG, "Rows deleted: " + rows); } /** * Insert or update an entry in the DB. * <p> * First checks that no required fields are left empty, shows a Toast and * returns if this is the case.<br> * If activity was called to add a new book, inserts it and calls {@link #createCalendarEvent}<br> * If activity was called to edit an existing book, update it in the DB and <br> * create/update/delete a calendar event as needed. * * @see #createCalendarEvent * @see #updateCalendarEvent * @see #deleteCalendarEvent * @see Toast * * @param view view that was clicked to call this function. */ public void submitBook(View view) { // New entries (ADD_MANUAL and ADD_ISBN) require an insert of // Bookname, Contact, Author, Duedate, Loandate, Hasreminder of which // only author can be null. // Updated entries have the same NONNULL requirement, but the // column loandate will not be updated. Calendar rightNow = Calendar.getInstance(); ContentValues values = new ContentValues(); Resources res = getResources(); EditText editView = (EditText) findViewById(R.id.editBookTitle); if(editView == null || editView.getText().toString().equals("")) { // Alert the user of this field being required Context context = getApplicationContext(); CharSequence text = res.getString(R.string.submit_book_title); int duration = Toast.LENGTH_LONG; Toast toast = Toast.makeText(context, text, duration); toast.show(); return; } values.put(BooksStorage.COLUMN_NAME_BOOK, editView.getText().toString()); editView = (EditText) findViewById(R.id.editContact); if(editView == null || editView.getText().toString().equals("")) { // Alert the user of this field being required Context context = getApplicationContext(); CharSequence text = res.getString(R.string.submit_book_name); int duration = Toast.LENGTH_LONG; Toast toast = Toast.makeText(context, text, duration); toast.show(); return; } values.put(BooksStorage.COLUMN_NAME_CONTACT, editView.getText().toString()); editView = (EditText) findViewById(R.id.editAuthor); if(editView == null) { // Author field can be empty but this should still not be null. return; } values.put(BooksStorage.COLUMN_NAME_AUTHOR, editView.getText().toString()); long date = dueDate.getTimeInMillis(); if(date < rightNow.getTimeInMillis()) { // Cannot give a return date that is in the past Context context = getApplicationContext(); CharSequence text = res.getString(R.string.submit_book_date); int duration = Toast.LENGTH_LONG; Toast toast = Toast.makeText(context, text, duration); toast.show(); return; } values.put(BooksStorage.COLUMN_NAME_DUEDATE, dueDate.getTimeInMillis()); CheckBox reminder = (CheckBox) findViewById(R.id.checkBoxCalendar); values.put(BooksStorage.COLUMN_NAME_HASREMINDER, reminder.isChecked()); if(getIntent().getAction() != MainView.ACTION_EDIT) { // This intent is not an edit, add loandate column, create calendar event // if requested by user and insert row. if(reminder.isChecked()) { long eventID = createCalendarEvent(); values.put(BooksStorage.COLUMN_NAME_CALENDAREVENTID, eventID); } values.put(BooksStorage.COLUMN_NAME_LOANDATE, rightNow.getTimeInMillis()); if(MainView.BooksProvider.insert(BooksStorage.BOOKS_TABLE_NAME, values) < 0) { Log.e(TAG, "Error inserting row in books table"); } finish(); } else { // This intent is an edit, get row # from Intent payload and update int row = getIntent().getIntExtra(MainView.EXTRA_INDEX, -1); if(row < 0) { // Did not receive valid row with Intent Log.e(TAG, "Did not receive valid row value with EDIT intent"); finish(); } if(hasCalendarEvent && reminder.isChecked()) { // Book already has a calendar event, update it updateCalendarEvent(row); } else if(hasCalendarEvent && !reminder.isChecked()) { // Book has a calender event but the user wants to delete it deleteCalendarEvent(row); } else if(!hasCalendarEvent && reminder.isChecked()) { // Book does not have a calendar event but user wants it now, create it long eventID = createCalendarEvent(); values.put(BooksStorage.COLUMN_NAME_CALENDAREVENTID, eventID); } else { // !hasCalendarEvent && !reminder.isChecked() // Book does not have a calendar event and user doesnt want one, do nothing } if( MainView.BooksProvider.update(BooksStorage.BOOKS_TABLE_NAME, values, BooksStorage.COLUMN_NAME__ID + " IS ?", new String[] { new String(Integer.toString(row)) }) < 0) { Log.e(TAG, "Error updating row in books table."); } finish(); } } /** * Fetches book at row from the DB and copies its data to the UI elements * <p> * Also sets {@link #hasCalendarEvent} * @param row row ID of the book to be fetched */ private void fetchRow(int row) { int column; Cursor cursor = MainView.BooksProvider.query(BooksStorage.BOOKS_TABLE_NAME, new String[] { BooksStorage.COLUMN_NAME_BOOK, BooksStorage.COLUMN_NAME_AUTHOR, BooksStorage.COLUMN_NAME_CONTACT, BooksStorage.COLUMN_NAME_DUEDATE, BooksStorage.COLUMN_NAME_HASREMINDER }, BooksStorage.COLUMN_NAME__ID + " IS ?", new String[] { new String(Integer.toString(row)) }, null); if(!cursor.moveToFirst()) { Log.e(TAG, "Received empty cursor"); finish(); } TextView view = (TextView) findViewById(R.id.editBookTitle); column = cursor.getColumnIndex(BooksStorage.COLUMN_NAME_BOOK); view.setText(cursor.getString(column)); view = (TextView) findViewById(R.id.editAuthor); column = cursor.getColumnIndex(BooksStorage.COLUMN_NAME_AUTHOR); view.setText(cursor.getString(column)); view = (TextView) findViewById(R.id.editContact); column = cursor.getColumnIndex(BooksStorage.COLUMN_NAME_CONTACT); view.setText(cursor.getString(column)); column = cursor.getColumnIndex(BooksStorage.COLUMN_NAME_DUEDATE); setDueDate(cursor.getLong(column)); CheckBox reminder = (CheckBox) findViewById(R.id.checkBoxCalendar); column = cursor.getColumnIndex(BooksStorage.COLUMN_NAME_HASREMINDER); if(cursor.getInt(column) != 0) { // cursor has no getBool() method so we will assume any non-zero value is TRUE reminder.setChecked(true); hasCalendarEvent = true; } else reminder.setChecked(false); } /** * Sets the date view in the UI with a formatted date. * <p> * If called with a date of -1 it will set the date in the UI to reflect the default loan period * as set in SharedPreferences. * Otherwise set the date in the UI as passed by parameter * @see SharedPreferences * * @param date date to be set in the UI, pass -1 to set the default loan period from SharedPrefences */ private void setDueDate(long date) { if(initializedDate) { // Date already set previously during this lifecycle return; } dueDate = Calendar.getInstance(); if(date == -1) { // Set dueDate to the default length from the settings SharedPreferences settings = this.getSharedPreferences(MainView.PREFS_NAME, 0); int periodLength = settings.getInt(MainView.PREFS_PERIOD_LENGTH, 2); int periodUnit = settings.getInt(MainView.PREFS_PERIOD_UNIT, 1); switch(periodUnit) { case 0: // Days periodUnit = Calendar.DAY_OF_YEAR; break; case 1: // Weeks periodUnit = Calendar.WEEK_OF_YEAR; break; case 2: // Months periodUnit = Calendar.MONTH; break; default: periodUnit = Calendar.DAY_OF_YEAR; } dueDate.add(periodUnit, periodLength); } else { // Set dueDate to parameter value dueDate.setTimeInMillis(date); } TextView editDueDate = (TextView) findViewById(R.id.editDuedate); //CharSequence format = DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMM d yyyy"); //CharSequence format = DateFormat.format("MMM d, yyyy", dueDate); java.text.DateFormat df = DateFormat.getMediumDateFormat(this); CharSequence format = df.format(dueDate.getTime()); editDueDate.setText(format); initializedDate = true; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_book); if (savedInstanceState != null) { initializedDate = savedInstanceState.getBoolean(STATE_INITIALIZED_DATE); fetchedRow = savedInstanceState.getBoolean(STATE_FETCHED_ROW); didIsbnLookup = savedInstanceState.getBoolean(STATE_DID_ISBN_LOOKUP); hasCalendarEvent = savedInstanceState.getBoolean(STATE_HAS_REMINDER); } else { // Superfluous but clear initializedDate = false; fetchedRow = false; didIsbnLookup = false; hasCalendarEvent = false; } } /** * Intialize activity depending on the intent * <p> * On EDIT intent will fetch a row from the DB and load its contents into the UI, * and on ISBN intent fetches book information from Google. */ @Override public void onResume() { super.onResume(); initContactAutocomplete(); Intent intent = getIntent(); switch(intent.getAction()) { case MainView.ACTION_EDIT: if(!fetchedRow) { // Fetch the row # from intent payload, set up the data in the UI // for the user to edit. int row = getIntent().getIntExtra(MainView.EXTRA_INDEX, -1); if(row < 0){ // Something wrong finish(); } fetchRow(row); fetchedRow = true; } break; case MainView.ACTION_ADD_MANUAL: setDueDate(-1); break; case MainView.ACTION_ADD_ISBN: setDueDate(-1); // Grab ISBN number from intent payload and fetch data from google if(!didIsbnLookup) { String isbn = intent.getStringExtra(MainView.EXTRA_ISBN); if(isbn == null) { // No isbn payload, something wrong } else { // Lookup ISBN code new BookResolver().execute(isbn); } } break; default: // Shouldnt happen, but assume ACTION_ADD_MANUAL is alright as a default // and log this. setDueDate(-1); Log.e(TAG, "No valid intent received."); } } @Override public void onSaveInstanceState(Bundle savedInstanceState) { savedInstanceState.putBoolean(STATE_DID_ISBN_LOOKUP, didIsbnLookup); savedInstanceState.putBoolean(STATE_FETCHED_ROW, fetchedRow); savedInstanceState.putBoolean(STATE_HAS_REMINDER, hasCalendarEvent); savedInstanceState.putBoolean(STATE_INITIALIZED_DATE, initializedDate); super.onSaveInstanceState(savedInstanceState); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.book, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. Intent intent; switch(item.getItemId()) { case R.id.action_settings: intent = new Intent(getApplicationContext(), SettingsActivity.class); startActivity(intent); return true; case R.id.action_about: AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.about_title); Resources res = getResources(); String aboutMessage = res.getString(R.string.about_message, MainView.BOOKS_VERSION); builder.setMessage(aboutMessage); AlertDialog aboutDialog = builder.create(); aboutDialog.show(); TextView view = (TextView) aboutDialog.findViewById(android.R.id.message); view.setTextSize(12); return true; } return super.onOptionsItemSelected(item); } }