package org.commcare.adapters;
import android.content.Context;
import android.database.DataSetObserver;
import android.os.AsyncTask.Status;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import org.commcare.CommCareApplication;
import org.commcare.activities.FormRecordListActivity.FormRecordFilter;
import org.commcare.models.database.SqlStorage;
import org.commcare.android.database.user.models.FormRecord;
import org.commcare.suite.model.Entry;
import org.commcare.suite.model.FormEntry;
import org.commcare.suite.model.Suite;
import org.commcare.suite.model.Text;
import org.commcare.tasks.FormRecordLoadListener;
import org.commcare.tasks.FormRecordLoaderTask;
import org.commcare.utils.AndroidCommCarePlatform;
import org.commcare.views.IncompleteFormRecordView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
/**
* Responsible for delegating the loading of form lists and performing
* filtering over them.
*
* @author ctsims
*/
public class IncompleteFormListAdapter extends BaseAdapter implements FormRecordLoadListener {
private final Context context;
private final List<DataSetObserver> observers = new ArrayList<>();
private FormRecordFilter filter;
/**
* All loaded form records of a given status, with no filtering.
*/
private final List<FormRecord> records = new ArrayList<>();
/**
* Filtered form records. getView reads from this to display all the forms.
*/
private final List<FormRecord> current = new ArrayList<>();
/**
* Maps FormRecord ID to an array of text that will be shown to the user
* and query-able. Text should includes modified date, record title, and
* form name.
*/
private final Hashtable<Integer, String[]> searchCache = new Hashtable<>();
/**
* The last query made, used to filter forms.
*/
private String query = "";
/**
* The current query split up by spaces. Used for filtering forms.
*/
private String[] queryPieces = new String[0];
private FormRecordLoaderTask loader;
/**
* Maps form namespace (unique id for forms) to their form title
* (entry-point text). Needed because FormRecords don't have form title
* info, but do have the namespace.
*/
private final Hashtable<String, Text> names = new Hashtable<>();
public IncompleteFormListAdapter(Context context,
AndroidCommCarePlatform platform,
FormRecordLoaderTask loader) {
this.context = context;
this.filter = null;
this.loader = loader;
loader.addListener(this);
// create a mapping from form definition IDs to their entry point text
for (Suite s : platform.getInstalledSuites()) {
for (Enumeration en = s.getEntries().elements(); en.hasMoreElements(); ) {
Entry entry = (Entry) en.nextElement();
if (!(entry.isView() || entry.isRemoteRequest())) {
String namespace = ((FormEntry)entry).getXFormNamespace();
//Some of our old definitions for views still come in as entries with dead
//namespaces for now, so check. Can clean up when FormEntry's enforce a
//namespace invariant
if(namespace != null) {
names.put(namespace, entry.getText());
}
}
}
}
}
/**
* Add a newly-loaded form to the current list if it satisfies the current
* query.
*/
@Override
public void notifyPriorityLoaded(FormRecord record, boolean isLoaded) {
if (isLoaded && satisfiesQuery(record)) {
current.add(record);
notifyDataSetChanged();
}
}
/**
* Notify observers that the form list has loaded/potentially been updated.
*/
@Override
public void notifyLoaded() {
notifyDataSetChanged();
}
/**
* Load new records and text if the given FormFilter differs from the
* current filter.
*
* @param newFilter update the internal FormFilter to this value
*/
public void setFilterAndResetRecords(FormRecordFilter newFilter) {
if (!newFilter.equals(this.filter)) {
setFormFilter(newFilter);
resetRecords();
}
}
/**
* Reload form record list for current filter status and collect pertinent
* text data using FormRecordLoaderTask; results will then be re-filtered
* and displayed via callbacks.
*/
public void resetRecords() {
// reload the form records, even if they are currently being loaded
if (loader.getStatus() == Status.RUNNING) {
loader.cancel(false);
loader = loader.spawn();
} else if (loader.getStatus() == Status.FINISHED) {
loader = loader.spawn();
}
SqlStorage<FormRecord> storage = CommCareApplication.instance().getUserStorage(FormRecord.class);
// choose a default filter if none set
if (filter == null) {
filter = FormRecordFilter.SubmittedAndPending;
}
records.clear();
String currentAppId = CommCareApplication.instance().getCurrentApp().getAppRecord().getApplicationId();
// Grab all form records that satisfy ANY of the statuses in the filter, AND belong to the
// currently seated app
for (String status : filter.getStatus()) {
records.addAll(storage.getRecordsForValues(
new String[]{FormRecord.META_STATUS, FormRecord.META_APP_ID},
new Object[]{status, currentAppId}));
}
// Sort FormRecords by modification time, most recent first.
Collections.sort(records, new Comparator<FormRecord>() {
@Override
public int compare(FormRecord left, FormRecord right) {
long leftModTime = left.lastModified().getTime();
long rightModTime = right.lastModified().getTime();
if (leftModTime > rightModTime) {
return -1;
} else if (leftModTime == rightModTime) {
return 0;
} else {
return 1;
}
}
});
searchCache.clear();
current.clear();
notifyDataSetChanged();
// load specific data about the 'records' into the searchCache, such as
// record title, form name, modified date
loader.init(searchCache, names);
loader.executeParallel(records.toArray(new FormRecord[records.size()]));
}
public int findRecordPosition(int formRecordId) {
for (int i = 0; i < current.size(); ++i) {
FormRecord record = current.get(i);
if (record.getID() == formRecordId) {
return i;
}
}
return -1;
}
@Override
public void notifyDataSetChanged() {
super.notifyDataSetChanged();
for (DataSetObserver observer : observers) {
observer.onChanged();
}
}
@Override
public void notifyDataSetInvalidated() {
super.notifyDataSetInvalidated();
resetRecords();
for (DataSetObserver observer : observers) {
observer.onChanged();
}
}
@Override
public boolean areAllItemsEnabled() {
return true;
}
@Override
public boolean isEnabled(int i) {
return true;
}
@Override
public int getCount() {
return current.size();
}
@Override
public Object getItem(int i) {
return current.get(i);
}
@Override
public long getItemId(int i) {
//Skeeeeetccchhyyyy maybe?
return current.get(i).getID();
}
@Override
public int getItemViewType(int i) {
return 0;
}
@Override
public View getView(int i, View v, ViewGroup vg) {
FormRecord r = current.get(i);
IncompleteFormRecordView ifrv = (IncompleteFormRecordView) v;
if (ifrv == null) {
ifrv = new IncompleteFormRecordView(context);
}
if (searchCache.containsKey(r.getID())) {
ifrv.setParams(r, searchCache.get(r.getID())[1], r.lastModified().getTime(), names);
} else {
// notify the loader that we need access to this record immediately
loader.registerPriority(r);
ifrv.setParams(r, "Loading...", r.lastModified().getTime(), names);
}
return ifrv;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean isEmpty() {
return getCount() == 0;
}
public void setFormFilter(FormRecordFilter filter) {
this.filter = filter;
}
public FormRecordFilter getFilter() {
return this.filter;
}
/**
* Filter loaded FormRecords by those whose data contains any word in the
* query field.
*
* Reads from FormRecords in the 'records' field and moves them into the
* cleared out the 'current' field.
*/
private void filterValues() {
if (loader.doneLoadingFormRecords()) {
current.clear();
if ("".equals(query)) {
current.addAll(records);
} else {
// collect all forms that have text data that contains pieces.
for (FormRecord r : records) {
if (satisfiesQuery(r)) {
current.add(r);
}
}
}
notifyDataSetChanged();
}
}
/**
* Does the form record have text that contains one of the query segments?
*
* @param r Lookup this form record's text and compare to the current query
* segments.
* @return Did the text corresponding to the form record argument contain
* any of the query segments?
*/
private boolean satisfiesQuery(FormRecord r) {
if (queryPieces.length == 0) {
// empty queries always pass
return true;
}
for (String cacheValue : searchCache.get(r.getID())) {
for (String piece : this.queryPieces) {
if (cacheValue.toLowerCase().contains(piece)) {
return true;
}
}
}
return false;
}
/**
* Re-filter form listing based on query parameter.
*
* @param newQuery set the current query to this value.
*/
public void applyTextFilter(String newQuery) {
if (this.query.trim().equals(newQuery.trim())) {
// don't perform filtering if old and new queries are same, modulo
// whitespace
return;
}
this.query = newQuery;
// split the query up into segments, by whitespace.
if ("".equals(this.query)) {
this.queryPieces = new String[0];
} else {
this.queryPieces = newQuery.toLowerCase().split(" ");
}
filterValues();
for (DataSetObserver o : observers) {
o.onChanged();
}
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
this.observers.add(observer);
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
this.observers.remove(observer);
}
public void release() {
if (loader.getStatus() == Status.RUNNING) {
loader.cancel(false);
}
}
public boolean isValid(int i) {
return names.containsKey(current.get(i).getFormNamespace());
}
}