/* * Copyright (C) 2009 University of Washington * * 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 org.commcare.dalvik.activities; import java.io.IOException; import org.commcare.android.adapters.IncompleteFormListAdapter; import org.commcare.android.database.user.models.FormRecord; import org.commcare.android.database.user.models.SessionStateDescriptor; import org.commcare.android.database.user.models.User; import org.commcare.android.framework.CommCareActivity; import org.commcare.android.javarosa.AndroidLogger; import org.commcare.android.models.logic.FormRecordProcessor; import org.commcare.android.tasks.DataPullTask; import org.commcare.android.tasks.FormRecordCleanupTask; import org.commcare.android.tasks.FormRecordLoadListener; import org.commcare.android.tasks.FormRecordLoaderTask; import org.commcare.android.util.AndroidCommCarePlatform; import org.commcare.android.util.CommCareUtil; import org.commcare.android.util.SessionUnavailableException; import org.commcare.android.view.IncompleteFormRecordView; import org.commcare.dalvik.R; import org.commcare.dalvik.application.CommCareApplication; import org.commcare.dalvik.dialogs.CustomProgressDialog; import org.javarosa.core.services.Logger; import org.javarosa.core.services.locale.Localization; import org.javarosa.core.services.storage.StorageFullException; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; import android.speech.tts.TextToSpeech; import android.text.Editable; import android.text.TextWatcher; import android.util.Pair; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; public class FormRecordListActivity extends CommCareActivity<FormRecordListActivity> implements TextWatcher, FormRecordLoadListener, OnItemClickListener { private static final int OPEN_RECORD = Menu.FIRST; private static final int DELETE_RECORD = Menu.FIRST + 1; private static final int RESTORE_RECORD = Menu.FIRST + 2; private static final int SCAN_RECORD = Menu.FIRST + 3; private static final int DOWNLOAD_FORMS = Menu.FIRST; private static final int MENU_SUBMIT_QUARANTINE_REPORT = Menu.FIRST + 1; private static final int CLEANUP_ID = 0; public static final String KEY_INITIAL_RECORD_ID = "cc_initial_rec_id"; private AndroidCommCarePlatform platform; private IncompleteFormListAdapter adapter; private int initialSelection = -1; private EditText searchbox; private LinearLayout header; private ImageButton barcodeButton; private Spinner filterSelect; private ListView listView; public enum FormRecordFilter { /** Processed and Pending **/ SubmittedAndPending("form.record.filter.subandpending", new String[] {FormRecord.STATUS_SAVED, FormRecord.STATUS_UNSENT}), /** Submitted Only **/ Submitted("form.record.filter.submitted", new String[] {FormRecord.STATUS_SAVED}), /** Pending Submission **/ Pending("form.record.filter.pending", new String[] {FormRecord.STATUS_UNSENT}), /** Incomplete forms **/ Incomplete("form.record.filter.incomplete", new String[] {FormRecord.STATUS_INCOMPLETE}, false), /** Limbo forms **/ Limbo("form.record.filter.limbo", new String[] {FormRecord.STATUS_LIMBO}, false); FormRecordFilter(String message, String[] statuses) {this(message, statuses, true);} FormRecordFilter(String message, String[] statuses, boolean visible) {this.message = message; this.statuses = statuses; this.visible = visible;} private final String message; private final String[] statuses; public boolean visible; public String getMessage() { return message;} public String[] getStatus() { return statuses; } } /* * (non-Javadoc) * @see org.commcare.android.framework.CommCareActivity#onCreate(android.os.Bundle) */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); try { platform = CommCareApplication._().getCommCarePlatform(); setContentView(R.layout.entity_select_layout); findViewById(R.id.entity_select_loading).setVisibility(View.GONE); searchbox = (EditText)findViewById(R.id.searchbox); header = (LinearLayout)findViewById(R.id.entity_select_header); barcodeButton = (ImageButton)findViewById(R.id.barcodeButton); filterSelect = (Spinner)findViewById(R.id.entity_select_filter_dropdown); listView = (ListView)findViewById(R.id.screen_entity_select_list); listView.setOnItemClickListener(this); header.setVisibility(View.GONE); barcodeButton.setVisibility(View.GONE); TextView searchLabel = (TextView)findViewById(R.id.screen_entity_select_search_label); searchLabel.setText(Localization.get("select.search.label")); searchbox.addTextChangedListener(this); FormRecordLoaderTask task = new FormRecordLoaderTask(this, CommCareApplication._().getUserStorage(SessionStateDescriptor.class), platform); task.setListener(this); adapter = new IncompleteFormListAdapter(this, platform, task); FormRecordFilter filter = null; initialSelection = this.getIntent().getIntExtra(KEY_INITIAL_RECORD_ID, -1); if(this.getIntent().hasExtra(FormRecord.META_STATUS)) { String incomingFilter = this.getIntent().getStringExtra(FormRecord.META_STATUS); if(incomingFilter.equals(FormRecord.STATUS_INCOMPLETE)) { //special case, no special filtering options filter = FormRecordFilter.Incomplete; } } else { filter = FormRecordFilter.SubmittedAndPending; FormRecordFilter[] filters = FormRecordFilter.values(); String[] names = new String[filters.length]; for(int i = 0 ; i < filters.length; ++i ) { names[i] = Localization.get(filters[i].getMessage()); } ArrayAdapter<String> spinneritems = new ArrayAdapter<String>(this, R.layout.form_filter_display, names); filterSelect.setAdapter(spinneritems); spinneritems.setDropDownViewResource(R.layout.form_filter_item); filterSelect.setOnItemSelectedListener(new OnItemSelectedListener() { /* * (non-Javadoc) * @see android.widget.AdapterView.OnItemSelectedListener#onItemSelected(android.widget.AdapterView, android.view.View, int, long) */ @Override public void onItemSelected(AdapterView<?> arg0, View arg1, int index, long id) { adapter.setFormFilter(FormRecordFilter.values()[index]); adapter.resetRecords(); adapter.notifyDataSetChanged(); //This is only relevant with the new menu format, old menus have a hard //button and don't need their menu to be rebuilt if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { invalidateOptionsMenu(); } } /* * (non-Javadoc) * @see android.widget.AdapterView.OnItemSelectedListener#onNothingSelected(android.widget.AdapterView) */ @Override public void onNothingSelected(AdapterView<?> arg0) { // TODO Auto-generated method stub } }); filterSelect.setVisibility(View.VISIBLE); } if(filter != null) { adapter.setFormFilter(filter); } this.registerForContextMenu(listView); refreshView(); } catch(SessionUnavailableException sue) { //TODO: session is dead, login and return } } public String getActivityTitle() { if(adapter == null){ return Localization.get("app.workflow.saved.heading"); } if(adapter.getFilter() == FormRecordFilter.Incomplete) { return Localization.get("app.workflow.incomplete.heading"); } else { return Localization.get("app.workflow.saved.heading"); } } /** * Get form list from database and insert into view. */ private void refreshView() { disableSearch(); adapter.resetRecords(); listView.setAdapter(adapter); } protected void onResume() { super.onResume(); if(adapter != null && initialSelection != -1) { listView.setSelection(adapter.findRecordPosition(initialSelection)); } } protected void disableSearch() { searchbox.setEnabled(false); } protected void enableSearch() { searchbox.setEnabled(true); } /* * (non-Javadoc) * @see android.widget.AdapterView.OnItemClickListener#onItemClick(android.widget.AdapterView, android.view.View, int, long) * * Stores the path of selected form and finishes. */ @Override public void onItemClick(AdapterView<?> listView, View view, int position, long id) { returnItem(position); } private void returnItem(int position) { if(adapter.isValid(position)) { FormRecord value = (FormRecord)adapter.getItem(position); // We want to actually launch an interactive form entry. Intent i = new Intent(); i.putExtra("FORMRECORDS", value.getID()); setResult(RESULT_OK, i); finish(); } else { new AlertDialog.Builder(this).setMessage(Localization.get("form.record.gone.message")).create().show(); } } /* * (non-Javadoc) * @see android.app.Activity#onCreateContextMenu(android.view.ContextMenu, android.view.View, android.view.ContextMenu.ContextMenuInfo) */ @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)menuInfo; IncompleteFormRecordView ifrv = (IncompleteFormRecordView)adapter.getView(info.position, null, null); menu.setHeaderTitle(ifrv.mPrimaryTextView.getText() + " (" + ifrv.mRightTextView.getText() + ")"); FormRecord value = (FormRecord)adapter.getItem(info.position); menu.add(Menu.NONE, OPEN_RECORD, OPEN_RECORD, Localization.get("app.workflow.forms.open")); menu.add(Menu.NONE, DELETE_RECORD, DELETE_RECORD, Localization.get("app.workflow.forms.delete")); if(FormRecord.STATUS_LIMBO.equals(value.getStatus())) { menu.add(Menu.NONE, RESTORE_RECORD, RESTORE_RECORD, Localization.get("app.workflow.forms.restore")); } menu.add(Menu.NONE, SCAN_RECORD, SCAN_RECORD, Localization.get("app.workflow.forms.scan")); } /* * (non-Javadoc) * @see android.app.Activity#onContextItemSelected(android.view.MenuItem) */ @Override public boolean onContextItemSelected(MenuItem item) { try { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); switch(item.getItemId()) { case OPEN_RECORD: returnItem(info.position); return true; case DELETE_RECORD: FormRecordCleanupTask.wipeRecord(this, CommCareApplication._().getUserStorage(FormRecord.class).read((int)info.id)); listView.post(new Runnable() { public void run() {adapter.notifyDataSetInvalidated();}}); case RESTORE_RECORD: FormRecord record = (FormRecord)adapter.getItem(info.position); try { new FormRecordProcessor(this).updateRecordStatus(record, FormRecord.STATUS_UNSENT); adapter.resetRecords(); adapter.notifyDataSetChanged(); return true; } catch (StorageFullException e) {} catch (IOException e) { Logger.log(AndroidLogger.TYPE_ERROR_STORAGE, "error restoring quarantined record: " + e.getMessage()); } case SCAN_RECORD: FormRecord theRecord = (FormRecord)adapter.getItem(info.position); Pair<Boolean, String> result = new FormRecordProcessor(this).verifyFormRecordIntegrity(theRecord); createFormRecordScanResultDialog(result); } return true; } catch(SessionUnavailableException sue) { //TODO: Login and try again return true; } } private void createFormRecordScanResultDialog(Pair<Boolean, String> result) { AlertDialog mAlertDialog = new AlertDialog.Builder(this).create(); mAlertDialog.setIcon(result.first ? R.drawable.checkmark : R.drawable.redx); mAlertDialog.setTitle(result.first ? Localization.get("app.workflow.forms.scan.title.valid") : Localization.get("app.workflow.forms.scan.title.invalid")); mAlertDialog.setMessage(result.second); DialogInterface.OnClickListener errorListener = new DialogInterface.OnClickListener() { /* * (non-Javadoc) * @see android.content.DialogInterface.OnClickListener#onClick(android.content.DialogInterface, int) */ @Override public void onClick(DialogInterface dialog, int i) { switch (i) { case DialogInterface.BUTTON1: break; } } }; mAlertDialog.setCancelable(false); mAlertDialog.setButton(Localization.get("dialog.ok"), errorListener); mAlertDialog.show(); } /* * (non-Javadoc) * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu) */ @Override public boolean onCreateOptionsMenu(Menu menu) { boolean parent = super.onCreateOptionsMenu(menu); if(!FormRecordFilter.Incomplete.equals(adapter.getFilter())) { SharedPreferences prefs =CommCareApplication._().getCurrentApp().getAppPreferences(); String source = prefs.getString("form-record-url", this.getString(R.string.form_record_url)); //If there's nowhere to fetch forms from, we can't really go fetch them if(!(source == null || source.equals(""))) { menu.add(0, DOWNLOAD_FORMS, 0, Localization.get("app.workflow.forms.fetch")).setIcon(android.R.drawable.ic_menu_rotate); } menu.add(0, MENU_SUBMIT_QUARANTINE_REPORT, MENU_SUBMIT_QUARANTINE_REPORT, Localization.get("app.workflow.forms.quarantine.report")); return true; } return parent; } /* * (non-Javadoc) * @see android.app.Activity#onPrepareOptionsMenu(android.view.Menu) */ @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); MenuItem quarantine = menu.findItem(MENU_SUBMIT_QUARANTINE_REPORT); if(quarantine != null) { if(FormRecordFilter.Limbo.equals(adapter.getFilter())) { quarantine.setVisible(true); } else { quarantine.setVisible(false); } } return menu.hasVisibleItems(); } TextToSpeech mTts; /* * (non-Javadoc) * @see org.commcare.android.framework.CommCareActivity#onOptionsItemSelected(android.view.MenuItem) */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case DOWNLOAD_FORMS: SharedPreferences prefs = CommCareApplication._().getCurrentApp().getAppPreferences(); User u = CommCareApplication._().getSession().getLoggedInUser(); String source = prefs.getString("form-record-url", this.getString(R.string.form_record_url)); //We should go digest auth this user on the server and see whether to pull them //down. DataPullTask<FormRecordListActivity> pull = new DataPullTask<FormRecordListActivity>(u.getUsername(),u.getCachedPwd(), source, "", this) { /* * (non-Javadoc) * @see org.commcare.android.tasks.templates.CommCareTask#deliverResult(java.lang.Object, java.lang.Object) */ @Override protected void deliverResult(FormRecordListActivity receiver, Integer status) { switch(status) { case DataPullTask.DOWNLOAD_SUCCESS: FormRecordCleanupTask<FormRecordListActivity> task = new FormRecordCleanupTask<FormRecordListActivity>(FormRecordListActivity.this, platform,CLEANUP_ID) { /* * (non-Javadoc) * @see org.commcare.android.tasks.templates.CommCareTask#deliverResult(java.lang.Object, java.lang.Object) */ @Override protected void deliverResult( FormRecordListActivity receiver, Integer result) { receiver.refreshView(); } /* * (non-Javadoc) * @see org.commcare.android.tasks.templates.CommCareTask#deliverUpdate(java.lang.Object, java.lang.Object[]) */ @Override protected void deliverUpdate( FormRecordListActivity receiver, Integer... values) { if(values[0] < 0) { if(values[0] == FormRecordCleanupTask.STATUS_CLEANUP) { receiver.updateProgress("Forms Processed. " + "Cleaning up form records...", CLEANUP_ID); } } else { receiver.updateProgress("Forms downloaded. Processing " + values[0] + " of " + values[1] +"...", CLEANUP_ID); } } /* * (non-Javadoc) * @see org.commcare.android.tasks.templates.CommCareTask#deliverError(java.lang.Object, java.lang.Exception) */ @Override protected void deliverError( FormRecordListActivity receiver, Exception e) { receiver.taskError(e); } }; task.connect(receiver); task.execute(); break; case DataPullTask.UNKNOWN_FAILURE: Toast.makeText(receiver, "Failure retrieving or processing data, please try again later...", Toast.LENGTH_LONG).show(); break; case DataPullTask.AUTH_FAILED: Toast.makeText(receiver, "Authentication failure. Please logout and resync with the server and try again.", Toast.LENGTH_LONG).show(); break; case DataPullTask.BAD_DATA: Toast.makeText(receiver, "Bad data from server. Please talk with your supervisor.", Toast.LENGTH_LONG).show(); break; case DataPullTask.CONNECTION_TIMEOUT: Toast.makeText(receiver, "The server took too long to generate a response. Please try again later, and ask your supervisor if the problem persists.", Toast.LENGTH_LONG).show(); break; case DataPullTask.SERVER_ERROR: Toast.makeText(receiver, "The server had an error processing your data. Please try again later, and contact technical support if the problem persists.", Toast.LENGTH_LONG).show(); break; case DataPullTask.UNREACHABLE_HOST: Toast.makeText(receiver, "Couldn't contact server, please check your network connection and try again.", Toast.LENGTH_LONG).show(); break; } } /* * (non-Javadoc) * @see org.commcare.android.tasks.templates.CommCareTask#deliverUpdate(java.lang.Object, java.lang.Object[]) */ @Override protected void deliverUpdate(FormRecordListActivity receiver, Integer... update) { switch(update[0]){ case DataPullTask.PROGRESS_AUTHED: receiver.updateProgress("Authed with server, downloading forms" + (update[1] == 0 ? "" : " (" +update[1] + ")"), DataPullTask.DATA_PULL_TASK_ID); break; } } /* * (non-Javadoc) * @see org.commcare.android.tasks.templates.CommCareTask#deliverError(java.lang.Object, java.lang.Exception) */ @Override protected void deliverError(FormRecordListActivity receiver, Exception e) { receiver.taskError(e); } }; pull.connect(this); pull.execute(); return true; case MENU_SUBMIT_QUARANTINE_REPORT: generateQuarantineReport(); return true; } return super.onOptionsItemSelected(item); } private void generateQuarantineReport() { FormRecordProcessor processor = new FormRecordProcessor(this); Logger.log(AndroidLogger.TYPE_ERROR_STORAGE, "Beginning form Quarantine report"); for(int i = 0 ; i < adapter.getCount() ; ++i) { FormRecord r = (FormRecord)adapter.getItem(i); Pair<Boolean, String> integrity = processor.verifyFormRecordIntegrity(r); String passfail = integrity.first ? "PASS:": "FAIL:"; Logger.log(AndroidLogger.TYPE_ERROR_STORAGE,passfail + integrity.second); } CommCareUtil.triggerLogSubmission(this); } /* * (non-Javadoc) * @see org.commcare.android.framework.CommCareActivity#onDestroy() */ @Override protected void onDestroy() { super.onDestroy(); adapter.release(); } /* * (non-Javadoc) * @see org.commcare.android.framework.CommCareActivity#getWakeLockingLevel() */ @Override protected int getWakeLockingLevel() { return PowerManager.PARTIAL_WAKE_LOCK; } public void afterTextChanged(Editable s) { if(searchbox.getText() == s) { adapter.applyTextFilter(s.toString()); } } public void beforeTextChanged(CharSequence s, int start, int count, int after) { //nothing } public void onTextChanged(CharSequence s, int start, int before, int count) { //nothing } public void notifyPriorityLoaded(Integer record, boolean priority) { if(priority) { adapter.notifyDataSetChanged(); } } public void notifyLoaded() { enableSearch(); } /* * (non-Javadoc) * @see org.commcare.android.framework.CommCareActivity#generateProgressDialog(int) * * Implementation of generateProgressDialog() for DialogController -- other methods * handled entirely in CommCareActivity */ @Override public CustomProgressDialog generateProgressDialog(int taskId) { String title, message; switch (taskId) { case DataPullTask.DATA_PULL_TASK_ID: title = "Fetching Old Forms"; message = "Connecting to server..."; break; case CLEANUP_ID: title = "Fetching Old Forms"; message = "Forms downloaded. Processing..."; break; default: System.out.println("WARNING: taskId passed to generateProgressDialog does not match " + "any valid possibilities in FormRecordListActivity"); return null; } return CustomProgressDialog.newInstance(title, message, taskId); } }