package org.commcare.adapters;
import android.database.DataSetObserver;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import org.commcare.CommCareApplication;
import org.commcare.activities.CommCareActivity;
import org.commcare.cases.entity.Entity;
import org.commcare.cases.entity.EntitySortNotificationInterface;
import org.commcare.cases.entity.EntitySorter;
import org.commcare.cases.entity.NodeEntityFactory;
import org.commcare.dalvik.R;
import org.commcare.models.AsyncNodeEntityFactory;
import org.commcare.preferences.CommCarePreferences;
import org.commcare.session.SessionInstanceBuilder;
import org.commcare.suite.model.Action;
import org.commcare.suite.model.Detail;
import org.commcare.utils.AndroidUtil;
import org.commcare.utils.CachingAsyncImageLoader;
import org.commcare.utils.StringUtils;
import org.commcare.views.EntityActionViewUtils;
import org.commcare.views.EntityView;
import org.commcare.views.EntityViewTile;
import org.commcare.views.notifications.NotificationMessageFactory;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.core.services.locale.Localization;
import org.javarosa.core.util.OrderedHashtable;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
/**
* This adapter class handles displaying the cases for a CommCareODK user.
* Depending on the <grid> block of the Detail this adapter is constructed with, cases might be
* displayed as normal EntityViews or as AdvancedEntityViews
*
* @author ctsims
* @author wspride
*/
public class EntityListAdapter implements ListAdapter, EntitySortNotificationInterface {
public static final int ENTITY_TYPE = 0;
public static final int ACTION_TYPE = 1;
public static final int DIVIDER_TYPE = 2;
public static final int DIVIDER_ID = -2;
private int dividerPosition = 0;
private final int actionsCount;
private final int dividerCount;
private boolean mFuzzySearchEnabled = true;
private boolean isFilteringByCalloutResult = false;
private final CommCareActivity commCareActivity;
private final Detail detail;
private final List<DataSetObserver> observers;
private final List<Entity<TreeReference>> full;
private List<Entity<TreeReference>> current;
private final List<TreeReference> references;
private final List<Action> actions;
private TreeReference selected;
private int[] currentSort = {};
private boolean reverseSort = false;
private final NodeEntityFactory mNodeFactory;
private boolean mAsyncMode = false;
private String[] currentSearchTerms;
private String searchQuery = "";
private EntityFiltererBase entityFilterer = null;
// Asyncronous image loader, allows rows with images to scroll smoothly
private final CachingAsyncImageLoader mImageLoader;
// false until we determine the Detail has at least one <grid> block
private boolean usesCaseTiles = false;
// key to data mapping used to attach callout results to individual entities
private OrderedHashtable<String, String> calloutResponseData = new OrderedHashtable<>();
private final boolean selectActivityInAwesomeMode;
public EntityListAdapter(CommCareActivity activity, Detail detail,
List<TreeReference> references,
List<Entity<TreeReference>> full,
int[] sort, NodeEntityFactory factory,
boolean hideActions, List<Action> actions, boolean inAwesomeMode) {
this.detail = detail;
this.selectActivityInAwesomeMode = inAwesomeMode;
this.actions = actions;
if (actions == null || actions.isEmpty() || hideActions) {
actionsCount = 0;
dividerCount = 0;
} else {
actionsCount = actions.size();
dividerCount = 2;
}
this.full = full;
this.references = references;
this.commCareActivity = activity;
this.observers = new ArrayList<>();
this.mNodeFactory = factory;
//TODO: I am a bad person and I should feel bad. This should get encapsulated
//somewhere in the factory as a callback (IE: How to sort/or whether to or something)
mAsyncMode = (factory instanceof AsyncNodeEntityFactory);
//TODO: Maybe we can actually just replace by checking whether the node is ready?
if (!mAsyncMode) {
if (sort.length != 0) {
sort(sort);
}
}
if (android.os.Build.VERSION.SDK_INT >= 14) {
mImageLoader = new CachingAsyncImageLoader(commCareActivity);
} else {
mImageLoader = null;
}
this.usesCaseTiles = detail.usesEntityTileView();
this.mFuzzySearchEnabled = CommCarePreferences.isFuzzySearchEnabled();
setCurrent(new ArrayList<>(full));
}
/**
* Set the current display set for this adapter
*/
void setCurrent(List<Entity<TreeReference>> arrayList) {
current = arrayList;
if (actionsCount > 0) {
dividerPosition = current.size();
}
update();
}
void clearSearch() {
currentSearchTerms = null;
searchQuery = "";
}
public void clearCalloutResponseData() {
isFilteringByCalloutResult = false;
setCurrent(full);
calloutResponseData.clear();
}
private void sort(int[] fields) {
//The reversing here is only relevant if there's only one sort field and we're on it
sort(fields, (currentSort.length == 1 && currentSort[0] == fields[0]) && !reverseSort);
}
private void sort(int[] fields, boolean reverse) {
this.reverseSort = reverse;
currentSort = fields;
java.util.Collections.sort(full, new EntitySorter(detail.getFields(), reverseSort, currentSort, this));
}
@Override
public boolean areAllItemsEnabled() {
return true;
}
@Override
public boolean isEnabled(int position) {
return true;
}
/**
* Includes action, if enabled, as an item.
*/
@Override
public int getCount() {
return getCurrentCountWithActions();
}
public int getFullCount() {
return full.size();
}
public int getCurrentCount() {
return current.size();
}
public int getCurrentCountWithActions() {
return current.size() + actionsCount + dividerCount;
}
@Override
public TreeReference getItem(int position) {
return current.get(position).getElement();
}
@Override
public long getItemId(int position) {
int type = getItemViewType(position);
switch (type) {
case ENTITY_TYPE:
return references.indexOf(current.get(position).getElement());
case ACTION_TYPE:
return dividerPosition + actions.indexOf(getAction(position));
case DIVIDER_TYPE:
return -2;
default:
throw new RuntimeException("Invalid view type");
}
}
private Action getAction(int position) {
int baseActionPosition = dividerPosition + 1;
return actions.get(position - baseActionPosition);
}
@Override
public int getItemViewType(int position) {
if (actionsCount > 0) {
if (position > dividerPosition && position != getCount() - 1) {
return ACTION_TYPE;
} else if (position == dividerPosition || position == getCount() - 1) {
return DIVIDER_TYPE;
}
}
return ENTITY_TYPE;
}
/**
* Note that position gives a unique "row" id, EXCEPT that the header row AND the first content row
* are both assigned position 0 -- this is not an issue for current usage, but it could be in future
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
int type = getItemViewType(position);
switch (type) {
case ENTITY_TYPE:
return getEntityView(position, convertView);
case ACTION_TYPE:
return getActionView(position, (FrameLayout)convertView, parent);
case DIVIDER_TYPE:
return getDividerView((LinearLayout)convertView, parent);
default:
throw new RuntimeException("Invalid view type");
}
}
private View getEntityView(int position, View convertView) {
Entity<TreeReference> entity = current.get(position);
if (usesCaseTiles) {
// if we use a <grid>, setup an AdvancedEntityView
return getTileView(entity, (EntityViewTile)convertView);
} else {
return getListEntityView(entity, (EntityView)convertView, position);
}
}
private View getTileView(Entity<TreeReference> entity, EntityViewTile tile) {
int[] titleColor = AndroidUtil.getThemeColorIDs(commCareActivity, new int[]{R.attr.entity_select_title_text_color});
if (tile == null) {
tile = EntityViewTile.createTileForEntitySelectDisplay(commCareActivity, detail, entity,
currentSearchTerms, mImageLoader, mFuzzySearchEnabled, selectActivityInAwesomeMode);
} else {
tile.setSearchTerms(currentSearchTerms);
tile.addFieldViews(commCareActivity, detail, entity);
}
tile.setTitleTextColor(titleColor[0]);
return tile;
}
private View getListEntityView(Entity<TreeReference> entity, EntityView emv, int position) {
if (emv == null) {
emv = EntityView.buildEntryEntityView(
commCareActivity, detail, entity,
currentSearchTerms, position, mFuzzySearchEnabled,
getCalloutDataForEntity(entity));
} else {
emv.setSearchTerms(currentSearchTerms);
if (detail.getCallout() != null) {
emv.setExtraData(detail.getCallout().getResponseDetailField(), getCalloutDataForEntity(entity));
}
emv.refreshViewsForNewEntity(entity, entity.getElement().equals(selected), position);
}
return emv;
}
private String getCalloutDataForEntity(Entity<TreeReference> entity) {
if (entity.extraKey != null) {
return calloutResponseData.get(entity.extraKey);
} else {
return null;
}
}
private View getActionView(int position, FrameLayout actionCardView, ViewGroup parent) {
if (actionCardView == null) {
actionCardView = (FrameLayout)LayoutInflater.from(parent.getContext()).inflate(R.layout.action_card, parent, false);
}
EntityActionViewUtils.buildActionView(actionCardView,
getAction(position),
commCareActivity);
return actionCardView;
}
private LinearLayout getDividerView(LinearLayout convertView, ViewGroup parent) {
if (convertView == null) {
return (LinearLayout)LayoutInflater.from(parent.getContext()).inflate(R.layout.line_separator, parent, false);
}
convertView.setOnClickListener(null);
convertView.setEnabled(false);
convertView.setFocusable(false);
return convertView;
}
@Override
public int getViewTypeCount() {
return (actionsCount > 0) ? 3 : 1;
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public boolean isEmpty() {
return getCount() > 0;
}
public synchronized void filterByString(String filterRaw) {
if (entityFilterer != null) {
entityFilterer.cancelSearch();
}
// split by whitespace
String[] searchTerms = filterRaw.split("\\s+");
for (int i = 0; i < searchTerms.length; ++i) {
searchTerms[i] = StringUtils.normalize(searchTerms[i]);
}
currentSearchTerms = searchTerms;
searchQuery = filterRaw;
entityFilterer =
new EntityStringFilterer(this, searchTerms, mAsyncMode,
mFuzzySearchEnabled, mNodeFactory, full, commCareActivity);
entityFilterer.start();
}
/**
* Filter entity list to only include entities that have extra keys present
* in the provided mapping. Reorders entities by the key ordering of the
* mapping.
*/
public synchronized void filterByKeyedCalloutData(OrderedHashtable<String, String> keyToExtraDataMapping) {
calloutResponseData = keyToExtraDataMapping;
if (entityFilterer != null) {
entityFilterer.cancelSearch();
}
LinkedHashSet<String> keysToFilterBy = new LinkedHashSet<>();
for (Enumeration en = calloutResponseData.keys(); en.hasMoreElements(); ) {
String key = (String)en.nextElement();
keysToFilterBy.add(key);
}
isFilteringByCalloutResult = true;
entityFilterer =
new EntityKeyFilterer(this, mNodeFactory, full, commCareActivity, keysToFilterBy);
entityFilterer.start();
}
void update() {
for (DataSetObserver o : observers) {
o.onChanged();
}
}
public void sortEntities(int[] keys) {
sort(keys);
}
public int[] getCurrentSort() {
return currentSort;
}
public boolean isCurrentSortReversed() {
return reverseSort;
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
if (!observers.contains(observer)) {
this.observers.add(observer);
}
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
this.observers.remove(observer);
}
public void notifyCurrentlyHighlighted(TreeReference chosen) {
this.selected = chosen;
update();
}
public int getPosition(TreeReference chosen) {
for (int i = 0; i < current.size(); ++i) {
Entity<TreeReference> e = current.get(i);
if (e.getElement().equals(chosen)) {
return i;
}
}
return -1;
}
/**
* Signal that this adapter is dying. If we are doing any asynchronous work,
* we need to stop doing so.
*/
public synchronized void signalKilled() {
if (entityFilterer != null) {
entityFilterer.cancelSearch();
}
}
public String getSearchNotificationText() {
if (isFilteringByCalloutResult) {
return Localization.get("select.callout.search.status", new String[]{
"" + getCurrentCount(),
"" + getFullCount()});
} else {
return Localization.get("select.search.status", new String[]{
"" + getCurrentCount(),
"" + getFullCount(),
searchQuery});
}
}
public String getSearchQuery() {
return searchQuery;
}
public boolean isFilteringByCalloutResult() {
return isFilteringByCalloutResult;
}
public boolean hasCalloutResponseData() {
return !calloutResponseData.isEmpty();
}
public void loadCalloutDataFromSession() {
OrderedHashtable<String, String> externalData =
(OrderedHashtable<String, String>)CommCareApplication.instance()
.getCurrentSession()
.getCurrentFrameStepExtra(SessionInstanceBuilder.KEY_ENTITY_LIST_EXTRA_DATA);
if (externalData != null) {
filterByKeyedCalloutData(externalData);
}
}
public void saveCalloutDataToSession() {
if (isFilteringByCalloutResult) {
CommCareApplication.instance().getCurrentSession().addExtraToCurrentFrameStep(SessionInstanceBuilder.KEY_ENTITY_LIST_EXTRA_DATA, calloutResponseData);
}
}
@Override
public void notifyBadfilter(String[] args) {
CommCareApplication.notificationManager().reportNotificationMessage(NotificationMessageFactory.message(NotificationMessageFactory.StockMessages.Bad_Case_Filter, args));
}
}