Java tutorial
/* * Copyright (C) 2012 Louis Fazen * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.alphabetbloc.accessmrs.services; import java.io.DataInputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import net.sqlcipher.DatabaseUtils.InsertHelper; import net.sqlcipher.database.SQLiteDatabase; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import android.accounts.Account; import android.accounts.AccountManager; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SyncResult; import android.database.Cursor; import android.os.Bundle; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.alphabetbloc.accessforms.provider.InstanceProviderAPI; import com.alphabetbloc.accessforms.provider.InstanceProviderAPI.InstanceColumns; import com.alphabetbloc.accessmrs.R; import com.alphabetbloc.accessmrs.data.Form; import com.alphabetbloc.accessmrs.providers.DataModel; import com.alphabetbloc.accessmrs.providers.Db; import com.alphabetbloc.accessmrs.providers.DbProvider; import com.alphabetbloc.accessmrs.utilities.App; import com.alphabetbloc.accessmrs.utilities.UiUtils; import com.alphabetbloc.accessmrs.utilities.XformUtils; /** * A Class to help with all Database interaction during a Sync. * * @author Louis Fazen (louis.fazen@gmail.com) * */ public class SyncManager { private static final String TAG = SyncManager.class.getSimpleName(); public static final String SYNC_MESSAGE = "com.alphabetbloc.accessmrs.utilities.sync_message"; public static final String TOAST_MESSAGE = "toast_message"; public static final String TOAST_ERROR = "toast_error"; public static final String START_NEW_SYNC = "start_new_sync"; public static final String REQUEST_NEW_SYNC = "request_new_sync"; public static final int UPLOAD_FORMS = 1; public static final int DOWNLOAD_FORMS = 2; public static final int DOWNLOAD_OBS = 3; public static final int SYNC_COMPLETE = 4; private String mSyncResultString; private Context mContext; public static AtomicInteger sSyncStep = new AtomicInteger(0); public static AtomicInteger sLoopCount = new AtomicInteger(0); public static AtomicInteger sLoopProgress = new AtomicInteger(0); public static String sSyncTitle = ""; public static AtomicBoolean sEndSync = new AtomicBoolean(false); public static AtomicBoolean sStartSync = new AtomicBoolean(false); public static AtomicBoolean sCancelSync = new AtomicBoolean(false); // Android OS does not allow concurrent sync... so static progress int works public SyncManager(Context context) { mContext = context; sSyncTitle = mContext.getString(R.string.sync_in_progress); sSyncStep.set(0); sLoopProgress.set(0); sLoopCount.set(0); sEndSync.set(false); if (App.DEBUG) Log.v(TAG, "New SyncManager with: Step=" + sSyncStep + " Progress=" + sLoopProgress + " Count=" + sLoopCount); } // REQUEST MANUAL SYNC public static void syncData() { if (App.DEBUG) Log.v(TAG, "SyncData is Requested"); AccountManager accountManager = AccountManager.get(App.getApp()); Account[] accounts = accountManager.getAccountsByType(App.getApp().getString(R.string.app_account_type)); if (accounts.length > 0) { sStartSync.set(true); Bundle bundle = new Bundle(); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_FORCE, true); bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); // //this resets the scheduled sync ContentResolver.requestSync(accounts[0], App.getApp().getString(R.string.app_provider_authority), bundle); } else UiUtils.toastAlert(App.getApp().getString(R.string.sync_error), App.getApp().getString(R.string.no_account_setup)); } public void addSyncStep(String title, boolean increment) { if (increment) { sSyncStep.getAndIncrement(); sLoopProgress.set(0); sLoopCount.set(0); } else if (sLoopCount.get() > 0) { // If leaving loop, round up and reset counters float loop = ((float) sLoopProgress.get() / (float) sLoopCount.get()); sSyncStep.set((int) ((float) sSyncStep.get() + loop + 0.5F)); sLoopProgress.set(0); sLoopCount.set(0); } // Or else just update the title sSyncTitle = title; // if (App.DEBUG) // Log.v(TAG, "addSyncStep: Step=" + sSyncStep + " Progress=" + // sLoopProgress + " Count=" + sLoopCount); // if (sSyncStep == 0) // sSyncStep++; // int roundLoopUp = (int) (syncStepAndLoop + 0.5); // if (roundLoopUp <= sSyncStep) // roundLoopUp++; // sSyncStep = roundLoopUp; // } // UPLOAD SECTION: public String[] getInstancesToUpload() { ArrayList<String> selectedInstances = new ArrayList<String>(); Cursor c = Db.open().fetchFormInstancesByStatus(DataModel.STATUS_UNSUBMITTED); if (c != null) { if (c.moveToFirst()) { do { String dbPath = c.getString(c.getColumnIndex(DataModel.KEY_PATH)); selectedInstances.add(dbPath); } while (c.moveToNext()); } c.close(); } return selectedInstances.toArray(new String[selectedInstances.size()]); } public String updateUploadedForms(String[] uploaded, SyncResult syncResult) { StringBuilder error = new StringBuilder(); String e = null; sLoopProgress.set(0); if (uploaded.length > 0) { String path; sLoopCount.set(uploaded.length); // update the databases with new status submitted for (int i = 0; i < uploaded.length; i++) { path = uploaded[i]; if (App.DEBUG) Log.v(TAG, "Updating the uploaded instance in db " + path); // update AccessMRS e = updateAccessMrsInstances(path, syncResult); if (e != null) error.append(" AccessMRS Form ").append(i).append(": ").append(e); // update AccessForms e = updateAccessFormsInstances(path, syncResult); if (e != null) error.append(" AccessForms Form ").append(i).append(": ").append(e); sLoopProgress.getAndIncrement(); } Db.open().clearSubmittedForms(); // Encrypt the uploaded data with wakelock to ensure it happens! WakefulIntentService.sendWakefulWork(mContext, EncryptionService.class); } return error.toString(); } public String updateAccessMrsInstances(String path, SyncResult syncResult) { try { // Cursor c = Db.open().fetchFormInstancesByPath(path); // if (c != null) { Db.open().updateFormInstance(path, DataModel.STATUS_SUBMITTED); // c.close(); // } } catch (Exception e) { e.printStackTrace(); ++syncResult.stats.numIoExceptions; return e.getLocalizedMessage(); } return null; } public String updateAccessFormsInstances(String path, SyncResult syncResult) { try { ContentValues insertValues = new ContentValues(); insertValues.put(InstanceColumns.STATUS, InstanceProviderAPI.STATUS_SUBMITTED); String where = InstanceColumns.INSTANCE_FILE_PATH + "=?"; String whereArgs[] = { path }; int updatedrows = mContext.getContentResolver().update(InstanceColumns.CONTENT_URI, insertValues, where, whereArgs); if (updatedrows > 1) { ++syncResult.stats.numIoExceptions; Log.e(TAG, "Error! updated more than one entry when tyring to update: " + path.toString()); } else if (updatedrows == 1) { if (App.DEBUG) Log.v(TAG, "Instance successfully updated to Submitted Status"); } else { ++syncResult.stats.numIoExceptions; Log.e(TAG, "Error, Instance doesn't exist but we have its path!! " + path.toString()); } } catch (Exception e) { e.printStackTrace(); ++syncResult.stats.numIoExceptions; return e.getLocalizedMessage(); } return null; } // DOWNLOAD FORMS SECTION /** * Inserts a list of forms into the AccessMRS form list database * * @param doc * Document created from parsed input stream * @throws Exception */ public ArrayList<Form> readFormListStream(InputStream is) throws Exception { ArrayList<Form> allForms = new ArrayList<Form>(); Document doc = null; DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); doc = db.parse(is); if (doc == null) return allForms; // clean existing DbProvider dbHelper = DbProvider.openDb(); dbHelper.delete(DataModel.FORMS_TABLE, null, null); // make new form list NodeList formElements = doc.getElementsByTagName("xform"); sLoopCount.set(formElements.getLength()); sLoopProgress.set(0); Form f; Element n; String formName; String formId; for (int i = 0; i < sLoopCount.get(); i++) { n = (Element) formElements.item(i); formName = n.getElementsByTagName("name").item(0).getChildNodes().item(0).getNodeValue(); formId = n.getElementsByTagName("id").item(0).getChildNodes().item(0).getNodeValue(); f = new Form(); f.setName(formName); f.setFormId(Integer.valueOf(formId)); Db.open().addDownloadedForm(f); allForms.add(f); sLoopProgress.getAndIncrement(); } return allForms; } public String updateDownloadedForms(Form[] downloaded, SyncResult syncResult) { StringBuilder error = new StringBuilder(); if (downloaded.length > 0) { // update the databases with new status submitted sLoopProgress.set(0); sLoopCount.set(downloaded.length); for (int i = 0; i < downloaded.length; i++) { try { // update AccessMRS Db.open().updateFormPath(downloaded[i].getFormId(), downloaded[i].getPath()); // update AccessForms if (!XformUtils.insertSingleForm(downloaded[i].getPath())) UiUtils.toastSyncMessage(null, "AccessForms not initialized.", true); sLoopProgress.getAndIncrement(); } catch (Exception e) { e.printStackTrace(); ++syncResult.stats.numIoExceptions; error.append(" Download Form ").append(i).append(": ").append(e.getLocalizedMessage()); } } } return error.toString(); } // DOWNLOAD OBS SECTION public String readObsFile(File tempFile, SyncResult syncResult) { if (tempFile == null) return "error"; try { DataInputStream dis = new DataInputStream(new FileInputStream(tempFile)); if (dis != null) { DbProvider dbHelper = DbProvider.openDb(); // open db and clean entries dbHelper.delete(DataModel.PATIENTS_TABLE, DataModel.KEY_CLIENT_CREATED + " IS NULL", null); dbHelper.delete(DataModel.OBSERVATIONS_TABLE, null, null); insertPatients(dis); addSyncStep(mContext.getString(R.string.sync_updating_data), false); // 70% insertObservations(dis); try { addSyncStep(mContext.getString(R.string.sync_updating_data), false); // 90% // (doubled // due // to // slow // speed) insertPatientForms(dis); } catch (EOFException e) { // do nothing for EOFExceptions in this case if (App.DEBUG) Log.v(TAG, "No SmartForms available on server"); } dis.close(); } updateAccessMrsObs(); addSyncStep(mContext.getString(R.string.sync_updating_data), false); // 100% } catch (Exception e) { e.printStackTrace(); ++syncResult.stats.numIoExceptions; return e.getLocalizedMessage(); } return null; } public void updateAccessMrsObs() { // sync db Db.open().resolveTemporaryPatients(); Db.open().updatePriorityFormNumbers(); Db.open().updateSavedFormNumbers(); // log the event Db.open().createDownloadLog(); } private void insertPatients(DataInputStream zdis) throws Exception { long start = System.currentTimeMillis(); SQLiteDatabase db = DbProvider.getDb(); SimpleDateFormat output = new SimpleDateFormat("MMM dd, yyyy"); SimpleDateFormat input = new SimpleDateFormat("yyyy-MM-dd"); InsertHelper ih = new InsertHelper(db, DataModel.PATIENTS_TABLE); int ptIdIndex = ih.getColumnIndex(DataModel.KEY_PATIENT_ID); int ptIdentifierIndex = ih.getColumnIndex(DataModel.KEY_IDENTIFIER); int ptGivenIndex = ih.getColumnIndex(DataModel.KEY_GIVEN_NAME); int ptFamilyIndex = ih.getColumnIndex(DataModel.KEY_FAMILY_NAME); int ptMiddleIndex = ih.getColumnIndex(DataModel.KEY_MIDDLE_NAME); int ptBirthIndex = ih.getColumnIndex(DataModel.KEY_BIRTH_DATE); int ptGenderIndex = ih.getColumnIndex(DataModel.KEY_GENDER); db.beginTransaction(); sLoopProgress.set(0); try { sLoopCount.set(zdis.readInt()); if (App.DEBUG) Log.v(TAG, "insertPatients icount: " + sLoopCount); for (int i = 1; i < sLoopCount.get() + 1; i++) { ih.prepareForInsert(); ih.bind(ptIdIndex, zdis.readInt()); ih.bind(ptFamilyIndex, zdis.readUTF()); ih.bind(ptMiddleIndex, zdis.readUTF()); ih.bind(ptGivenIndex, zdis.readUTF()); ih.bind(ptGenderIndex, zdis.readUTF()); ih.bind(ptBirthIndex, parseDate(input, output, zdis.readUTF())); ih.bind(ptIdentifierIndex, zdis.readUTF()); ih.execute(); sLoopProgress.getAndIncrement(); } db.setTransactionSuccessful(); } finally { ih.close(); db.endTransaction(); } long end = System.currentTimeMillis(); if (App.DEBUG) Log.v("SYNC BENCHMARK", String.format("Total Patients: \n%d\nTotal Time: \n%d\nRecords per second: \n%.2f", sLoopCount.get(), (int) (end - start), 1000 * (double) sLoopCount.get() / (double) (end - start))); } private void insertObservations(DataInputStream zdis) throws Exception { long start = System.currentTimeMillis(); SimpleDateFormat output = new SimpleDateFormat("MMM dd, yyyy"); SimpleDateFormat input = new SimpleDateFormat("yyyy-MM-dd"); SQLiteDatabase db = DbProvider.getDb(); InsertHelper ih = new InsertHelper(db, DataModel.OBSERVATIONS_TABLE); int ptIdIndex = ih.getColumnIndex(DataModel.KEY_PATIENT_ID); int obsTextIndex = ih.getColumnIndex(DataModel.KEY_VALUE_TEXT); int obsNumIndex = ih.getColumnIndex(DataModel.KEY_VALUE_NUMERIC); int obsDateIndex = ih.getColumnIndex(DataModel.KEY_VALUE_DATE); int obsIntIndex = ih.getColumnIndex(DataModel.KEY_VALUE_INT); int obsFieldIndex = ih.getColumnIndex(DataModel.KEY_FIELD_NAME); int obsTypeIndex = ih.getColumnIndex(DataModel.KEY_DATA_TYPE); int obsEncDateIndex = ih.getColumnIndex(DataModel.KEY_ENCOUNTER_DATE); db.beginTransaction(); sLoopProgress.set(0); int count = 0; try { sLoopCount.set(zdis.readInt()); if (App.DEBUG) Log.v(TAG, "insertObservations icount: " + sLoopCount); for (int i = 1; i < sLoopCount.get() + 1; i++) { ih.prepareForInsert(); ih.bind(ptIdIndex, zdis.readInt()); ih.bind(obsFieldIndex, zdis.readUTF()); byte dataType = zdis.readByte(); if (dataType == DataModel.TYPE_STRING) { ih.bind(obsTextIndex, zdis.readUTF()); } else if (dataType == DataModel.TYPE_INT) { ih.bind(obsIntIndex, zdis.readInt()); } else if (dataType == DataModel.TYPE_DOUBLE) { ih.bind(obsNumIndex, zdis.readDouble()); } else if (dataType == DataModel.TYPE_DATE) { ih.bind(obsDateIndex, parseDate(input, output, zdis.readUTF())); } ih.bind(obsTypeIndex, dataType); ih.bind(obsEncDateIndex, parseDate(input, output, zdis.readUTF())); ih.execute(); count++; sLoopProgress.set(count * 2); } db.setTransactionSuccessful(); } finally { ih.close(); db.endTransaction(); } long end = System.currentTimeMillis(); if (App.DEBUG) Log.v("SYNC BENCHMARK", String.format("Total Observations: \n%d\nTotal Time: \n%d\nRecords per second: \n%.2f", sLoopCount.get(), (int) (end - start), 1000 * (double) sLoopCount.get() / (double) (end - start))); } private void insertPatientForms(final DataInputStream zdis) throws Exception { long start = System.currentTimeMillis(); SimpleDateFormat output = new SimpleDateFormat("MMM dd, yyyy"); SimpleDateFormat input = new SimpleDateFormat("yyyy-MM-dd"); SQLiteDatabase db = DbProvider.getDb(); InsertHelper ih = new InsertHelper(db, DataModel.OBSERVATIONS_TABLE); int ptIdIndex = ih.getColumnIndex(DataModel.KEY_PATIENT_ID); int obsTextIndex = ih.getColumnIndex(DataModel.KEY_VALUE_TEXT); int obsNumIndex = ih.getColumnIndex(DataModel.KEY_VALUE_NUMERIC); int obsDateIndex = ih.getColumnIndex(DataModel.KEY_VALUE_DATE); int obsIntIndex = ih.getColumnIndex(DataModel.KEY_VALUE_INT); int obsFieldIndex = ih.getColumnIndex(DataModel.KEY_FIELD_NAME); int obsTypeIndex = ih.getColumnIndex(DataModel.KEY_DATA_TYPE); int obsEncDateIndex = ih.getColumnIndex(DataModel.KEY_ENCOUNTER_DATE); db.beginTransaction(); sLoopProgress.set(0); try { sLoopCount.set(zdis.readInt()); if (App.DEBUG) Log.v(TAG, "insertPatientForms icount: " + sLoopCount); for (int i = 1; i < sLoopCount.get() + 1; i++) { ih.prepareForInsert(); ih.bind(ptIdIndex, zdis.readInt()); ih.bind(obsFieldIndex, zdis.readUTF()); byte dataType = zdis.readByte(); if (dataType == DataModel.TYPE_STRING) { ih.bind(obsTextIndex, zdis.readUTF()); } else if (dataType == DataModel.TYPE_INT) { ih.bind(obsIntIndex, zdis.readInt()); } else if (dataType == DataModel.TYPE_DOUBLE) { ih.bind(obsNumIndex, zdis.readDouble()); } else if (dataType == DataModel.TYPE_DATE) { ih.bind(obsDateIndex, parseDate(input, output, zdis.readUTF())); } ih.bind(obsTypeIndex, dataType); ih.bind(obsEncDateIndex, parseDate(input, output, zdis.readUTF())); ih.execute(); sLoopProgress.getAndIncrement(); } db.setTransactionSuccessful(); } finally { ih.close(); db.endTransaction(); } long end = System.currentTimeMillis(); if (App.DEBUG) Log.v("SYNC BENCHMARK", String.format("Total Patient-Forms: \n%d\nTotal Time: \n%d\nRecords per second: \n%.2f", sLoopCount.get(), (int) (end - start), 1000 * (double) sLoopCount.get() / (double) (end - start))); } private String parseDate(SimpleDateFormat input, SimpleDateFormat output, String s) { try { return output.format(input.parse(s.split("T")[0])); } catch (ParseException e) { return "Unknown date"; } } public void toastSyncResult(int totalErrors, String errorMessage) { StringBuilder result = new StringBuilder(); result.append(mSyncResultString); if (totalErrors > 0) { result.append(" (") .append(mContext.getResources().getQuantityString(R.plurals.errors, totalErrors, totalErrors)); if (errorMessage != null) result.append(" : ").append(errorMessage); result.append(")"); } // Send to Activity Intent broadcast = new Intent(SYNC_MESSAGE); broadcast.putExtra(TOAST_MESSAGE, result.toString()); broadcast.putExtra(TOAST_ERROR, (totalErrors == 0) ? false : true); LocalBroadcastManager.getInstance(mContext).sendBroadcast(broadcast); } public void toastSyncUpdate(int syncType, int success, int total, int currentErrors, String dbError) { // if (App.DEBUG) // Log.v(TAG, "toasting a sync message with parameters=" + syncType + // ", " + success + ", " + total + ", " + currentErrors + ", " + // dbError); String currentToast = createToastString(syncType, success, total, currentErrors); if (currentToast != null) { StringBuilder result = new StringBuilder(); result.append(currentToast); if (currentErrors > 0) { result.append(" (").append( mContext.getResources().getQuantityString(R.plurals.errors, currentErrors, currentErrors)); if (dbError != null && !dbError.equalsIgnoreCase("")) result.append(" : ").append(dbError); result.append(")"); } // Send to Activity Intent broadcast = new Intent(SYNC_MESSAGE); broadcast.putExtra(TOAST_MESSAGE, result.toString()); broadcast.putExtra(TOAST_ERROR, (currentErrors == 0) ? false : true); LocalBroadcastManager.getInstance(mContext).sendBroadcast(broadcast); } } private String createToastString(int syncMethod, int success, int total, int currentErrors) { String toast = null; // Get string based on sync method int successNoSync = 0; int successAllSync = 0; int failPartialSync = 0; int failNoSync = 0; switch (syncMethod) { case UPLOAD_FORMS: failNoSync = R.string.sync_upload_forms_failed; failPartialSync = R.plurals.sync_upload_forms_partial; successNoSync = R.string.sync_upload_forms_not_needed; successAllSync = R.plurals.sync_upload_forms_successful; break; case DOWNLOAD_FORMS: failNoSync = R.string.sync_download_forms_failed; failPartialSync = R.plurals.sync_download_forms_partial; successNoSync = R.string.sync_download_forms_not_needed; successAllSync = R.plurals.sync_download_forms_successful; break; case DOWNLOAD_OBS: failNoSync = R.string.sync_download_obs_failed; // don't toast success break; default: return mSyncResultString; } // ERRORS: Add to Current Toast // Error 1: Exception if (currentErrors > 0) toast = mContext.getString(failNoSync); // Error 2: Partial upload/download else if (total > 0 && success != total) toast = mContext.getResources().getQuantityString(failPartialSync, total, String.valueOf(total - success) + " of " + String.valueOf(total)); // SUCCESS: Add to SyncResult Toast (Don't show until Sync Complete) else { StringBuilder sb = new StringBuilder(); if (mSyncResultString != null) { sb.append(mSyncResultString); sb.append(". "); } // Success 1: Nothing to upload/download if (total == 0 && currentErrors == 0) sb.append(mContext.getString(successNoSync)); // Success 2: All uploads/downloads were successful else if (total > 0 && success == total) sb.append(mContext.getResources().getQuantityString(successAllSync, total, String.valueOf(total))); mSyncResultString = sb.toString(); } return toast; } } // TESTING DOWNLOADS LOGGING FOR INSERTPATIENTS: // int logid = zdis.readInt(); // ih.bind(ptIdIndex, logid); // String logperson = zdis.readUTF(); // ih.bind(ptFamilyIndex, logperson); // if(App.DEBUG) Log.i(TAG, "Adding Patient Number " + i + " ID=" + logid + // " FamlyName=" + logperson); // private boolean isSyncActiveOrPending() { // AccountManager accountManager = AccountManager.get(mContext); // Account[] accounts = // accountManager.getAccountsByType(mContext.getString(R.string.app_account_type)); // // if (accounts.length <= 0) // return false; // else // return ContentResolver.isSyncActive(accounts[0], // getString(R.string.app_provider_authority)); // } // current = (int) Math.round(((float) i / (float) icount) * // 2.0); // if (current != progress) { // if (App.DEBUG) Log.v(TAG, "i=" + i + " current=" + current + " /progress=" + // progress + " icount=" + icount + " mProgress" + // sSyncProgress); // progress = current; // sSyncProgress = sSyncProgress + current; // }