/* * @copyright 2011 Philip Warner * @license GNU General Public License * * This file is part of Book Catalogue. * * Book Catalogue is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Book Catalogue is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Book Catalogue. If not, see <http://www.gnu.org/licenses/>. */ package com.eleybourn.bookcatalogue; import java.lang.ref.WeakReference; import java.text.ParseException; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import android.app.Activity; import android.content.SharedPreferences; import android.database.Cursor; import android.os.Bundle; import android.text.Editable; import android.text.Html; import android.text.TextWatcher; import android.view.MotionEvent; import android.view.View; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ImageView; import android.widget.RatingBar; import android.widget.Spinner; import android.widget.TextView; import com.actionbarsherlock.app.SherlockFragment; import com.eleybourn.bookcatalogue.datamanager.DataManager; import com.eleybourn.bookcatalogue.datamanager.ValidatorException; import com.eleybourn.bookcatalogue.debug.Tracker; import com.eleybourn.bookcatalogue.utils.Logger; import com.eleybourn.bookcatalogue.utils.Utils; /** * This is the class that manages data and views for an activity; access to the data that * each view represents should be handled via this class (and its related classes) where * possible. * <p> * Features is provides are: * <ul> * <li> handling of visibility via preferences * <li> handling of 'group' visibility via the 'group' property of a field. * <li> understanding of kinds of views (setting a checkbox value to 'true' will work as * expected as will setting the value of a Spinner). As new view types are added, it * will be necessary to add new FieldAccessor implementations. * <li> Custom data accessors and formatters to provide application-specific data rules. * <li> validation: calling validate will call user-defined or predefined validation routines and * return success or failure. The text of any exceptions will be available after the call. * <li> simplified loading of data from a cursor. * <li> simplified extraction of data to a ContentValues collection. * </ul> * <p> * Formatters and Accessors * <p> * It is up to each accessor to decide what to do with any formatters defined for a field. * The fields themselves have extract() and format() methods that will apply the formatter * functions (if present) or just pass the value through. * <p> * On a set(), the accessor should call format() function then apply the value * <p> * On a get() the accessor should retrieve the value and apply the extract() function. * <p> * The use of a formatter typically results in all values being converted to strings so * they should be avoided for most non-string data. * <p> * Data Flow * <p> * Data flows to and from a view as follows: * IN (with formatter): (Cursor or other source) -> format() (via accessor) -> transform (in accessor) -> View * IN ( no formatter ): (Cursor or other source) -> transform (in accessor) -> View * OUT (with formatter): (Cursor or other source) -> transform (in accessor) -> extract (via accessor) -> validator -> (ContentValues or Object) * OUT ( no formatter ): (Cursor or other source) -> transform (in accessor) -> validator -> (ContentValues or Object) * <p> * Usage Note: * <p> * 1. Which Views to Add? * <p> * It is not necessary to add every control to the 'Fields' collection, but as a general rule * any control that displays data from a database, or related derived data, or labels for such * data should be added. * <p> * Typical controls NOT added, are 'Save' and 'Cancel' buttons, or other controls whose * interactions are purely functional. * <p> * 2. Handlers? * <p> * The add() method of Fields returns a new Field object which exposes the 'view' member; this * can be used to perform view-specific tasks like setting onClick() handlers. * * TODO: Rationalize the use of this collection with the DataManager. * * @author Philip Warner * */ public class Fields extends ArrayList<Fields.Field> { // Used for date parsing static java.text.SimpleDateFormat mDateSqlSdf = new java.text.SimpleDateFormat("yyyy-MM-dd"); static java.text.DateFormat mDateDispSdf = java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM); // Java likes this public static final long serialVersionUID = 1L; // The activity and preferences related to this object. private FieldsContext mContext = null; SharedPreferences mPrefs = null; public interface AfterFieldChangeListener { void afterFieldChange(Field field, String newValue); } private AfterFieldChangeListener mAfterFieldChangeListener = null; private interface FieldsContext { Object dbgGetOwnerContext(); View findViewById(int id); } private class ActivityContext implements FieldsContext { private final WeakReference<Activity> mActivity; public ActivityContext(Activity a) { mActivity = new WeakReference<Activity>(a); } @Override public Object dbgGetOwnerContext() { return mActivity.get(); } @Override public View findViewById(int id) { return mActivity.get().findViewById(id); } } private class FragmentContext implements FieldsContext { private final WeakReference<SherlockFragment> mFragment; public FragmentContext(SherlockFragment f) { mFragment = new WeakReference<SherlockFragment>(f); } @Override public Object dbgGetOwnerContext() { return mFragment.get(); } @Override public View findViewById(int id) { if (mFragment.get() == null) { System.out.println("Fragment is NULL"); return null; } View v = mFragment.get().getView(); if (v == null) { System.out.println("View is NULL"); return null; } return v.findViewById(id); } } /** * Constructor * * @param a The parent activity which contains all Views this object * will manage. */ Fields(android.app.Activity a) { super(); mContext = new ActivityContext(a); mPrefs = a.getSharedPreferences("bookCatalogue", android.content.Context.MODE_PRIVATE); } /** * Constructor * * @param a The parent fragment which contains all Views this object * will manage. */ Fields(SherlockFragment f) { super(); mContext = new FragmentContext(f); mPrefs = f.getActivity().getSharedPreferences("bookCatalogue", android.content.Context.MODE_PRIVATE); } /** * Set the listener for field changes * * @param listener * @return */ public AfterFieldChangeListener setAfterFieldChangeListener(AfterFieldChangeListener listener) { AfterFieldChangeListener old = mAfterFieldChangeListener; mAfterFieldChangeListener = listener; return old; } /** * Utility routine to parse a date. Parses YYYY-MM-DD and DD-MMM-YYYY format. * Could be generalized even further if desired by supporting more formats. * * @param s String to parse * @return Parsed date * * @throws ParseException If parse failed. */ static Date parseDate(String s) throws ParseException { Date d; try { // Parse as SQL/ANSI date d = mDateSqlSdf.parse(s); } catch (Exception e) { try { d = mDateDispSdf.parse(s); } catch (Exception e1) { java.text.DateFormat df = java.text.DateFormat.getDateInstance(java.text.DateFormat.SHORT); d = df.parse(s); } } return d; } // The last validator exception caught by this object private ArrayList<ValidatorException> mValidationExceptions = new ArrayList<ValidatorException>(); // A list of cross-validators to apply if all fields pass simple validation. private ArrayList<FieldCrossValidator> mCrossValidators = new ArrayList<FieldCrossValidator>(); /** * Interface for view-specific accessors. One of these will be implemented for each view type that * is supported. * * @author Philip Warner * */ public interface FieldDataAccessor { /** * Passed a Field and a Cursor get the column from the cursor and set the view value. * * @param field Field which defines the View details * @param c Cursor with data to load. */ void set(Field field, Cursor c); /** * Passed a Field and a Cursor get the column from the cursor and set the view value. * * @param field Field which defines the View details * @param b Bundle with data to load. */ void set(Field field, Bundle b); /** * Passed a Field and a DataManager get the column from the data manager and set the view value. * * @param field Field which defines the View details * @param b Bundle with data to load. */ void set(Field field, DataManager data); /** * Passed a Field and a String, use the string to set the view value. * * @param field Field which defines the View details * @param s Source string for value to set. */ void set(Field field, String s); /** * Get the the value from the view associated with Field and store a native version * in the passed values collection. * * @param field Field associated with the View object * @param values Collection to save value. */ void get(Field field, Bundle values); /** * Get the the value from the view associated with Field and store a native version * in the passed DataManager. * * @param field Field associated with the View object * @param values Collection to save value. */ void get(Field field, DataManager values); /** * Get the the value from the view associated with Field and return it as am Object. * * @param field Field associated with the View object * @return The most natural value to associate with the View value. */ Object get(Field field); } /** * Implementation that stores and retrieves data from a string variable. * Only used when a Field fails to find a layout. * * @author Philip Warner * */ static public class StringDataAccessor implements FieldDataAccessor { private String mLocalValue = ""; public void set(Field field, Cursor c) { set(field, c.getString(c.getColumnIndex(field.column))); } public void set(Field field, Bundle b) { set(field, b.getString(field.column)); } public void set(Field field, DataManager data) { set(field, data.getString(field.column)); } public void set(Field field, String s) { mLocalValue = field.format(s); } public void get(Field field, Bundle values) { values.putString(field.column, field.extract(mLocalValue)); } @Override public void get(Field field, DataManager values) { values.putString(field.column, field.extract(mLocalValue)); } public Object get(Field field) { return field.extract(mLocalValue); } } /** * Implementation that stores and retrieves data from a TextView. * This is treated differently to an EditText in that HTML is * displayed properly. * * @author Philip Warner * */ static public class TextViewAccessor implements FieldDataAccessor { private boolean mFormatHtml; private String mRawValue; public TextViewAccessor(boolean formatHtml) { mFormatHtml = formatHtml; } public void set(Field field, Cursor c) { set(field, c.getString(c.getColumnIndex(field.column))); } public void set(Field field, Bundle b) { set(field, b.getString(field.column)); } public void set(Field field, DataManager data) { set(field, data.getString(field.column)); } public void set(Field field, String s) { mRawValue = s; TextView v = (TextView) field.getView(); // Allow for the (apparent) possibility that the view may have been removed due to a tab change or similar. // See Issue 505. if (v == null) { // Log the error. Not much more we can do. String msg = "NULL View: col=" + field.column + ", id=" + field.id + ", group=" + field.group; Fields fs = field.getFields(); if (fs == null) { msg += ". Fields is NULL."; } else { msg += ". Fields is valid."; FieldsContext ctx = fs.getContext(); if (ctx == null) { msg += ". Context is NULL."; } else { msg += ". Context is " + ctx.getClass().getSimpleName() + "."; Object o = ctx.dbgGetOwnerContext(); if (o == null) { msg += ". Owner is NULL."; } else { msg += ". Owner is " + o.getClass().getSimpleName() + " (" + o.toString() + ")"; } } } Tracker.handleEvent(this, msg, Tracker.States.Running); // This should NEVER happen, but it does. So we need more info about why & when. throw new RuntimeException("Unable to get associated View object"); } else { if (mFormatHtml && s != null) { v.setText(Html.fromHtml(field.format(s))); v.setFocusable(false); v.setTextColor(BookCatalogueApp.context.getResources().getColor(android.R.color.primary_text_dark_nodisable)); } else { v.setText(field.format(s)); } } } public void get(Field field, Bundle values) { //TextView v = (TextView) field.getView(); //values.putString(field.column, field.extract(v.getText().toString())); values.putString(field.column, mRawValue); } @Override public void get(Field field, DataManager values) { values.putString(field.column, mRawValue); } public Object get(Field field) { return mRawValue; //return field.extract(((TextView) field.getView()).getText().toString()); } /** * Set the TextViewAccessor to support HTML. */ public void setShowHtml(boolean showHtml) { mFormatHtml = showHtml; } } /** * Implementation that stores and retrieves data from an EditText. * Just uses for defined formatter and setText() and getText(). * * @author Philip Warner * */ static public class EditTextAccessor implements FieldDataAccessor { private boolean mIsSetting = false; public void set(Field field, Cursor c) { set(field, c.getString(c.getColumnIndex(field.column))); } public void set(Field field, Bundle b) { set(field, b.getString(field.column)); } public void set(Field field, DataManager data) { set(field, data.getString(field.column)); } public void set(Field field, String s) { synchronized(this) { if (mIsSetting) return; // Avoid recursion now we watch text mIsSetting = true; } try { TextView v = (TextView) field.getView(); // // Every field MUST have an associated View object, but sometimes it is not found. // When not found, the app crashes. // // The following code is to help diagnose these cases, not avoid them. // // NOTE: This does NOT entirly fix the problem, it gathers debug info. but // we have implemented one work-around // // Work-around #1: // // It seems that sometimes the afterTextChanged() event fires after the text field // is removed from the screen. In this case, there is no need to synchronize the values // since the view is gone. // if (v == null) { String msg = "NULL View: col=" + field.column + ", id=" + field.id + ", group=" + field.group; Fields fs = field.getFields(); if (fs == null) { msg += ". Fields is NULL."; } else { msg += ". Fields is valid."; FieldsContext ctx = fs.getContext(); if (ctx == null) { msg += ". Context is NULL."; } else { msg += ". Context is " + ctx.getClass().getSimpleName() + "."; Object o = ctx.dbgGetOwnerContext(); if (o == null) { msg += ". Owner is NULL."; } else { msg += ". Owner is " + o.getClass().getSimpleName() + " (" + o.toString() + ")"; } } } Tracker.handleEvent(this, msg, Tracker.States.Running); Logger.logError(new RuntimeException("Unable to get associated View object")); } // If the view is still present, make sure it is accurate. if (v != null) { String newVal = field.format(s); // Despite assurances otherwise, getText() apparently returns null sometimes String oldVal = v.getText() == null ? null : v.getText().toString(); if (newVal == null && oldVal == null) return; if (newVal != null && oldVal != null && newVal.equals(oldVal)) return; v.setText(newVal); } } finally { mIsSetting = false; } } public void get(Field field, Bundle values) { TextView v = (TextView) field.getView(); values.putString(field.column, field.extract(v.getText().toString())); } @Override public void get(Field field, DataManager values) { try { TextView v = (TextView) field.getView(); if (v == null) { throw new RuntimeException("No view for field " + field.column); } if (v.getText() == null) { throw new RuntimeException("Text is NULL for field " + field.column); } values.putString(field.column, field.extract(v.getText().toString())); } catch (Exception e) { throw new RuntimeException("Unable to save data", e); } } public Object get(Field field) { return field.extract(((TextView) field.getView()).getText().toString()); } } /** * CheckBox accessor. Attempt to convert data to/from a boolean. * * @author Philip Warner * */ static public class CheckBoxAccessor implements FieldDataAccessor { public void set(Field field, Cursor c) { set(field, c.getString(c.getColumnIndex(field.column))); } public void set(Field field, Bundle b) { set(field, b.getString(field.column)); } public void set(Field field, DataManager data) { set(field, data.getString(field.column)); } public void set(Field field, String s) { CheckBox v = (CheckBox) field.getView(); if (s != null) { try { s = field.format(s); v.setChecked(Utils.stringToBoolean(s, true)); } catch (Exception e) { v.setChecked(false); } } else { v.setChecked(false); } } public void get(Field field, Bundle values) { CheckBox v = (CheckBox) field.getView(); if (field.formatter != null) values.putString(field.column, field.extract(v.isChecked() ? "1" : "0")); else values.putBoolean(field.column, v.isChecked()); } @Override public void get(Field field, DataManager values) { CheckBox v = (CheckBox) field.getView(); if (field.formatter != null) values.putString(field.column, field.extract(v.isChecked() ? "1" : "0")); else values.putBoolean(field.column, v.isChecked()); } public Object get(Field field) { if (field.formatter != null) return field.formatter.extract(field, (((CheckBox)field.getView()).isChecked() ? "1" : "0")); else return (Integer)(((CheckBox)field.getView()).isChecked() ? 1 : 0); } } /** * RatingBar accessor. Attempt to convert data to/from a Float. * * @author Philip Warner * */ static public class RatingBarAccessor implements FieldDataAccessor { public void set(Field field, Cursor c) { RatingBar v = (RatingBar) field.getView(); if (field.formatter != null) v.setRating(Float.parseFloat(field.formatter.format(field, c.getString(c.getColumnIndex(field.column))))); else v.setRating(c.getFloat(c.getColumnIndex(field.column))); } public void set(Field field, Bundle b) { set(field, b.getString(field.column)); } public void set(Field field, DataManager data) { set(field, data.getString(field.column)); } public void set(Field field, String s) { RatingBar v = (RatingBar) field.getView(); Float f = 0.0f; try { s = field.format(s); f = Float.parseFloat(s); } catch (Exception e) { } v.setRating(f); } public void get(Field field, Bundle values) { RatingBar v = (RatingBar) field.getView(); if (field.formatter != null) values.putString(field.column, field.extract("" + v.getRating())); else values.putFloat(field.column, v.getRating()); } public void get(Field field, DataManager values) { RatingBar v = (RatingBar) field.getView(); if (field.formatter != null) values.putString(field.column, field.extract("" + v.getRating())); else values.putFloat(field.column, v.getRating()); } public Object get(Field field) { RatingBar v = (RatingBar) field.getView(); return v.getRating(); } } /** * Spinner accessor. Assumes the Spinner contains a list of Strings and * sets the spinner to the matching item. * * @author Philip Warner * */ static public class SpinnerAccessor implements FieldDataAccessor { public void set(Field field, Cursor c) { set(field, c.getString(c.getColumnIndex(field.column))); } public void set(Field field, Bundle b) { set(field, b.getString(field.column)); } public void set(Field field, DataManager data) { set(field, data.getString(field.column)); } public void set(Field field, String s) { s = field.format(s); Spinner v = (Spinner) field.getView(); if (v == null) return; for(int i=0; i < v.getCount(); i++) { if (v.getItemAtPosition(i).equals(s)) { v.setSelection(i); return; } } } public void get(Field field, Bundle values) { String value; Spinner v = (Spinner) field.getView(); if (v == null) value = ""; else { Object selItem = v.getSelectedItem(); if (selItem != null) value = selItem.toString(); else value = ""; } values.putString(field.column, value); } public void get(Field field, DataManager values) { String value; Spinner v = (Spinner) field.getView(); if (v == null) value = ""; else { Object selItem = v.getSelectedItem(); if (selItem != null) value = selItem.toString(); else value = ""; } values.putString(field.column, value); } public Object get(Field field) { String value; Spinner v = (Spinner) field.getView(); if (v == null) value = ""; else { Object selItem = v.getSelectedItem(); if (selItem != null) value = selItem.toString(); else value = ""; } return field.extract(value); } } /** * Interface for all field-level validators. Each field validator is called twice; once * with the crossValidating flag set to false, then, if all validations were successful, * they are all called a second time with the flag set to true. This is an alternate * method of applying cross-validation. * * @author Philip Warner */ public interface FieldValidator { /** * Validation method. Must throw a ValidatorException if validation fails. * * @param fields The Fields object containing the Field being validated * @param field The Field to validate * @param values A ContentValues collection to store the validated value. * On a cross-validation pass this collection will have all * field values set and can be read. * @param crossValidating Flag indicating if this is the cross-validation pass. * * @throws ValidatorException For any validation failure. */ void validate(Fields fields, Field field, Bundle values, boolean crossValidating); } /** * Interface for all cross-validators; these are applied after all field-level validators * have succeeded. * * @author Philip Warner * */ public interface FieldCrossValidator { /** * * @param fields The Fields object containing the Field being validated * @param values A Bundle collection with all validated field values. */ void validate(Fields fields, Bundle values); } /** * Interface definition for Field formatters. * * @author Philip Warner * */ public interface FieldFormatter { /** // Format a string for applying to a View * * @param source Input value * @return The formatted value */ abstract String format(Field f, String source); /** * Extract a formatted string from the display version * * @param source The value to be back-translated * @return The extracted value */ abstract String extract(Field f, String source); } /** * Formatter for date fields. On failure just return the raw string. * * @author Philip Warner * */ static public class DateFieldFormatter implements FieldFormatter { /** * Display as a human-friendly date */ public String format(Field f, String source) { try { java.util.Date d = parseDate(source); return mDateDispSdf.format(d); } catch (Exception e) { return source; } } /** * Extract as an SQL date. */ public String extract(Field f, String source) { try { java.util.Date d = parseDate(source); return mDateSqlSdf.format(d); } catch (Exception e) { return source; } } } /** * Field definition contains all information and methods necessary to manage display and * extraction of data in a view. * * @author Philip Warner * */ public class Field { /** Owning collction */ WeakReference<Fields> mFields; /** Layout ID */ public int id; /** database column name (can be blank) */ public String column; /** Visibility group name. Used in conjunction with preferences to show/hide Views */ public String group; /** FieldFormatter to use (can be null) */ public FieldFormatter formatter = null; /** Validator to use (can be null) */ public FieldValidator validator; /** Has the field been set to invisible **/ public boolean visible; /** Flag indicating that even though field has a column name, it should NOT be fetched from a * Cursor. This is usually done for synthetic fields needed when saving the data */ public boolean doNoFetch = false; /** Accessor to use (automatically defined) */ private FieldDataAccessor mAccessor = null; /** Optional field-specific tag object */ private Object mTag = null; ///** Property used to determine if edits have been made. // * // * Set to true in case the view is clicked // * // * This a good and simple metric to identify if a field was changed despite not being 100% accurate // * */ //private boolean mWasClicked = false; /** * Constructor. * * @param fields Parent object * @param fieldId Layout ID * @param sourceColumn Source database column. Can be empty. * @param visibilityGroupName Visibility group. Can be blank. * @param fieldValidator Validator. Can be null. * @param fieldFormatter Formatter. Can be null. */ Field(Fields fields, int fieldId, String sourceColumn, String visibilityGroupName, FieldValidator fieldValidator, FieldFormatter fieldFormatter) { mFields = new WeakReference<Fields>(fields); id = fieldId; column = sourceColumn; group = visibilityGroupName; formatter = fieldFormatter; validator = fieldValidator; /* * Load the layout from the passed Activity based on the ID and set visibility and accessor. */ FieldsContext c = fields.getContext(); if (c == null) return; // Lookup the view final View view = c.findViewById(id); // Set the appropriate accessor if (view == null) { mAccessor = new StringDataAccessor(); } else { if (view instanceof Spinner) { mAccessor = new SpinnerAccessor(); } else if (view instanceof CheckBox) { mAccessor = new CheckBoxAccessor(); addTouchSignalsDirty(view); } else if (view instanceof EditText) { mAccessor = new EditTextAccessor(); EditText et = (EditText) view; et.addTextChangedListener(new TextWatcher() { @Override public void afterTextChanged(Editable arg0) { Field.this.setValue(arg0.toString()); } @Override public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {} @Override public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {}} ); } else if (view instanceof Button) { mAccessor = new TextViewAccessor(false); } else if (view instanceof TextView) { mAccessor = new TextViewAccessor(false); } else if (view instanceof ImageView) { mAccessor = new TextViewAccessor(false); } else if (view instanceof RatingBar) { mAccessor = new RatingBarAccessor(); addTouchSignalsDirty(view); } else { throw new IllegalArgumentException(); } visible = fields.getPreferences().getBoolean(FieldVisibility.prefix + group, true); if (!visible) { view.setVisibility(View.GONE); } } } public Field setFormatter(FieldFormatter formatter) { this.formatter = formatter; return this; } /** * If a text field, set the TextViewAccessor to support HTML. * Call this before loading the field. */ public Field setShowHtml(boolean showHtml) { if (mAccessor instanceof TextViewAccessor) { ((TextViewAccessor)mAccessor).setShowHtml(showHtml); } return this; } /** * Reset one fields visibility based on user preferences */ private void resetVisibility(FieldsContext c) { if (c == null) return; // Lookup the view final View view = c.findViewById(id); if (view != null) { visible = BookCatalogueApp.getAppPreferences().getBoolean(FieldVisibility.prefix + group, true); if (visible) { view.setVisibility(View.VISIBLE); } else { view.setVisibility(View.GONE); } } } /** * Add on onTouch listener that signals a 'dirty' event when touched. * * @param view The view to watch */ private void addTouchSignalsDirty(View view) { // Touching this is considered a change // We need to introduce a better way to handle this. view.setOnTouchListener(new View.OnTouchListener(){ @Override public boolean onTouch(View v, MotionEvent event) { if (MotionEvent.ACTION_UP == event.getAction()) { if (mAfterFieldChangeListener != null) { mAfterFieldChangeListener.afterFieldChange(Field.this, null); } } return false; } }); } /** * Accessor; added for debugging only. Try not to use! * @return */ protected Fields getFields() { if (mFields == null) { return null; } else { return mFields.get(); } } /** * Get the view associated with this Field, if available. * @param id View ID. * @return Resulting View, or null. */ View getView() { Fields fs = mFields.get(); if (fs == null) { System.out.println("Fields is NULL"); return null; } FieldsContext c = fs.getContext(); if (c == null) { System.out.println("Context is NULL"); return null; } return c.findViewById(this.id); } /** * Return the current value of the tag field. * @return Current value of tag. */ public Object getTag() { return mTag; } /** * Set the current value of the tag field. * @return Current value of tag. */ public void setTag(Object tag) { mTag = tag; } /** * Return the current value of this field. * @return Current value in native form. */ public Object getValue() { return mAccessor.get(this); } /** * Get the current value of this field and put into the Bundle collection. * @return Current value in native form. */ public void getValue(Bundle values) { mAccessor.get(this, values); } /** * Get the current value of this field and put into the Bundle collection. * @return Current value in native form. */ public void getValue(DataManager data) { mAccessor.get(this, data); } /** * Set the value to the passed string value. * * @param s New value */ public void setValue(String s) { mAccessor.set(this, s); if (mAfterFieldChangeListener != null) { mAfterFieldChangeListener.afterFieldChange(this, s); } } /** * Utility function to call the formatters format() method if present, or just return the raw value. * * @param s String to format * @return Formatted value */ public String format(String s) { if (formatter == null) return s; return formatter.format(this, s); } /** * Utility function to call the formatters extract() method if present, or just return the raw value. * * @param s * @return */ public String extract(String s) { if (formatter == null) return s; return formatter.extract(this, s); } /** * Set the value of this field from the passed cursor. Useful for getting access to * raw data values from the database. * * @param c */ public void set(Cursor c) { if (column.length() > 0 && !doNoFetch) { try { mAccessor.set(this, c); } catch (android.database.CursorIndexOutOfBoundsException e) { throw new RuntimeException("Column '" + this.column + "' not found in cursor",e); } } } /** * Set the value of this field from the passed Bundle. Useful for getting access to * raw data values from a saved data bundle. * * @param c */ public void set(Bundle b) { if (column.length() > 0 && !doNoFetch) { try { mAccessor.set(this, b); } catch (android.database.CursorIndexOutOfBoundsException e) { throw new RuntimeException("Column '" + this.column + "' not found in cursor",e); } } } /** * Set the value of this field from the passed Bundle. Useful for getting access to * raw data values from a saved data bundle. * * @param c */ public void set(DataManager data) { if (column.length() > 0 && !doNoFetch) { try { mAccessor.set(this, data); } catch (android.database.CursorIndexOutOfBoundsException e) { throw new RuntimeException("Column '" + this.column + "' not found in data",e); } } } //public boolean isEdited(){ // return mWasClicked; //} } /** * Accessor for related Activity * * @return Activity for this collection. */ private FieldsContext getContext() { return mContext; } /** * Accessor for related Preferences * * @return SharedPreferences for this collection. */ public SharedPreferences getPreferences() { return mPrefs; } /** * Provides access to the underlying arrays get() method. * * @param index * @return */ public Field getItem(int index) { return super.get(index); } /** * Add a field to this collection * * @param fieldId Layout ID * @param sourceColumn Source DB column (can be blank) * @param fieldValidator Field Validator (can be null) * * @return The resulting Field. */ public Field add(int fieldId, String sourceColumn, FieldValidator fieldValidator) { return add(fieldId, sourceColumn, sourceColumn, fieldValidator, null); } /** * Add a field to this collection * * @param fieldId Layout ID * @param sourceColumn Source DB column (can be blank) * @param fieldValidator Field Validator (can be null) * @param formatter Formatter to use * * @return The resulting Field. */ public Field add(int fieldId, String sourceColumn, FieldValidator fieldValidator, FieldFormatter formatter) { return add(fieldId, sourceColumn, sourceColumn, fieldValidator, formatter); } /** * Add a field to this collection * * @param fieldId Layout ID * @param sourceColumn Source DB column (can be blank) * @param visibilityGroup Group name to determine visibility. * @param fieldValidator Field Validator (can be null) * * @return The resulting Field. */ public Field add(int fieldId, String sourceColumn, String visibilityGroup, FieldValidator fieldValidator) { return add(fieldId, sourceColumn, visibilityGroup, fieldValidator, null); } /** * Add a field to this collection * * @param fieldId Layout ID * @param sourceColumn Source DB column (can be blank) * @param visibilityGroup Group name to determine visibility. * @param fieldValidator Field Validator (can be null) * @param formatter Formatter to use * * @return The resulting Field. */ public Field add(int fieldId, String sourceColumn, String visibilityGroup, FieldValidator fieldValidator, FieldFormatter formatter) { Field fe = new Field(this, fieldId, sourceColumn, visibilityGroup, fieldValidator, formatter); this.add(fe); return fe; } /** * Return the Field associated with the passed layout ID * * @return Associated Field. */ public Field getField(int id) { Iterator<Field> iter = this.iterator(); while (iter.hasNext()) { Field f = iter.next(); if (f.id == id) return f; } throw new IllegalArgumentException(); } /** * Convenience function: For an AutoCompleteTextView, set the adapter * * @param fieldId Layout ID of View * @param adapter Adapter to use */ public void setAdapter(int fieldId, ArrayAdapter<String> adapter) { Field f = getField(fieldId); TextView tv = (TextView)f.getView(); if (tv instanceof AutoCompleteTextView) ((AutoCompleteTextView)tv).setAdapter(adapter); } /** * For a View that supports onClick() (all of them?), set the listener. * * @param id Layout ID * @param listener onClick() listener. */ void setListener(int id, View.OnClickListener listener) { Field f = getField(id); View v = f.getView(); if (v != null) { f.getView().setOnClickListener(listener); } else { v = f.getView(); throw new RuntimeException("Unable to find view for field id " + id); } } /** * Load all fields from the passed cursor * * @param c Cursor to load Field objects from. */ public void setAll(Cursor c) { Iterator<Field> fi = this.iterator(); while(fi.hasNext()) { Field fe = fi.next(); fe.set(c); } } /** * Load all fields from the passed cursor * * @param c Cursor to load Field objects from. */ public void setAll(Bundle b) { Iterator<Field> fi = this.iterator(); while(fi.hasNext()) { Field fe = fi.next(); fe.set(b); } } /** * Load all fields from the passed datamanager * * @param c Cursor to load Field objects from. */ public void setAll(DataManager data) { Iterator<Field> fi = this.iterator(); while(fi.hasNext()) { Field fe = fi.next(); fe.set(data); } } /** * Save all fields to the passed DataManager (ie. 'get' them *into* the DataManager). * * @param c Cursor to load Field objects from. */ public void getAll(DataManager data) { Iterator<Field> fi = this.iterator(); while(fi.hasNext()) { Field fe = fi.next(); if (fe.column != null && !fe.column.equals("")) { fe.getValue(data); } } } public void getAll(Bundle b) { Iterator<Field> fi = this.iterator(); while(fi.hasNext()) { Field fe = fi.next(); if (fe.column != null && !fe.column.equals("")) { fe.getValue(b); } } } /** * Internal utility routine to perform one loop validating all fields. * * @param values The Bundle to fill in/use. * @param crossValidating Flag indicating if this is a cross validation pass. */ private boolean doValidate(Bundle values, boolean crossValidating) { Iterator<Field> fi = this.iterator(); boolean isOk = true; while(fi.hasNext()) { Field fe = fi.next(); if (fe.validator != null) { try { fe.validator.validate(this,fe, values, crossValidating); } catch(ValidatorException e) { mValidationExceptions.add(e); isOk = false; // Always save the value...even if invalid. Or at least try to. if (!crossValidating) try { values.putString(fe.column, fe.getValue().toString()); } catch (Exception e2) {}; } } else { if (!fe.column.equals("") && values != null) fe.getValue(values); } } return isOk; } /** * Reset all field visibility based on user preferences */ public void resetVisibility() { FieldsContext c = this.getContext(); Iterator<Field> fi = this.iterator(); while(fi.hasNext()) { Field fe = fi.next(); fe.resetVisibility(c); } } /** * Loop through and apply validators, generating a Bundle collection as a by-product. * The Bundle collection is then used in cross-validation as a second pass, and finally * passed to each defined cross-validator. * * @param values The Bundle collection to fill * * @return boolean True if all validation passed. */ public boolean validate(Bundle values) { if (values == null) throw new NullPointerException(); boolean isOk = true; mValidationExceptions.clear(); // First, just validate individual fields with the cross-val flag set false if (!doValidate(values, false)) isOk = false; // Now re-run with cross-val set to true. if (!doValidate(values, true)) isOk = false; // Finally run the local cross-validation Iterator<FieldCrossValidator> i = mCrossValidators.iterator(); while (i.hasNext()) { FieldCrossValidator v = i.next(); try { v.validate(this,values); } catch(ValidatorException e) { mValidationExceptions.add(e); isOk = false; } } return isOk; } /** * Retrieve the text message associated with the last validation exception t occur. * * @return res The resource manager to use when looking up strings. */ public String getValidationExceptionMessage(android.content.res.Resources res) { if (mValidationExceptions.size() == 0) return "No error"; else { String message = ""; Iterator<ValidatorException> i = mValidationExceptions.iterator(); int cnt = 1; if (i.hasNext()) message = "(" + cnt + ") " + i.next().getFormattedMessage(res); while (i.hasNext()) { cnt ++; message += " (" + cnt + ") " + i.next().getFormattedMessage(res) + "\n"; } return message; } } /** * Append a cross-field validator to the collection. These will be applied after * the field-specific validators have all passed. * * @param v An instance of FieldCrossValidator to append */ public void addCrossValidator(FieldCrossValidator v) { mCrossValidators.add(v); } ///** // * Check if any field has been modified // * // * @return true if a field has been edited (or clicked) // */ //public boolean isEdited(){ // // for (Field field : this){ // if (field.isEdited()){ // return true; // } // } // // return false; //} }