Java tutorial
/**************************************************************************************** * Copyright (c) 2009 Edu Zamora <edu.zasu@gmail.com> * * Copyright (c) 2011 Kostas Spyropoulos <inigo.aldana@gmail.com> * * Copyright (c) 2012 Norbert Nagold <norbert.nagold@gmail.com> * * * * This program 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. * * * * This program 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 * * this program. If not, see <http://www.gnu.org/licenses/>. * ****************************************************************************************/ package website.openeng.async; import android.content.Context; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.AsyncTask; import website.openeng.anki.KanjiDroidApp; import website.openeng.anki.CollectionHelper; import website.openeng.anki.R; import website.openeng.anki.exception.UnknownHttpResponseException; import website.openeng.anki.exception.UnsupportedSyncException; import website.openeng.libanki.Collection; import website.openeng.libanki.Decks; import website.openeng.libanki.sync.FullSyncer; import website.openeng.libanki.sync.HttpSyncer; import website.openeng.libanki.sync.MediaSyncer; import website.openeng.libanki.sync.RemoteMediaServer; import website.openeng.libanki.sync.RemoteServer; import website.openeng.libanki.sync.Syncer; import org.apache.http.HttpResponse; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.HashMap; import timber.log.Timber; public class Connection extends BaseAsyncTask<Connection.Payload, Object, Connection.Payload> { public static final int TASK_TYPE_LOGIN = 0; public static final int TASK_TYPE_SYNC = 1; public static final int TASK_TYPE_DOWNLOAD_MEDIA = 5; public static final int TASK_TYPE_REGISTER = 6; public static final int TASK_TYPE_UPGRADE_DECKS = 7; public static final int CONN_TIMEOUT = 30000; private static Connection sInstance; private TaskListener mListener; private CancelCallback mCancelCallback; private static Connection launchConnectionTask(TaskListener listener, Payload data) { if (!isOnline()) { data.success = false; listener.onDisconnected(); return null; } try { if ((sInstance != null) && (sInstance.getStatus() != AsyncTask.Status.FINISHED)) { sInstance.get(); } } catch (Exception e) { e.printStackTrace(); } sInstance = new Connection(); sInstance.mListener = listener; sInstance.execute(data); return sInstance; } /* * Runs on GUI thread */ @Override protected void onCancelled() { super.onCancelled(); if (mCancelCallback != null) { mCancelCallback.cancelAllConnections(); } if (mListener instanceof CancellableTaskListener) { ((CancellableTaskListener) mListener).onCancelled(); } } /* * Runs on GUI thread */ @Override protected void onPreExecute() { super.onPreExecute(); if (mListener != null) { mListener.onPreExecute(); } } /* * Runs on GUI thread */ @Override protected void onPostExecute(Payload data) { super.onPostExecute(data); if (mListener != null) { mListener.onPostExecute(data); } } /* * Runs on GUI thread */ @Override protected void onProgressUpdate(Object... values) { super.onProgressUpdate(values); if (mListener != null) { mListener.onProgressUpdate(values); } } public static Connection login(TaskListener listener, Payload data) { data.taskType = TASK_TYPE_LOGIN; return launchConnectionTask(listener, data); } public static Connection register(TaskListener listener, Payload data) { data.taskType = TASK_TYPE_REGISTER; return launchConnectionTask(listener, data); } public static Connection sync(TaskListener listener, Payload data) { data.taskType = TASK_TYPE_SYNC; return launchConnectionTask(listener, data); } @Override protected Payload doInBackground(Payload... params) { super.doInBackground(params); if (params.length != 1) { throw new IllegalArgumentException(); } return doOneInBackground(params[0]); } private Payload doOneInBackground(Payload data) { switch (data.taskType) { case TASK_TYPE_LOGIN: return doInBackgroundLogin(data); case TASK_TYPE_REGISTER: return doInBackgroundRegister(data); case TASK_TYPE_SYNC: return doInBackgroundSync(data); case TASK_TYPE_DOWNLOAD_MEDIA: return doInBackgroundDownloadMissingMedia(data); case TASK_TYPE_UPGRADE_DECKS: throw new RuntimeException("Upgrade decks no longer supported"); default: return null; } } private Payload doInBackgroundLogin(Payload data) { String username = (String) data.data[0]; String password = (String) data.data[1]; HttpSyncer server = new RemoteServer(this, null); HttpResponse ret; try { ret = server.hostKey(username, password); } catch (UnknownHttpResponseException e) { data.success = false; data.result = new Object[] { "error", e.getResponseCode(), e.getMessage() }; return data; } catch (Exception e2) { // Ask user to report all bugs which aren't timeout errors if (!timeoutOccured(e2)) { KanjiDroidApp.sendExceptionReport(e2, "doInBackgroundLogin"); } data.success = false; data.result = new Object[] { "connectionError" }; return data; } String hostkey = null; boolean valid = false; if (ret != null) { data.returnType = ret.getStatusLine().getStatusCode(); Timber.d("doInBackgroundLogin - response from server: %d, (%s)", data.returnType, ret.getStatusLine().getReasonPhrase()); if (data.returnType == 200) { try { JSONObject jo = (new JSONObject(server.stream2String(ret.getEntity().getContent()))); hostkey = jo.getString("key"); valid = (hostkey != null) && (hostkey.length() > 0); } catch (JSONException e) { valid = false; } catch (IllegalStateException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } } else { Timber.e("doInBackgroundLogin - empty response from server"); } if (valid) { data.success = true; data.data = new String[] { username, hostkey }; } else { data.success = false; } return data; } private Payload doInBackgroundRegister(Payload data) { String username = (String) data.data[0]; String password = (String) data.data[1]; HttpSyncer server = new RemoteServer(this, null); HttpResponse ret; try { ret = server.register(username, password); } catch (UnknownHttpResponseException e) { data.success = false; data.result = new Object[] { "error", e.getResponseCode(), e.getMessage() }; return data; } String hostkey = null; boolean valid = false; String status = null; if (ret != null) { data.returnType = ret.getStatusLine().getStatusCode(); if (data.returnType == 200) { try { JSONObject jo = (new JSONObject(server.stream2String(ret.getEntity().getContent()))); status = jo.getString("status"); if (status.equals("ok")) { hostkey = jo.getString("hkey"); valid = (hostkey != null) && (hostkey.length() > 0); } } catch (JSONException e) { } catch (IllegalStateException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } } if (valid) { data.success = true; data.data = new String[] { username, hostkey }; } else { data.success = false; data.data = new String[] { status != null ? status : KanjiDroidApp.getAppResources().getString(R.string.connection_error_message) }; } return data; } private boolean timeoutOccured(Exception e) { String msg = e.getMessage(); return msg.contains("UnknownHostException") || msg.contains("HttpHostConnectException") || msg.contains("SSLException while building HttpClient") || msg.contains("SocketTimeoutException") || msg.contains("ClientProtocolException") || msg.contains("TimeoutException"); } private Payload doInBackgroundSync(Payload data) { // for for doInBackgroundLoadDeckCounts if any Timber.d("doInBackgroundSync()"); // Block execution until any previous background task finishes, or timeout after 5s boolean ok = DeckTask.waitToFinish(5); String hkey = (String) data.data[0]; boolean media = (Boolean) data.data[1]; String conflictResolution = (String) data.data[2]; Collection col = data.col; boolean colCorruptFullSync = false; if (!CollectionHelper.getInstance().colIsOpen() || !ok) { if (conflictResolution != null && conflictResolution.equals("download")) { colCorruptFullSync = true; } else { data.success = false; data.result = new Object[] { "genericError" }; return data; } } try { CollectionHelper.getInstance().lockCollection(); HttpSyncer server = new RemoteServer(this, hkey); Syncer client = new Syncer(col, server); // run sync and check state boolean noChanges = false; if (conflictResolution == null) { Timber.i("Sync - starting sync"); publishProgress(R.string.sync_prepare_syncing); Object[] ret = client.sync(this); data.message = client.getSyncMsg(); if (ret == null) { data.success = false; data.result = new Object[] { "genericError" }; return data; } String retCode = (String) ret[0]; if (!retCode.equals("noChanges") && !retCode.equals("success")) { data.success = false; data.result = ret; // Check if there was a sanity check error if (retCode.equals("sanityCheckError")) { // Force full sync next time col.modSchemaNoCheck(); col.save(); } return data; } // save and note success state if (retCode.equals("noChanges")) { // publishProgress(R.string.sync_no_changes_message); noChanges = true; } else { // publishProgress(R.string.sync_database_acknowledge); } } else { try { server = new FullSyncer(col, hkey, this); if (conflictResolution.equals("upload")) { Timber.i("Sync - fullsync - upload collection"); publishProgress(R.string.sync_preparing_full_sync_message); Object[] ret = server.upload(); if (ret == null) { data.success = false; data.result = new Object[] { "genericError" }; CollectionHelper.getInstance().reopenCollection(); // TODO: is this needed? return data; } if (!ret[0].equals(HttpSyncer.ANKIWEB_STATUS_OK)) { data.success = false; data.result = ret; CollectionHelper.getInstance().reopenCollection(); // TODO: is this needed? return data; } } else if (conflictResolution.equals("download")) { Timber.i("Sync - fullsync - download collection"); publishProgress(R.string.sync_downloading_message); Object[] ret = server.download(); if (ret == null) { data.success = false; data.result = new Object[] { "genericError" }; CollectionHelper.getInstance().reopenCollection(); // TODO: is this needed? return data; } if (!ret[0].equals("success")) { data.success = false; data.result = ret; if (!colCorruptFullSync) { CollectionHelper.getInstance().reopenCollection(); // TODO: is this needed? } return data; } } col = CollectionHelper.getInstance().reopenCollection(); // TODO: is this needed? } catch (OutOfMemoryError e) { KanjiDroidApp.sendExceptionReport(e, "doInBackgroundSync-fullSync"); data.success = false; data.result = new Object[] { "OutOfMemoryError" }; return data; } catch (RuntimeException e) { if (timeoutOccured(e)) { data.result = new Object[] { "connectionError" }; } else { KanjiDroidApp.sendExceptionReport(e, "doInBackgroundSync-fullSync"); data.result = new Object[] { "IOException" }; } data.success = false; return data; } } // clear undo to avoid non syncing orphans (because undo resets usn too if (!noChanges) { col.clearUndo(); } // then move on to media sync boolean noMediaChanges = false; String mediaError = null; if (media) { server = new RemoteMediaServer(col, hkey, this); MediaSyncer mediaClient = new MediaSyncer(col, (RemoteMediaServer) server, this); String ret; try { ret = mediaClient.sync(); if (ret == null) { mediaError = KanjiDroidApp.getAppResources().getString(R.string.sync_media_error); } else { if (ret.equals("noChanges")) { publishProgress(R.string.sync_media_no_changes); noMediaChanges = true; } if (ret.equals("sanityFailed")) { mediaError = KanjiDroidApp.getAppResources() .getString(R.string.sync_media_sanity_failed); } else { publishProgress(R.string.sync_media_success); } } } catch (UnsupportedSyncException e) { mediaError = KanjiDroidApp.getAppResources().getString(R.string.sync_media_unsupported); KanjiDroidApp.getSharedPrefs(KanjiDroidApp.getInstance().getApplicationContext()).edit() .putBoolean("syncFetchesMedia", false).commit(); KanjiDroidApp.sendExceptionReport(e, "doInBackgroundSync-mediaSync"); } catch (RuntimeException e) { if (timeoutOccured(e)) { data.result = new Object[] { "connectionError" }; } else { KanjiDroidApp.sendExceptionReport(e, "doInBackgroundSync-mediaSync"); } mediaError = e.getLocalizedMessage(); } } if (noChanges && noMediaChanges) { data.success = false; data.result = new Object[] { "noChanges" }; return data; } else { data.success = true; data.data = new Object[] { conflictResolution, col, mediaError }; return data; } } catch (UnknownHttpResponseException e) { Timber.e("doInBackgroundSync -- unknown response code error"); e.printStackTrace(); data.success = false; Integer code = e.getResponseCode(); String msg = e.getLocalizedMessage(); data.result = new Object[] { "error", code, msg }; return data; } catch (Exception e) { // Global error catcher. // Try to give a human readable error, otherwise print the raw error message Timber.e("doInBackgroundSync error"); e.printStackTrace(); data.success = false; if (timeoutOccured(e)) { data.result = new Object[] { "connectionError" }; } else { KanjiDroidApp.sendExceptionReport(e, "doInBackgroundSync"); data.result = new Object[] { e.getLocalizedMessage() }; } return data; } finally { // Close collection to roll back any sync failures and Timber.d("doInBackgroundSync -- closing collection on outer finally statement"); col.close(false); CollectionHelper.getInstance().unlockCollection(); Timber.d("doInBackgroundSync -- reopening collection on outer finally statement"); CollectionHelper.getInstance().reopenCollection(); } } public void publishProgress(int id) { super.publishProgress(id); } public void publishProgress(String message) { super.publishProgress(message); } public void publishProgress(int id, long up, long down) { super.publishProgress(id, up, down); } /** * Downloads any missing media files according to the mediaURL deckvar. * * @param data * @return The return type contains data.resultType and an array of Integer in data.data. data.data[0] is the number * of total missing media, data.data[1] is the number of downloaded ones. */ private Payload doInBackgroundDownloadMissingMedia(Payload data) { Timber.i("DownloadMissingMedia"); HashMap<String, String> missingPaths = new HashMap<String, String>(); HashMap<String, String> missingSums = new HashMap<String, String>(); data.result = (Decks) data.data[0]; // pass it to the return object so we close the deck in the deck picker String syncName = "";// deck.getDeckName(); data.success = false; data.data = new Object[] { 0, 0, 0 }; // if (!deck.hasKey("mediaURL")) { // data.success = true; // return data; // } String urlbase = "";// deck.getVar("mediaURL"); if (urlbase.equals("")) { data.success = true; return data; } String mdir = "";// deck.mediaDir(true); int totalMissing = 0; int missing = 0; int grabbed = 0; Cursor cursor = null; try { cursor = null;// deck.getDB().getDatabase().rawQuery("SELECT filename, originalPath FROM media", null); String path = null; String f = null; while (cursor.moveToNext()) { f = cursor.getString(0); path = mdir + "/" + f; File file = new File(path); if (!file.exists()) { missingPaths.put(f, path); missingSums.put(f, cursor.getString(1)); Timber.d("Missing file: %s", f); } } } finally { if (cursor != null) { cursor.close(); } } totalMissing = missingPaths.size(); data.data[0] = totalMissing; if (totalMissing == 0) { data.success = true; return data; } publishProgress(Boolean.FALSE, totalMissing, 0, syncName); URL url = null; HttpURLConnection connection = null; String path = null; String sum = null; int readbytes = 0; byte[] buf = new byte[4096]; for (String file : missingPaths.keySet()) { try { android.net.Uri uri = android.net.Uri.parse(Uri.encode(urlbase, ":/@%") + Uri.encode(file)); url = new URI(uri.toString()).toURL(); connection = (HttpURLConnection) url.openConnection(); connection.connect(); if (connection.getResponseCode() == 200) { path = missingPaths.get(file); InputStream is = connection.getInputStream(); BufferedInputStream bis = new BufferedInputStream(is, 4096); FileOutputStream fos = new FileOutputStream(path); while ((readbytes = bis.read(buf, 0, 4096)) != -1) { fos.write(buf, 0, readbytes); Timber.d("Downloaded %d file: %s", readbytes, path); } fos.close(); // Verify with checksum sum = missingSums.get(file); if (true) {// sum.equals("") || sum.equals(Utils.fileChecksum(path))) { grabbed++; } else { // Download corrupted, delete file Timber.i("Downloaded media file %s failed checksum.", path); File f = new File(path); f.delete(); missing++; } } else { Timber.e("Connection error (" + connection.getResponseCode() + ") while retrieving media file " + urlbase + file); Timber.e("Connection message: " + connection.getResponseMessage()); if (missingSums.get(file).equals("")) { // Ignore and keep going missing++; } else { data.success = false; data.data = new Object[] { file }; return data; } } connection.disconnect(); } catch (URISyntaxException e) { Timber.e(e, "doInBackgroundDownloadMissingMedia URISyntaxException"); } catch (MalformedURLException e) { Timber.e(e, "MalformedURLException while download media file " + path); if (missingSums.get(file).equals("")) { // Ignore and keep going missing++; } else { data.success = false; data.data = new Object[] { file }; return data; } } catch (IOException e) { Timber.e(e, "IOException while download media file " + path); if (missingSums.get(file).equals("")) { // Ignore and keep going missing++; } else { data.success = false; data.data = new Object[] { file }; return data; } } finally { if (connection != null) { connection.disconnect(); } } publishProgress(Boolean.TRUE, totalMissing, grabbed + missing, syncName); } data.data[1] = grabbed; data.data[2] = missing; data.success = true; return data; } public static boolean isOnline() { ConnectivityManager cm = (ConnectivityManager) KanjiDroidApp.getInstance().getApplicationContext() .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo netInfo = cm.getActiveNetworkInfo(); if (netInfo == null || !netInfo.isConnected() || !netInfo.isAvailable()) { return false; } return true; } public static interface TaskListener { public void onPreExecute(); public void onProgressUpdate(Object... values); public void onPostExecute(Payload data); public void onDisconnected(); } public static interface CancellableTaskListener extends TaskListener { public void onCancelled(); } public static class Payload { public int taskType; public Object[] data; public Object result; public boolean success; public int returnType; public Exception exception; public String message; public Collection col; public Payload() { data = null; success = true; } public Payload(Object[] data, Collection col) { this.data = data; this.col = col; success = true; } public Payload(Object[] data) { this.data = data; success = true; } public Payload(int taskType, Object[] data) { this.taskType = taskType; this.data = data; success = true; } public Payload(int taskType, Object[] data, String path) { this.taskType = taskType; this.data = data; success = true; } } public class CancelCallback { private WeakReference<ThreadSafeClientConnManager> mConnectionManager = null; public void setConnectionManager(ThreadSafeClientConnManager connectionManager) { mConnectionManager = new WeakReference<ThreadSafeClientConnManager>(connectionManager); } public void cancelAllConnections() { if (mConnectionManager != null) { ThreadSafeClientConnManager connectionManager = mConnectionManager.get(); if (connectionManager != null) { connectionManager.shutdown(); } } } } }