package org.commcare.activities; import android.annotation.TargetApi; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; 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.SearchView; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import org.commcare.CommCareApplication; import org.commcare.adapters.IncompleteFormListAdapter; import org.commcare.dalvik.R; import org.commcare.logging.AndroidLogger; import org.commcare.google.services.analytics.GoogleAnalyticsFields; import org.commcare.google.services.analytics.GoogleAnalyticsUtils; import org.commcare.logic.ArchivedFormRemoteRestore; import org.commcare.models.FormRecordProcessor; import org.commcare.android.database.user.models.FormRecord; import org.commcare.android.database.user.models.SessionStateDescriptor; import org.commcare.preferences.CommCareServerPreferences; import org.commcare.tasks.DataPullTask; import org.commcare.tasks.FormRecordCleanupTask; import org.commcare.tasks.FormRecordLoadListener; import org.commcare.tasks.FormRecordLoaderTask; import org.commcare.tasks.PurgeStaleArchivedFormsTask; import org.commcare.tasks.TaskListener; import org.commcare.tasks.TaskListenerRegistrationException; import org.commcare.utils.AndroidCommCarePlatform; import org.commcare.utils.CommCareUtil; import org.commcare.utils.SessionUnavailableException; import org.commcare.views.IncompleteFormRecordView; import org.commcare.views.dialogs.StandardAlertDialog; import org.commcare.views.dialogs.CustomProgressDialog; import org.javarosa.core.services.Logger; import org.javarosa.core.services.locale.Localization; public class FormRecordListActivity extends SessionAwareCommCareActivity<FormRecordListActivity> implements TextWatcher, FormRecordLoadListener, OnItemClickListener, TaskListener<Void, Void> { private static final String TAG = FormRecordListActivity.class.getSimpleName(); private static final String FORM_RECORD_URL = CommCareServerPreferences.PREFS_FORM_RECORD_KEY; 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 BARCODE_FETCH = 1; public static final String KEY_INITIAL_RECORD_ID = "cc_initial_rec_id"; private AndroidCommCarePlatform platform; private IncompleteFormListAdapter adapter; private PurgeStaleArchivedFormsTask purgeTask; private int initialSelection = -1; private EditText searchbox; private ListView listView; private SearchView searchView; private MenuItem searchItem; private View.OnClickListener barcodeScanOnClickListener; private boolean incompleteMode; 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}), /** * Limbo forms **/ Limbo("form.record.filter.limbo", new String[]{FormRecord.STATUS_LIMBO}); FormRecordFilter(String message, String[] statuses) { this.message = message; this.statuses = statuses; } private final String message; private final String[] statuses; public String getMessage() { return message; } public String[] getStatus() { return statuses; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); platform = CommCareApplication.instance().getCommCarePlatform(); setContentView(R.layout.entity_select_layout); findViewById(R.id.entity_select_loading).setVisibility(View.GONE); searchbox = (EditText)findViewById(R.id.searchbox); LinearLayout header = (LinearLayout)findViewById(R.id.entity_select_header); ImageButton barcodeButton = (ImageButton)findViewById(R.id.barcodeButton); Spinner 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); barcodeScanOnClickListener = new View.OnClickListener() { @Override public void onClick(View v) { callBarcodeScanIntent(FormRecordListActivity.this); } }; TextView searchLabel = (TextView)findViewById(R.id.screen_entity_select_search_label); searchLabel.setText(this.localize("select.search.label")); searchbox.addTextChangedListener(this); FormRecordLoaderTask task = new FormRecordLoaderTask(this, CommCareApplication.instance().getUserStorage(SessionStateDescriptor.class), platform); task.addListener(this); adapter = new IncompleteFormListAdapter(this, platform, task); 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)) { incompleteMode = true; //special case, no special filtering options adapter.setFormFilter(FormRecordFilter.Incomplete); adapter.resetRecords(); } } else { 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<>(this, R.layout.form_filter_display, names); filterSelect.setAdapter(spinneritems); spinneritems.setDropDownViewResource(R.layout.form_filter_item); filterSelect.setOnItemSelectedListener(new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> arg0, View arg1, int index, long id) { // NOTE: This gets called every time a spinner gets // set-up and also every time spinner state is restored // on scree-rotation. Hence we defer onCreate record // loading until this gets triggered automatically. adapter.setFilterAndResetRecords(FormRecordFilter.values()[index]); //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(); } } @Override public void onNothingSelected(AdapterView<?> arg0) { // TODO Auto-generated method stub } }); filterSelect.setVisibility(View.VISIBLE); } this.registerForContextMenu(listView); refreshView(); restoreLastQueryString(); if (!isUsingActionBar()) { setSearchText(lastQueryString); } } private static void callBarcodeScanIntent(Activity act) { Intent i = new Intent("com.google.zxing.client.android.SCAN"); try { act.startActivityForResult(i, BARCODE_FETCH); } catch (ActivityNotFoundException anfe) { Toast.makeText(act, "No barcode reader available! You can install one " + "from the android market.", Toast.LENGTH_LONG).show(); } } @Override protected void onPause() { super.onPause(); // stop showing blocking dialog and getting updates from purge task. unregisterTask(); } private void unregisterTask() { if (purgeTask != null) { try { purgeTask.unregisterTaskListener(this); dismissProgressDialog(); } catch (TaskListenerRegistrationException e) { Log.e(TAG, "Attempting to unregister a not previously " + "registered TaskListener."); } purgeTask = null; } } @Override protected void onStop() { super.onStop(); saveLastQueryString(); } private void onBarcodeFetch(int resultCode, Intent intent) { if (resultCode == Activity.RESULT_OK) { String result = intent.getStringExtra("SCAN_RESULT"); if (result != null) { result = result.trim(); } setSearchText(result); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { switch (requestCode) { case BARCODE_FETCH: onBarcodeFetch(resultCode, intent); break; default: super.onActivityResult(requestCode, resultCode, intent); } } @Override 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. */ public void refreshView() { disableSearch(); listView.setAdapter(adapter); } @Override protected void onResumeSessionSafe() { attachToPurgeTask(); if (adapter != null && initialSelection != -1) { listView.setSelection(adapter.findRecordPosition(initialSelection)); } } /** * Attach activity to running purge task to block user while form purging * is in progress. */ private void attachToPurgeTask() { purgeTask = PurgeStaleArchivedFormsTask.getRunningInstance(); try { if (purgeTask != null) { purgeTask.registerTaskListener(this); showProgressDialog(PurgeStaleArchivedFormsTask.PURGE_STALE_ARCHIVED_FORMS_TASK_ID); } } catch (TaskListenerRegistrationException e) { Log.e(TAG, "Attempting to register a TaskListener to an already " + "registered task."); } } private void setSearchEnabled(boolean enabled) { if (isUsingActionBar()) { searchView.setEnabled(enabled); } else { searchbox.setEnabled(enabled); } } private void disableSearch() { setSearchEnabled(false); } private void enableSearch() { setSearchEnabled(true); } /** * Stores the path of selected form and finishes. */ @Override public void onItemClick(AdapterView<?> listView, View view, int position, long id) { if (incompleteMode) { GoogleAnalyticsUtils.reportOpenArchivedForm(GoogleAnalyticsFields.LABEL_INCOMPLETE); } else { GoogleAnalyticsUtils.reportOpenArchivedForm(GoogleAnalyticsFields.LABEL_COMPLETE); } 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 { showAlertDialog(StandardAlertDialog.getBasicAlertDialog(this, "Form Missing", Localization.get("form.record.gone.message"), null)); } } @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")); } @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: FormRecord toDelete = CommCareApplication.instance().getUserStorage(FormRecord.class).read((int)info.id); toDelete.logPendingDeletion(TAG, "the user manually selected 'DELETE' in FormRecordListActivity"); FormRecordCleanupTask.wipeRecord(this, toDelete); listView.post(new Runnable() { @Override public void run() { adapter.notifyDataSetInvalidated(); } }); return true; case RESTORE_RECORD: FormRecord record = (FormRecord)adapter.getItem(info.position); new FormRecordProcessor(this).updateRecordStatus(record, FormRecord.STATUS_UNSENT); adapter.resetRecords(); adapter.notifyDataSetChanged(); return true; 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 e) { //TODO: Login and try again return true; } } private void createFormRecordScanResultDialog(Pair<Boolean, String> result) { String title; if (result.first) { title = Localization.get("app.workflow.forms.scan.title.valid"); } else { title = Localization.get("app.workflow.forms.scan.title.invalid"); } int resId = result.first ? R.drawable.checkmark : R.drawable.redx; showAlertDialog(StandardAlertDialog.getBasicAlertDialogWithIcon(this, title, result.second, resId, null)); } /** * Checks if the action bar view is active */ private boolean isUsingActionBar() { return searchView != null; } @SuppressWarnings("NewApi") private void setSearchText(CharSequence text) { if (isUsingActionBar()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { searchItem.expandActionView(); } searchView.setQuery(text, false); } searchbox.setText(text); } @Override public boolean onCreateOptionsMenu(Menu menu) { boolean parent = super.onCreateOptionsMenu(menu); tryToAddSearchActionToAppBar(this, menu, new ActionBarInstantiator() { // this should be unnecessary... @TargetApi(Build.VERSION_CODES.HONEYCOMB) @Override public void onActionBarFound(MenuItem searchItem, SearchView searchView, MenuItem barcodeItem) { FormRecordListActivity.this.searchItem = searchItem; FormRecordListActivity.this.searchView = searchView; if (lastQueryString != null && lastQueryString.length() > 0) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { searchItem.expandActionView(); } setSearchText(lastQueryString); if (adapter != null) { adapter.applyTextFilter(lastQueryString == null ? "" : lastQueryString); } } FormRecordListActivity.this.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { return true; } @Override public boolean onQueryTextChange(String newText) { adapter.applyTextFilter(newText); return false; } }); } }); if (!FormRecordFilter.Incomplete.equals(adapter.getFilter())) { SharedPreferences prefs = CommCareApplication.instance().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.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; } @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(); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case DOWNLOAD_FORMS: SharedPreferences prefs = CommCareApplication.instance().getCurrentApp().getAppPreferences(); String source = prefs.getString(FORM_RECORD_URL, this.getString(R.string.form_record_url)); ArchivedFormRemoteRestore.pullArchivedFormsFromServer(source, this, platform); return true; case MENU_SUBMIT_QUARANTINE_REPORT: generateQuarantineReport(); return true; case R.id.barcode_scan_action_bar: barcodeScanOnClickListener.onClick(null); return true; case R.id.menu_settings: HomeScreenBaseActivity.createPreferencesMenu(this); 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); } @Override protected void onDestroy() { super.onDestroy(); adapter.release(); } @Override public int getWakeLockLevel() { return PowerManager.PARTIAL_WAKE_LOCK; } @Override public void afterTextChanged(Editable s) { String filtertext = s.toString(); if (searchbox.getText() == s) { adapter.applyTextFilter(filtertext); } if (!isUsingActionBar()) { lastQueryString = filtertext; } } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void notifyPriorityLoaded(FormRecord record, boolean priority) { } @Override public void notifyLoaded() { enableSearch(); } @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 ArchivedFormRemoteRestore.CLEANUP_ID: title = "Fetching Old Forms"; message = "Forms downloaded. Processing..."; break; case PurgeStaleArchivedFormsTask.PURGE_STALE_ARCHIVED_FORMS_TASK_ID: title = Localization.get("form.archive.purge.title"); message = Localization.get("form.archive.purge.message"); break; default: Log.w(TAG, "taskId passed to generateProgressDialog does not match " + "any valid possibilities in FormRecordListActivity"); return null; } return CustomProgressDialog.newInstance(title, message, taskId); } @Override public void handleTaskUpdate(Void... updateVals) { } /** * Archived form purging task complete, stop blocking user */ @Override public void handleTaskCompletion(Void result) { dismissProgressDialog(); // reload form list to make sure purged forms aren't shown if (adapter != null) { adapter.resetRecords(); } } /** * Archived form purging task cancelled, stop blocking user */ @Override public void handleTaskCancellation() { dismissProgressDialog(); } }