package org.commcare.dalvik.activities; import java.util.ArrayList; import java.util.List; import java.util.Vector; import org.commcare.android.adapters.EntityListAdapter; import org.commcare.android.framework.CommCareActivity; import org.commcare.android.models.AndroidSessionWrapper; import org.commcare.android.models.Entity; import org.commcare.android.models.NodeEntityFactory; import org.commcare.android.tasks.EntityLoaderListener; import org.commcare.android.tasks.EntityLoaderTask; import org.commcare.android.util.CommCareInstanceInitializer; import org.commcare.android.util.SessionUnavailableException; import org.commcare.android.view.EntityView; import org.commcare.android.view.TabbedDetailView; import org.commcare.android.view.ViewUtil; import org.commcare.dalvik.R; import org.commcare.dalvik.application.CommCareApplication; import org.commcare.suite.model.Action; import org.commcare.suite.model.Detail; import org.commcare.suite.model.DetailField; import org.commcare.suite.model.Entry; import org.commcare.suite.model.SessionDatum; import org.commcare.util.CommCareSession; import org.commcare.util.SessionFrame; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.services.locale.Localization; import org.javarosa.model.xform.XPathReference; import org.javarosa.xpath.expr.XPathEqExpr; import org.javarosa.xpath.expr.XPathExpression; import org.javarosa.xpath.expr.XPathStringLiteral; import android.app.Activity; import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.res.Configuration; import android.os.Bundle; import android.speech.tts.TextToSpeech; import android.text.Editable; import android.text.TextWatcher; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; /** * * TODO: Lots of locking and state-based cleanup * * @author ctsims * */ public class EntitySelectActivity extends CommCareActivity implements TextWatcher, EntityLoaderListener, OnItemClickListener, TextToSpeech.OnInitListener { private CommCareSession session; private AndroidSessionWrapper asw; public static final String EXTRA_ENTITY_KEY = "esa_entity_key"; public static final String EXTRA_IS_MAP = "is_map"; private static final int CONFIRM_SELECT = 0; private static final int BARCODE_FETCH = 1; private static final int MAP_SELECT = 2; private static final int MENU_SORT = Menu.FIRST; private static final int MENU_MAP = Menu.FIRST + 1; private static final int MENU_ACTION = Menu.FIRST + 2; EditText searchbox; TextView searchResultStatus; EntityListAdapter adapter; Entry prototype; LinearLayout header; ImageButton barcodeButton; TextToSpeech tts; SessionDatum selectDatum; EvaluationContext entityContext; boolean mResultIsMap = false; boolean mMappingEnabled = false; boolean mViewMode = false; boolean mNoDetailMode = false; private EntityLoaderTask loader; private boolean inAwesomeMode = false; FrameLayout rightFrame; TabbedDetailView detailView; Intent selectedIntent = null; String filterString = ""; private Detail shortSelect; /* * (non-Javadoc) * @see org.commcare.android.framework.CommCareActivity#onCreate(android.os.Bundle) */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EntitySelectActivity oldActivity = (EntitySelectActivity)this.getDestroyedActivityState(); if(savedInstanceState != null) { mResultIsMap = savedInstanceState.getBoolean(EXTRA_IS_MAP, false); } try { asw = CommCareApplication._().getCurrentSessionWrapper(); session = asw.getSession(); } catch(SessionUnavailableException sue){ //The user isn't logged in! bounce this back to where we came from this.setResult(Activity.RESULT_CANCELED); this.finish(); return; } selectDatum = session.getNeededDatum(); shortSelect = session.getDetail(selectDatum.getShortDetail()); mNoDetailMode = selectDatum.getLongDetail() == null; if(this.getString(R.string.panes).equals("two") && !mNoDetailMode) { //See if we're on a big 'ol screen. if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { //If we're in landscape mode, we can display this with the awesome UI. //Inflate and set up the normal view for now. setContentView(R.layout.screen_compound_select); View.inflate(this, R.layout.entity_select_layout, (ViewGroup)findViewById(R.id.screen_compound_select_left_pane)); inAwesomeMode = true; rightFrame = (FrameLayout)findViewById(R.id.screen_compound_select_right_pane); TextView message = (TextView)findViewById(R.id.screen_compound_select_prompt); message.setText(Localization.get("select.placeholder.message", new String[] {Localization.get("cchq.case")})); } else { setContentView(R.layout.entity_select_layout); //So we're not in landscape mode anymore, but were before. If we had something selected, we //need to go to the detail screen instead. if(oldActivity != null) { if(oldActivity.selectedIntent != null) { startActivityForResult(oldActivity.selectedIntent, CONFIRM_SELECT); startOther = true; } } } } else { setContentView(R.layout.entity_select_layout); } ((ListView)this.findViewById(R.id.screen_entity_select_list)).setOnItemClickListener(this); TextView searchLabel = (TextView)findViewById(R.id.screen_entity_select_search_label); searchLabel.setText(Localization.get("select.search.label")); searchLabel.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { // get the focus on the edittext by performing click searchbox.performClick(); // then force the keyboard up since performClick() apparently isn't enough on some devices InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); // only will trigger it if no physical keyboard is open inputMethodManager.showSoftInput(searchbox, InputMethodManager.SHOW_IMPLICIT); } }); searchbox = (EditText)findViewById(R.id.searchbox); searchbox.setMaxLines(3); searchbox.setHorizontallyScrolling(false); searchResultStatus = (TextView) findViewById(R.id.no_search_results); header = (LinearLayout)findViewById(R.id.entity_select_header); barcodeButton = (ImageButton)findViewById(R.id.barcodeButton); Vector<Entry> entries = session.getEntriesForCommand(session.getCommand()); prototype = entries.elementAt(0); //(We shouldn't need the "" here, but we're avoiding making changes to commcare core for release issues) if(entries.size() == 1 && (prototype.getXFormNamespace() == null || prototype.getXFormNamespace().equals(""))) { mViewMode = true; } barcodeButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { Intent i = new Intent("com.google.zxing.client.android.SCAN"); try { startActivityForResult(i, BARCODE_FETCH); } catch(ActivityNotFoundException anfe) { Toast noReader = Toast.makeText(EntitySelectActivity.this, "No barcode reader available! You can install one from the android market.", Toast.LENGTH_LONG); noReader.show(); } } }); searchbox.addTextChangedListener(this); searchbox.requestFocus(); if(oldActivity != null) { adapter = oldActivity.adapter; //not sure how this happens, but seem plausible. if(adapter != null) { adapter.setController(this); ((ListView)this.findViewById(R.id.screen_entity_select_list)).setAdapter(adapter); findViewById(R.id.entity_select_loading).setVisibility(View.GONE); } } //cts: disabling for non-demo purposes //tts = new TextToSpeech(this, this); } /* * (non-Javadoc) * @see org.commcare.android.framework.CommCareActivity#isTopNavEnabled() */ @Override protected boolean isTopNavEnabled() { return true; } /* * (non-Javadoc) * @see org.commcare.android.framework.CommCareActivity#getActivityTitle() */ @Override public String getActivityTitle() { //Skipping this until it's a more general pattern // String title = Localization.get("select.list.title"); // // try { // Detail detail = session.getDetail(selectDatum.getShortDetail()); // title = detail.getTitle().evaluate(); // } catch(Exception e) { // // } // // return title; return null; } boolean resuming = false; boolean startOther = false; public void onResume() { super.onResume(); //Don't go through making the whole thing if we're finishing anyway. if(this.isFinishing() || startOther) {return;} if(!resuming && !mNoDetailMode && this.getIntent().hasExtra(EXTRA_ENTITY_KEY)) { TreeReference entity = getEntityFromID(this.getIntent().getStringExtra(EXTRA_ENTITY_KEY)); if(entity != null) { if(inAwesomeMode) { if (adapter != null) { displayReferenceAwesome(entity, adapter.getPosition(entity)); adapter.setAwesomeMode(true); updateSelectedItem(entity, true); } } else { //Once we've done the initial dispatch, we don't want to end up triggering it later. this.getIntent().removeExtra(EXTRA_ENTITY_KEY); Intent i = getDetailIntent(entity, null); if (adapter != null) { i.putExtra("entity_detail_index", adapter.getPosition(entity)); } startActivityForResult(i, CONFIRM_SELECT); return; } } } refreshView(); } /** * Get form list from database and insert into view. */ private void refreshView() { try { //TODO: Get ec into these text's String[] headers = new String[shortSelect.getFields().length]; for(int i = 0 ; i < headers.length ; ++i) { headers[i] = shortSelect.getFields()[i].getHeader().evaluate(); if("address".equals(shortSelect.getFields()[i].getTemplateForm())) { this.mMappingEnabled = true; } } //Hm, sadly we possibly need to rebuild this each time. EntityView v = new EntityView(this, shortSelect, headers); header.removeAllViews(); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); v.setBackgroundResource(R.drawable.blue_tabbed_box); // only add headers if we're not using grid mode if(!shortSelect.usesGridView()){ header.addView(v,params); } if(adapter == null && loader == null && !EntityLoaderTask.attachToActivity(this)) { EntityLoaderTask theloader = new EntityLoaderTask(shortSelect, asw.getEvaluationContext()); theloader.attachListener(this); theloader.execute(selectDatum.getNodeset()); } } catch(SessionUnavailableException sue) { //TODO: login and return } } protected Intent getDetailIntent(TreeReference contextRef, Intent i) { //Parse out the return value first, and stick it in the appropriate intent so it'll get passed along when //we return if (i == null) { i = new Intent(getApplicationContext(), EntityDetailActivity.class); } TreeReference valueRef = XPathReference.getPathExpr(selectDatum.getValue()).getReference(true); AbstractTreeElement element = asw.getEvaluationContext().resolveReference(valueRef.contextualize(contextRef)); String value = ""; if(element != null && element.getValue() != null) { value = element.getValue().uncast().getString(); } //See if we even have a long datum if(selectDatum.getLongDetail() != null) { //If so, add this. otherwise that'll be the queue to just return i.putExtra(EntityDetailActivity.DETAIL_ID, selectDatum.getLongDetail()); } i.putExtra(SessionFrame.STATE_DATUM_VAL, value); CommCareApplication._().serializeToIntent(i, EntityDetailActivity.CONTEXT_REFERENCE, contextRef); return i; } /** * NOT GUARANTEED TO WORK! May return an entity if one exists * * @param uniqueid * @return */ protected TreeReference getEntityFromID(String uniqueid) { //The uniqueid here is the value selected, so we can in theory track down the value we're looking for. //Get root nodeset TreeReference nodesetRef = selectDatum.getNodeset().clone(); Vector<XPathExpression> predicates = nodesetRef.getPredicate(nodesetRef.size() -1); predicates.add(new XPathEqExpr(true, XPathReference.getPathExpr(selectDatum.getValue()), new XPathStringLiteral(uniqueid))); nodesetRef.addPredicate(nodesetRef.size() - 1, predicates); Vector<TreeReference> elements = asw.getEvaluationContext().expandReference(nodesetRef); if(elements.size() == 1) { return elements.firstElement(); } else if(elements.size() > 1) { //Lots of nodes. Can't really choose one yet. return null; } else { return null; } } /* * (non-Javadoc) * @see android.widget.AdapterView.OnItemClickListener#onItemClick(android.widget.AdapterView, android.view.View, int, long) */ @Override public void onItemClick(AdapterView<?> listView, View view, int position, long id) { if(id == EntityListAdapter.SPECIAL_ACTION) { triggerDetailAction(); return; } TreeReference selection = adapter.getItem(position); if(inAwesomeMode) { displayReferenceAwesome(selection, position); updateSelectedItem(selection, false); } else { Intent i = getDetailIntent(selection, null); i.putExtra("entity_detail_index", position); if (mNoDetailMode) { returnWithResult(i); } else { startActivityForResult(i, CONFIRM_SELECT); } } } /* * (non-Javadoc) * @see android.app.Activity#onActivityResult(int, int, android.content.Intent) */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { switch(requestCode){ case BARCODE_FETCH: if(resultCode == Activity.RESULT_OK) { String result = intent.getStringExtra("SCAN_RESULT"); this.searchbox.setText(result); } break; case CONFIRM_SELECT: resuming = true; if(resultCode == RESULT_OK && !mViewMode) { // create intent for return and store path returnWithResult(intent); return; } else { //Did we enter the detail from mapping mode? If so, go back to that if(mResultIsMap) { mResultIsMap = false; Intent i = new Intent(this, EntityMapActivity.class); this.startActivityForResult(i, MAP_SELECT); return; } //Otherwise, if we're in awesome mode, make sure we retain the original selection if(inAwesomeMode) { TreeReference r = CommCareApplication._().deserializeFromIntent(intent, EntityDetailActivity.CONTEXT_REFERENCE, TreeReference.class); if(r != null) { this.displayReferenceAwesome(r, adapter.getPosition(r)); updateSelectedItem(r, true); } releaseCurrentMediaEntity(); } return; } case MAP_SELECT: if(resultCode == RESULT_OK) { TreeReference r = CommCareApplication._().deserializeFromIntent(intent, EntityDetailActivity.CONTEXT_REFERENCE, TreeReference.class); if(inAwesomeMode) { this.displayReferenceAwesome(r, adapter.getPosition(r)); } else { Intent i = this.getDetailIntent(r, null); if(mNoDetailMode) { returnWithResult(i); } else { //To go back to map mode if confirm is false mResultIsMap = true; i.putExtra("entity_detail_index", adapter.getPosition(r)); startActivityForResult(i, CONFIRM_SELECT); } return; } } else { refreshView(); return; } default: super.onActivityResult(requestCode, resultCode, intent); } } private void returnWithResult(Intent intent) { Intent i = new Intent(this.getIntent()); i.putExtras(intent.getExtras()); setResult(RESULT_OK, i); finish(); } public void afterTextChanged(Editable s) { if(searchbox.getText() == s) { filterString = s.toString(); if(adapter != null) { adapter.applyFilter(filterString); String query = searchbox.getText().toString(); if (!"".equals(query)) { searchResultStatus.setText(Localization.get("select.search.status", new String[] { ""+adapter.getCount(), ""+adapter.getFullCount(), query })); searchResultStatus.setVisibility(View.VISIBLE); } else { searchResultStatus.setVisibility(View.GONE); } } } } public void beforeTextChanged(CharSequence s, int start, int count, int after) { // TODO Auto-generated method stub } public void onTextChanged(CharSequence s, int start, int before, int count) { // TODO Auto-generated method stub } /* * (non-Javadoc) * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu) */ @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); menu.add(0, MENU_SORT, MENU_SORT, Localization.get("select.menu.sort")).setIcon( android.R.drawable.ic_menu_sort_alphabetically); if(mMappingEnabled) { menu.add(0, MENU_MAP, MENU_MAP, Localization.get("select.menu.map")).setIcon( android.R.drawable.ic_menu_mapmode); } Action action = shortSelect.getCustomAction(); if(action != null) { ViewUtil.addDisplayToMenu(this, menu, MENU_ACTION, action.getDisplay()); } return true; } /* (non-Javadoc) * @see android.app.Activity#onPrepareOptionsMenu(android.view.Menu) */ @Override public boolean onPrepareOptionsMenu(Menu menu) { //only display the sort menu if we're going to be able to sort //(IE: not until the items have loaded) menu.findItem(MENU_SORT).setEnabled(adapter != null); return super.onPrepareOptionsMenu(menu); } /* * (non-Javadoc) * @see org.commcare.android.framework.CommCareActivity#onOptionsItemSelected(android.view.MenuItem) */ @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MENU_SORT: createSortMenu(); return true; case MENU_MAP: Intent i = new Intent(this, EntityMapActivity.class); this.startActivityForResult(i, MAP_SELECT); return true; case MENU_ACTION: triggerDetailAction(); return true; } return super.onOptionsItemSelected(item); } private void triggerDetailAction() { Action action = shortSelect.getCustomAction(); asw.executeStackActions(action.getStackOperations()); this.setResult(CommCareHomeActivity.RESULT_RESTART); this.finish(); } private void createSortMenu() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(Localization.get("select.menu.sort")); SessionDatum datum = session.getNeededDatum(); DetailField[] fields = session.getDetail(datum.getShortDetail()).getFields(); List<String> namesList = new ArrayList<String>(); final int[] keyarray = new int[fields.length]; int[] sorts = adapter.getCurrentSort(); int currentSort = sorts.length == 1 ? sorts[0] : -1; boolean reversed = adapter.isCurrentSortReversed(); int added = 0; for(int i = 0 ; i < fields.length ; ++i) { String result = fields[i].getHeader().evaluate(); if(!"".equals(result)) { String prepend = ""; if(currentSort == -1) { for(int j = 0 ; j < sorts.length ; ++ j) { if(sorts[j] == i) { prepend = (j+1) + " " + (fields[i].getSortDirection() == DetailField.DIRECTION_DESCENDING ? "(v) " : "(^) "); } } } else if(currentSort == i) { prepend = reversed ^ fields[i].getSortDirection() == DetailField.DIRECTION_DESCENDING ? "(v) " : "(^) "; } namesList.add(prepend + result); keyarray[added] = i; added++; } } final String[] names = namesList.toArray(new String[0]); builder.setItems(names, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { adapter.sortEntities(new int[] { keyarray[item]}); adapter.applyFilter(searchbox.getText().toString()); } }); builder.setOnCancelListener(new OnCancelListener() { public void onCancel(DialogInterface dialog) { // } }); AlertDialog alert = builder.create(); alert.show(); } /* * (non-Javadoc) * @see org.commcare.android.framework.CommCareActivity#onDestroy() */ @Override public void onDestroy() { super.onDestroy(); if(loader != null) { if(isFinishing()) { loader.cancel(false); } else { loader.detachActivity(); } } if (tts != null) { tts.stop(); tts.shutdown(); } } /* * (non-Javadoc) * @see android.speech.tts.TextToSpeech.OnInitListener#onInit(int) */ @Override public void onInit(int status) { if (status == TextToSpeech.SUCCESS) { //using the default speech engine for now. } else { } } /* * (non-Javadoc) * @see org.commcare.android.tasks.EntityLoaderListener#deliverResult(java.util.List, java.util.List) */ @Override public void deliverResult(List<Entity<TreeReference>> entities, List<TreeReference> references) { loader = null; Detail detail = session.getDetail(selectDatum.getShortDetail()); int[] order = detail.getSortOrder(); for(int i = 0 ; i < detail.getFields().length ; ++i) { String header = detail.getFields()[i].getHeader().evaluate(); if(order.length == 0 && !"".equals(header)) { order = new int[] {i}; } } ListView view = ((ListView)this.findViewById(R.id.screen_entity_select_list)); adapter = new EntityListAdapter(EntitySelectActivity.this, detail, references, entities, order, tts, this); view.setAdapter(adapter); findViewById(R.id.entity_select_loading).setVisibility(View.GONE); if(adapter != null) { adapter.applyFilter(filterString); } //In landscape we want to select something now. Either the top item, or the most recently selected one if(inAwesomeMode) { updateSelectedItem(true); } } private void updateSelectedItem(boolean forceMove) { TreeReference chosen = null; if(selectedIntent != null) { chosen = CommCareApplication._().deserializeFromIntent(selectedIntent, EntityDetailActivity.CONTEXT_REFERENCE, TreeReference.class); } updateSelectedItem(chosen, forceMove); } private void updateSelectedItem(TreeReference selected, boolean forceMove) { if(adapter == null) {return;} if(selected != null) { adapter.notifyCurrentlyHighlighted(selected); if(forceMove) { ListView view = ((ListView)this.findViewById(R.id.screen_entity_select_list)); view.setSelection(adapter.getPosition(selected)); } return; } } /* * (non-Javadoc) * @see org.commcare.android.tasks.EntityLoaderListener#attach(org.commcare.android.tasks.EntityLoaderTask) */ @Override public void attach(EntityLoaderTask task) { findViewById(R.id.entity_select_loading).setVisibility(View.VISIBLE); this.loader = task; } public boolean inAwesomeMode(){ return inAwesomeMode; } boolean rightFrameSetup = false; NodeEntityFactory factory; public void displayReferenceAwesome(final TreeReference selection, int detailIndex) { selectedIntent = getDetailIntent(selection, getIntent()); //this should be 100% "fragment" able if(!rightFrameSetup) { findViewById(R.id.screen_compound_select_prompt).setVisibility(View.GONE); View.inflate(this, R.layout.entity_detail, rightFrame); Button next = (Button)findViewById(R.id.entity_select_button); next.setText(Localization.get("select.detail.confirm")); next.setOnClickListener(new OnClickListener() { public void onClick(View v) { // create intent for return and store path Intent i = new Intent(EntitySelectActivity.this.getIntent()); i.putExtra(SessionFrame.STATE_DATUM_VAL, selectedIntent.getStringExtra(SessionFrame.STATE_DATUM_VAL)); setResult(RESULT_OK, i); finish(); return; } }); if(getIntent().getBooleanExtra(EntityDetailActivity.IS_DEAD_END, false)) { next.setText("Done"); } String passedCommand = selectedIntent.getStringExtra(SessionFrame.STATE_COMMAND_ID); Vector<Entry> entries = session.getEntriesForCommand(passedCommand == null ? session.getCommand() : passedCommand); prototype = entries.elementAt(0); detailView = new TabbedDetailView(this); detailView.setRoot((ViewGroup) rightFrame.findViewById(R.id.entity_detail_tabs)); factory = new NodeEntityFactory(session.getDetail(selectedIntent.getStringExtra(EntityDetailActivity.DETAIL_ID)), session.getEvaluationContext(new CommCareInstanceInitializer(session))); Detail detail = factory.getDetail(); detailView.setDetail(detail); if (detail.isCompound()) { // border around right panel doesn't look right when there are tabs rightFrame.setBackgroundDrawable(null); } rightFrameSetup = true; } detailView.refresh(factory.getDetail(), detailIndex, false); } /* * (non-Javadoc) * @see org.commcare.android.tasks.EntityLoaderListener#deliverError(java.lang.Exception) */ @Override public void deliverError(Exception e) { displayException(e); } }