package org.commcare.tasks;
import android.content.Context;
import android.text.format.DateUtils;
import android.util.Pair;
import org.commcare.CommCareApplication;
import org.commcare.models.AndroidSessionWrapper;
import org.commcare.models.database.AndroidSandbox;
import org.commcare.models.database.SqlStorage;
import org.commcare.android.database.user.models.FormRecord;
import org.commcare.android.database.user.models.SessionStateDescriptor;
import org.commcare.suite.model.Text;
import org.commcare.tasks.templates.ManagedAsyncTask;
import org.commcare.util.FormDataUtil;
import org.commcare.utils.AndroidCommCarePlatform;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Set;
/**
* Loads textual information for a list of FormRecords.
* <p/>
* This text currently includes the form name, record title, and last modified
* date
*
* @author ctsims
*/
public class FormRecordLoaderTask extends ManagedAsyncTask<FormRecord, Pair<FormRecord, ArrayList<String>>, Integer> {
private Hashtable<String, String> descriptorCache;
private final SqlStorage<SessionStateDescriptor> descriptorStorage;
private final AndroidCommCarePlatform platform;
private Hashtable<Integer, String[]> searchCache;
private final Context context;
// Functions to call when some or all of the data has been loaded. Data
// can be loaded normally, or be given precedence (priority), determining
// which callback is dispatched to the listeners.
private final ArrayList<FormRecordLoadListener> listeners = new ArrayList<>();
// These are all synchronized together
final private Queue<FormRecord> priorityQueue = new LinkedList<>();
// The IDs of FormRecords that have been loaded
private final Set<Integer> loaded = new HashSet<>();
// 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 Hashtable<String, Text> formNames;
// Is the background task done loading all the FormRecord information?
private boolean loadingComplete = false;
public FormRecordLoaderTask(Context c, SqlStorage<SessionStateDescriptor> descriptorStorage, AndroidCommCarePlatform platform) {
this(c, descriptorStorage, null, platform);
}
private FormRecordLoaderTask(Context c, SqlStorage<SessionStateDescriptor> descriptorStorage, Hashtable<String, String> descriptorCache, AndroidCommCarePlatform platform) {
this.context = c;
this.descriptorStorage = descriptorStorage;
this.descriptorCache = descriptorCache;
this.platform = platform;
}
/**
* Create a copy of this loader task.
*/
public FormRecordLoaderTask spawn() {
FormRecordLoaderTask task = new FormRecordLoaderTask(context, descriptorStorage, descriptorCache, platform);
task.setListeners(listeners);
return task;
}
/**
* Pass in hashtables that will be used to store data that is loaded.
*
* @param searchCache maps FormRecord ID to an array of query-able form descriptor text
* @param formNames map from form namespaces to their titles
*/
public void init(Hashtable<Integer, String[]> searchCache, Hashtable<String, Text> formNames) {
this.searchCache = searchCache;
if (descriptorCache == null) {
descriptorCache = new Hashtable<>();
}
priorityQueue.clear();
loaded.clear();
this.formNames = formNames;
}
/**
* Set the listeners list, whose callbacks will be executed once the data
* has been loaded.
*
* @param listeners a list of objects to call when data is done loading
*/
private void setListeners(ArrayList<FormRecordLoadListener> listeners) {
this.listeners.addAll(listeners);
}
/**
* Add a listener to the list that is called once the data has been loaded.
*
* @param listener an objects to call when data is done loading
*/
public void addListener(FormRecordLoadListener listener) {
this.listeners.add(listener);
}
@Override
protected Integer doInBackground(FormRecord... params) {
// Load text information for every FormRecord passed in, unless task is
// cancelled before that.
FormRecord current;
int loadedFormCount = 0;
while (loadedFormCount < params.length && !isCancelled()) {
synchronized (priorityQueue) {
//If we have one to do immediately, grab it
if (!priorityQueue.isEmpty()) {
current = priorityQueue.poll();
} else {
current = params[loadedFormCount++];
}
if (loaded.contains(current.getID())) {
// skip if we already loaded this record due to priority queue
continue;
}
}
// load text about this record: last modified date, title of the record, and form name
ArrayList<String> recordTextDesc = loadRecordText(current);
loaded.add(current.getID());
// Copy data into search task and notify anything waiting on this
// record.
this.publishProgress(new Pair<>(current, recordTextDesc));
}
return 1;
}
private ArrayList<String> loadRecordText(FormRecord current) {
ArrayList<String> recordTextDesc = new ArrayList<>();
// Get the date in a searchable format.
recordTextDesc.add(DateUtils.formatDateTime(context, current.lastModified().getTime(), DateUtils.FORMAT_NO_MONTH_DAY | DateUtils.FORMAT_NO_YEAR).toLowerCase());
String dataTitle = loadDataTitle(current.getID());
recordTextDesc.add(dataTitle);
if (formNames.containsKey(current.getFormNamespace())) {
Text name = formNames.get(current.getFormNamespace());
recordTextDesc.add(name.evaluate());
}
return recordTextDesc;
}
private String loadDataTitle(int formRecordId) {
// Grab our record hash
SessionStateDescriptor ssd = null;
try {
ssd = descriptorStorage.getRecordForValue(SessionStateDescriptor.META_FORM_RECORD_ID, formRecordId);
} catch (NoSuchElementException nsee) {
//s'all good
}
String dataTitle = "";
if (ssd != null) {
String descriptor = ssd.getSessionDescriptor();
if (!descriptorCache.containsKey(descriptor)) {
AndroidSessionWrapper asw = new AndroidSessionWrapper(platform);
asw.loadFromStateDescription(ssd);
try {
dataTitle =
FormDataUtil.getTitleFromSession(new AndroidSandbox(CommCareApplication.instance()),
asw.getSession(), asw.getEvaluationContext());
} catch (RuntimeException e) {
dataTitle = "[Unavailable]";
}
if (dataTitle == null) {
dataTitle = "";
}
descriptorCache.put(descriptor, dataTitle);
} else {
return descriptorCache.get(descriptor);
}
}
return dataTitle;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
// Tell users of the data being loaded that it isn't ready yet.
this.loadingComplete = false;
}
/**
* Has all the FormRecords' textual data been loaded yet? Used to let
* users of the data only start accessing it once it is all there.
*/
public boolean doneLoadingFormRecords() {
return this.loadingComplete;
}
@Override
protected void onPostExecute(Integer result) {
super.onPostExecute(result);
this.loadingComplete = true;
for (FormRecordLoadListener listener : this.listeners) {
if (listener != null) {
listener.notifyLoaded();
}
}
// free up things we don't need to spawn new tasks
priorityQueue.clear();
loaded.clear();
formNames = null;
}
@Override
protected void onProgressUpdate(Pair<FormRecord, ArrayList<String>>... values) {
super.onProgressUpdate(values);
// copy a single form record's data out of method arguments
String[] vals = new String[values[0].second.size()];
for (int i = 0; i < vals.length; ++i) {
vals[i] = values[0].second.get(i);
}
// store the loaded data in the search cache
this.searchCache.put(values[0].first.getID(), vals);
for (FormRecordLoadListener listener : this.listeners) {
if (listener != null) {
// TODO PLM: pretty sure loaded.contains(values[0].first) is
// always true at this point.
listener.notifyPriorityLoaded(values[0].first,
loaded.contains(values[0].first.getID()));
}
}
}
public boolean registerPriority(FormRecord record) {
synchronized (priorityQueue) {
if (loaded.contains(record.getID())) {
return false;
} else if (priorityQueue.contains(record)) {
// if we already have it in the queue, just move along
return true;
} else {
priorityQueue.add(record);
return true;
}
}
}
}