Java tutorial
/******************************************************************************* * This file is part of Zandy. * * Zandy is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Zandy 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Zandy. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package com.gimranov.zandy.app; import android.app.AlertDialog; import android.app.Dialog; import android.app.ListActivity; import android.app.ProgressDialog; import android.app.SearchManager; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.Editable; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.gimranov.zandy.app.data.Database; import com.gimranov.zandy.app.data.Item; import com.gimranov.zandy.app.data.ItemAdapter; import com.gimranov.zandy.app.data.ItemCollection; import com.gimranov.zandy.app.task.APIEvent; import com.gimranov.zandy.app.task.APIRequest; import com.gimranov.zandy.app.task.ZoteroAPITask; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; import com.squareup.otto.Subscribe; import org.apache.http.util.ByteArrayBuffer; import org.jetbrains.annotations.Nullable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; public class ItemActivity extends ListActivity { private static final String TAG = "com.gimranov.zandy.app.ItemActivity"; static final int DIALOG_VIEW = 0; static final int DIALOG_NEW = 1; static final int DIALOG_SORT = 2; static final int DIALOG_IDENTIFIER = 3; static final int DIALOG_PROGRESS = 6; /** * Allowed sort orderings */ static final String[] SORTS = { "item_year, item_title COLLATE NOCASE", "item_creator COLLATE NOCASE, item_year", "item_title COLLATE NOCASE, item_year", "timestamp ASC, item_title COLLATE NOCASE" }; /** * Strings providing the names of each ordering, respectively */ static final int[] SORT_NAMES = { R.string.sort_year_title, R.string.sort_creator_year, R.string.sort_title_year, R.string.sort_modified_title }; private static final String SORT_CHOICE = "sort_choice"; private String collectionKey; private String query; private Database db; private ProgressDialog mProgressDialog; private ProgressThread progressThread; public String sortBy = "item_year, item_title"; final Handler syncHandler = new Handler() { public void handleMessage(Message msg) { Log.d(TAG, "received message: " + msg.arg1); refreshView(); if (msg.arg1 == APIRequest.UPDATED_DATA) { refreshView(); return; } if (msg.arg1 == APIRequest.QUEUED_MORE) { Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_queued_more, msg.arg2), Toast.LENGTH_SHORT).show(); return; } if (msg.arg1 == APIRequest.BATCH_DONE) { Application.getInstance().getBus().post(SyncEvent.COMPLETE); Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_complete), Toast.LENGTH_SHORT).show(); return; } if (msg.arg1 == APIRequest.ERROR_UNKNOWN) { Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_error), Toast.LENGTH_SHORT).show(); return; } } }; private APIEvent mEvent = new APIEvent() { private int updates = 0; @Override public void onComplete(APIRequest request) { Message msg = syncHandler.obtainMessage(); msg.arg1 = APIRequest.BATCH_DONE; syncHandler.sendMessage(msg); Log.d(TAG, "fired oncomplete"); } @Override public void onUpdate(APIRequest request) { updates++; if (updates % 10 == 0) { Message msg = syncHandler.obtainMessage(); msg.arg1 = APIRequest.UPDATED_DATA; syncHandler.sendMessage(msg); } else { // do nothing } } @Override public void onError(APIRequest request, Exception exception) { Log.e(TAG, "APIException caught", exception); Message msg = syncHandler.obtainMessage(); msg.arg1 = APIRequest.ERROR_UNKNOWN; syncHandler.sendMessage(msg); } @Override public void onError(APIRequest request, int error) { Log.e(TAG, "API error caught"); Message msg = syncHandler.obtainMessage(); msg.arg1 = APIRequest.ERROR_UNKNOWN; syncHandler.sendMessage(msg); } }; protected Bundle b = new Bundle(); /** * Refreshes the current list adapter */ private void refreshView() { ItemAdapter adapter = (ItemAdapter) getListAdapter(); if (adapter == null) return; Cursor newCursor = prepareCursor(); adapter.changeCursor(newCursor); adapter.notifyDataSetChanged(); Log.d(TAG, "refreshing view on request"); } /** * Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String persistedSort = Persistence.read(SORT_CHOICE); if (persistedSort != null) sortBy = persistedSort; db = new Database(this); setContentView(R.layout.items); Intent intent = getIntent(); collectionKey = intent.getStringExtra("com.gimranov.zandy.app.collectionKey"); ItemCollection coll = ItemCollection.load(collectionKey, db); APIRequest req; if (coll != null) { req = APIRequest.fetchItems(coll, false, new ServerCredentials(this)); } else { req = APIRequest.fetchItems(false, new ServerCredentials(this)); } prepareAdapter(); ItemAdapter adapter = (ItemAdapter) getListAdapter(); Cursor cur = adapter.getCursor(); if (intent.getBooleanExtra("com.gimranov.zandy.app.rerequest", false) || cur == null || cur.getCount() == 0) { if (!ServerCredentials.check(getBaseContext())) { Toast.makeText(getBaseContext(), getResources().getString(R.string.sync_log_in_first), Toast.LENGTH_SHORT).show(); return; } Toast.makeText(this, getResources().getString(R.string.collection_empty), Toast.LENGTH_SHORT).show(); Log.d(TAG, "Running a request to populate missing items"); ZoteroAPITask task = new ZoteroAPITask(this); req.setHandler(mEvent); task.execute(req); } ListView lv = getListView(); lv.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // If we have a click on an item, do something... ItemAdapter adapter = (ItemAdapter) parent.getAdapter(); Cursor cur = adapter.getCursor(); // Place the cursor at the selected item if (cur.moveToPosition(position)) { // and load an activity for the item Item item = Item.load(cur); Log.d(TAG, "Loading item data with key: " + item.getKey()); // We create and issue a specified intent with the necessary data Intent i = new Intent(getBaseContext(), ItemDataActivity.class); i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); i.putExtra("com.gimranov.zandy.app.itemDbId", item.dbId); startActivity(i); } else { // failed to move cursor-- show a toast TextView tvTitle = (TextView) view.findViewById(R.id.item_title); Toast.makeText(getApplicationContext(), getResources().getString(R.string.cant_open_item, tvTitle.getText()), Toast.LENGTH_SHORT).show(); } } }); } @Override protected void onResume() { super.onResume(); Application.getInstance().getBus().register(this); refreshView(); } @Override public void onDestroy() { ItemAdapter adapter = (ItemAdapter) getListAdapter(); Cursor cur = adapter.getCursor(); if (cur != null) cur.close(); if (db != null) db.close(); super.onDestroy(); } @Override protected void onPause() { super.onPause(); Application.getInstance().getBus().unregister(this); } private void prepareAdapter() { ItemAdapter adapter = new ItemAdapter(this, prepareCursor()); setListAdapter(adapter); } private Cursor prepareCursor() { Cursor cursor; // Be ready for a search Intent intent = getIntent(); if (Intent.ACTION_SEARCH.equals(intent.getAction())) { query = intent.getStringExtra(SearchManager.QUERY); cursor = getCursor(query); this.setTitle(getResources().getString(R.string.search_results, query)); } else if (query != null) { cursor = getCursor(query); this.setTitle(getResources().getString(R.string.search_results, query)); } else if (intent.getStringExtra("com.gimranov.zandy.app.tag") != null) { String tag = intent.getStringExtra("com.gimranov.zandy.app.tag"); Query q = new Query(); q.set("tag", tag); cursor = getCursor(q); this.setTitle(getResources().getString(R.string.tag_viewing_items, tag)); } else { collectionKey = intent.getStringExtra("com.gimranov.zandy.app.collectionKey"); ItemCollection coll; if (collectionKey != null && (coll = ItemCollection.load(collectionKey, db)) != null) { cursor = getCursor(coll); this.setTitle(coll.getTitle()); } else { cursor = getCursor(); this.setTitle(getResources().getString(R.string.all_items)); } } return cursor; } protected Dialog onCreateDialog(int id) { switch (id) { case DIALOG_NEW: AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getResources().getString(R.string.item_type)) // XXX i18n .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int pos) { Item item = new Item(getBaseContext(), Item.ITEM_TYPES[pos]); item.dirty = APIRequest.API_DIRTY; item.save(db); if (collectionKey != null) { ItemCollection coll = ItemCollection.load(collectionKey, db); if (coll != null) { coll.loadChildren(db); coll.add(item); coll.saveChildren(db); } } Log.d(TAG, "Loading item data with key: " + item.getKey()); // We create and issue a specified intent with the necessary data Intent i = new Intent(getBaseContext(), ItemDataActivity.class); i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); startActivity(i); } }); AlertDialog dialog = builder.create(); return dialog; case DIALOG_SORT: // We generate the sort name list for our current locale String[] sorts = new String[SORT_NAMES.length]; for (int j = 0; j < SORT_NAMES.length; j++) { sorts[j] = getResources().getString(SORT_NAMES[j]); } AlertDialog.Builder builder2 = new AlertDialog.Builder(this); builder2.setTitle(getResources().getString(R.string.set_sort_order)).setItems(sorts, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int pos) { Cursor cursor; setSortBy(SORTS[pos]); ItemCollection collection; if (collectionKey != null && (collection = ItemCollection.load(collectionKey, db)) != null) { cursor = getCursor(collection); } else { if (query != null) { cursor = getCursor(query); } else { cursor = getCursor(); } } ItemAdapter adapter = (ItemAdapter) getListAdapter(); adapter.changeCursor(cursor); Log.d(TAG, "Re-sorting by: " + SORTS[pos]); Persistence.write(SORT_CHOICE, SORTS[pos]); } }); AlertDialog dialog2 = builder2.create(); return dialog2; case DIALOG_PROGRESS: mProgressDialog = new ProgressDialog(this); mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); mProgressDialog.setIndeterminate(true); mProgressDialog.setMessage(getResources().getString(R.string.identifier_looking_up)); return mProgressDialog; case DIALOG_IDENTIFIER: final EditText input = new EditText(this); input.setHint(getResources().getString(R.string.identifier_hint)); final ItemActivity current = this; dialog = new AlertDialog.Builder(this).setTitle(getResources().getString(R.string.identifier_message)) .setView(input).setPositiveButton(getResources().getString(R.string.menu_search), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { Editable value = input.getText(); // run search Bundle c = new Bundle(); c.putString("mode", "isbn"); c.putString("identifier", value.toString()); removeDialog(DIALOG_PROGRESS); ItemActivity.this.b = c; showDialog(DIALOG_PROGRESS); } }) .setNeutralButton(getResources().getString(R.string.scan), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // If we're about to download from Google play, cancel that dialog // and prompt from Amazon if we're on an Amazon device IntentIntegrator integrator = new IntentIntegrator(current); @Nullable AlertDialog producedDialog = integrator.initiateScan(); if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { producedDialog.dismiss(); AmazonZxingGlue.showDownloadDialog(current); } } }) .setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // do nothing } }) .create(); return dialog; default: return null; } } @Override protected void onPrepareDialog(int id, Dialog dialog) { switch (id) { case DIALOG_PROGRESS: } } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.zotero_menu, menu); // Turn on sort item MenuItem sort = menu.findItem(R.id.do_sort); sort.setEnabled(true); sort.setVisible(true); // Turn on search item MenuItem search = menu.findItem(R.id.do_search); search.setEnabled(true); search.setVisible(true); // Turn on identifier item MenuItem identifier = menu.findItem(R.id.do_identifier); identifier.setEnabled(true); identifier.setVisible(true); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection switch (item.getItemId()) { case R.id.do_sync: if (!ServerCredentials.check(getBaseContext())) { Toast.makeText(getBaseContext(), getResources().getString(R.string.sync_log_in_first), Toast.LENGTH_SHORT).show(); return true; } // Get credentials ServerCredentials cred = new ServerCredentials(getBaseContext()); // Make this a collection-specific sync, preceding by de-dirtying Item.queue(db); ArrayList<APIRequest> list = new ArrayList<APIRequest>(); APIRequest[] templ = {}; for (Item i : Item.queue) { Log.d(TAG, "Adding dirty item to sync: " + i.getTitle()); list.add(cred.prep(APIRequest.update(i))); } if (collectionKey == null) { Log.d(TAG, "Adding sync request for all items"); APIRequest req = APIRequest.fetchItems(false, cred); req.setHandler(mEvent); list.add(req); } else { Log.d(TAG, "Adding sync request for collection: " + collectionKey); APIRequest req = APIRequest.fetchItems(collectionKey, true, cred); req.setHandler(mEvent); list.add(req); } APIRequest[] reqs = list.toArray(templ); ZoteroAPITask task = new ZoteroAPITask(getBaseContext()); task.setHandler(syncHandler); task.execute(reqs); Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), Toast.LENGTH_SHORT).show(); return true; case R.id.do_new: removeDialog(DIALOG_NEW); showDialog(DIALOG_NEW); return true; case R.id.do_identifier: removeDialog(DIALOG_IDENTIFIER); showDialog(DIALOG_IDENTIFIER); return true; case R.id.do_search: onSearchRequested(); return true; case R.id.do_prefs: Intent i = new Intent(getBaseContext(), SettingsActivity.class); startActivity(i); return true; case R.id.do_sort: removeDialog(DIALOG_SORT); showDialog(DIALOG_SORT); return true; default: return super.onOptionsItemSelected(item); } } /* Sorting */ public void setSortBy(String sort) { this.sortBy = sort; } /* Handling the ListView and keeping it up to date */ public Cursor getCursor() { Cursor cursor = db.query("items", Database.ITEMCOLS, null, null, null, null, this.sortBy, null); if (cursor == null) { Log.e(TAG, "cursor is null"); } return cursor; } public Cursor getCursor(ItemCollection parent) { String[] args = { parent.dbId }; Cursor cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, " + "items._id, item_key, item_year, item_creator, timestamp, item_children " + " FROM items, itemtocollections WHERE items._id = item_id AND collection_id=? ORDER BY " + this.sortBy, args); if (cursor == null) { Log.e(TAG, "cursor is null"); } return cursor; } public Cursor getCursor(String query) { String[] args = { "%" + query + "%", "%" + query + "%" }; Cursor cursor = db.rawQuery( "SELECT item_title, item_type, item_content, etag, dirty, " + "_id, item_key, item_year, item_creator, timestamp, item_children " + " FROM items WHERE item_title LIKE ? OR item_creator LIKE ?" + " ORDER BY " + this.sortBy, args); if (cursor == null) { Log.e(TAG, "cursor is null"); } return cursor; } public Cursor getCursor(Query query) { return query.query(db); } @Subscribe public void syncComplete(SyncEvent event) { if (event.getStatus() == SyncEvent.COMPLETE_CODE) refreshView(); } /* Thread and helper to run lookups */ @Override public void onActivityResult(int requestCode, int resultCode, Intent intent) { Log.d(TAG, "_____________________on_activity_result"); IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); if (scanResult != null) { // handle scan result Bundle b = new Bundle(); b.putString("mode", "isbn"); b.putString("identifier", scanResult.getContents()); if (scanResult != null && scanResult.getContents() != null) { Log.d(TAG, b.getString("identifier")); progressThread = new ProgressThread(handler, b); progressThread.start(); this.b = b; removeDialog(DIALOG_PROGRESS); showDialog(DIALOG_PROGRESS); } else { Toast.makeText(getApplicationContext(), getResources().getString(R.string.identifier_scan_failed), Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(getApplicationContext(), getResources().getString(R.string.identifier_scan_failed), Toast.LENGTH_SHORT).show(); } } final Handler handler = new Handler() { public void handleMessage(Message msg) { Log.d(TAG, "______________________handle_message"); if (ProgressThread.STATE_DONE == msg.arg2) { Bundle data = msg.getData(); String itemKey = data.getString("itemKey"); if (itemKey != null) { if (collectionKey != null) { Item item = Item.load(itemKey, db); ItemCollection coll = ItemCollection.load(collectionKey, db); coll.add(item); coll.saveChildren(db); } mProgressDialog.dismiss(); mProgressDialog = null; Log.d(TAG, "Loading new item data with key: " + itemKey); // We create and issue a specified intent with the necessary data Intent i = new Intent(getBaseContext(), ItemDataActivity.class); i.putExtra("com.gimranov.zandy.app.itemKey", itemKey); startActivity(i); } return; } if (ProgressThread.STATE_PARSING == msg.arg2) { mProgressDialog.setMessage(getResources().getString(R.string.identifier_processing)); return; } if (ProgressThread.STATE_ERROR == msg.arg2) { dismissDialog(DIALOG_PROGRESS); Toast.makeText(getApplicationContext(), getResources().getString(R.string.identifier_lookup_failed), Toast.LENGTH_SHORT).show(); progressThread.setState(ProgressThread.STATE_DONE); return; } } }; private class ProgressThread extends Thread { Handler mHandler; Bundle arguments; final static int STATE_DONE = 5; final static int STATE_FETCHING = 1; final static int STATE_PARSING = 6; final static int STATE_ERROR = 7; int mState; ProgressThread(Handler h, Bundle b) { mHandler = h; arguments = b; Log.d(TAG, "_____________________thread_constructor"); } public void run() { Log.d(TAG, "_____________________thread_run"); mState = STATE_FETCHING; // Setup String identifier = arguments.getString("identifier"); String mode = arguments.getString("mode"); URL url; String urlstring; String response = ""; if ("isbn".equals(mode)) { urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" + identifier + "?method=getMetadata&fl=*&format=json&count=1"; } else { urlstring = ""; } try { Log.d(TAG, "Fetching from: " + urlstring); url = new URL(urlstring); /* Open a connection to that URL. */ URLConnection ucon = url.openConnection(); /* * Define InputStreams to read from the URLConnection. */ InputStream is = ucon.getInputStream(); BufferedInputStream bis = new BufferedInputStream(is, 16000); ByteArrayBuffer baf = new ByteArrayBuffer(50); int current = 0; /* * Read bytes to the Buffer until there is nothing more to read(-1). */ while (mState == STATE_FETCHING && (current = bis.read()) != -1) { baf.append((byte) current); } response = new String(baf.toByteArray()); Log.d(TAG, response); } catch (IOException e) { Log.e(TAG, "Error: ", e); } Message msg = mHandler.obtainMessage(); msg.arg2 = STATE_PARSING; mHandler.sendMessage(msg); /* * { "stat":"ok", "list":[{ "url":["http://www.worldcat.org/oclc/177669176?referer=xid"], "publisher":"O'Reilly", "form":["BA"], "lccn":["2004273129"], "lang":"eng", "city":"Sebastopol, CA", "author":"by Mark Lutz and David Ascher.", "ed":"2nd ed.", "year":"2003", "isbn":["0596002815"], "title":"Learning Python", "oclcnum":["177669176", .. "748093898"]}]} */ // This is OCLC-specific logic try { JSONObject result = new JSONObject(response); if (!result.getString("stat").equals("ok")) { Log.e(TAG, "Error response received"); msg = mHandler.obtainMessage(); msg.arg2 = STATE_ERROR; mHandler.sendMessage(msg); return; } result = result.getJSONArray("list").getJSONObject(0); String form = result.getJSONArray("form").getString(0); String type; if ("AA".equals(form)) type = "audioRecording"; else if ("VA".equals(form)) type = "videoRecording"; else if ("FA".equals(form)) type = "film"; else type = "book"; // TODO Fix this type = "book"; Item item = new Item(getBaseContext(), type); JSONObject content = item.getContent(); if (result.has("lccn")) { String lccn = "LCCN: " + result.getJSONArray("lccn").getString(0); content.put("extra", lccn); } if (result.has("isbn")) { content.put("ISBN", result.getJSONArray("isbn").getString(0)); } content.put("title", result.optString("title", "")); content.put("place", result.optString("city", "")); content.put("edition", result.optString("ed", "")); content.put("language", result.optString("lang", "")); content.put("publisher", result.optString("publisher", "")); content.put("date", result.optString("year", "")); item.setTitle(result.optString("title", "")); item.setYear(result.optString("year", "")); String author = result.optString("author", ""); item.setCreatorSummary(author); JSONArray array = new JSONArray(); JSONObject member = new JSONObject(); member.accumulate("creatorType", "author"); member.accumulate("name", author); array.put(member); content.put("creators", array); item.setContent(content); item.save(db); msg = mHandler.obtainMessage(); Bundle data = new Bundle(); data.putString("itemKey", item.getKey()); msg.setData(data); msg.arg2 = STATE_DONE; mHandler.sendMessage(msg); return; } catch (JSONException e) { Log.e(TAG, "exception parsing response", e); msg = mHandler.obtainMessage(); msg.arg2 = STATE_ERROR; mHandler.sendMessage(msg); return; } } public void setState(int state) { mState = state; } } }