Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3bbc8875 authored by Marten Gajda's avatar Marten Gajda
Browse files

switch to XmlObjects for pull parsing

update XmlModel to support checklist
add a fallback to provide the old behavior (show description & checklist
when only a description field is present)
parent 7f791335
Loading
Loading
Loading
Loading
+193 −123
Original line number Diff line number Diff line
@@ -28,6 +28,14 @@ import org.dmfs.tasks.model.adapters.FieldAdapter;
import org.dmfs.tasks.model.adapters.StringFieldAdapter;
import org.dmfs.tasks.model.contraints.UpdateAllDay;
import org.dmfs.tasks.model.layout.LayoutDescriptor;
import org.dmfs.xmlobjects.ElementDescriptor;
import org.dmfs.xmlobjects.QualifiedName;
import org.dmfs.xmlobjects.builder.AbstractObjectBuilder;
import org.dmfs.xmlobjects.pull.ParserContext;
import org.dmfs.xmlobjects.pull.Recyclable;
import org.dmfs.xmlobjects.pull.XmlObjectPull;
import org.dmfs.xmlobjects.pull.XmlObjectPullParserException;
import org.dmfs.xmlobjects.pull.XmlPath;

import android.content.Context;
import android.content.pm.PackageInfo;
@@ -59,149 +67,230 @@ import android.util.Log;
 */
public class XmlModel extends Model
{

	private final static String TAG = "org.dmfs.tasks.model.XmlModel";

	public final static String METADATA_TASKS = "org.dmfs.tasks.TASKS";

	public final static String NAMESPACE = "org.dmfs.tasks";

	private final static Map<String, FieldInflater> FIELD_INFLATER_MAP = new HashMap<String, FieldInflater>();
	public final static QualifiedName ATTR_KIND = QualifiedName.get("kind");

	private final PackageManager mPackageManager;
	private final String mPackageName;
	private final Context mModelContext;
	private final Context mContext;
	private boolean mInflated = false;
	/**
	 * This is a workaround for the transition from a combined description/checklist field to two separate fields.
	 * 
	 * TODO: remove once the new versions of CalDAV-Sync and SmoothSync are in use
	 */
	public final static QualifiedName ATTR_HIDECHECKLIST = QualifiedName.get("hideCheckList");

	private final static Map<String, FieldInflater> FIELD_INFLATER_MAP = new HashMap<String, FieldInflater>();

	public XmlModel(Context context, String packageName) throws ModelInflaterException
	{
		mContext = context;
		mPackageName = packageName;
		mPackageManager = context.getPackageManager();
		try
		{
			mModelContext = context.createPackageContext(packageName, 0);
		}
		catch (NameNotFoundException e)
	/**
	 * POJO that stores the attributes of a &lt;datakind> element.
	 */
	private static class DataKind implements Recyclable
	{
			throw new ModelInflaterException("No model definition found for package " + mPackageName);
		}
		public String datakind;
		public int titleId = -1;
		public int hintId = -1;

	}
		/**
		 * This is a workaround for the transition from a combined description/checklist field to two separate fields.
		 * 
		 * TODO: remove once the new versions of CalDAV-Sync and SmoothSync are in use
		 */
		public boolean hideCheckList = false;


	@SuppressWarnings("unchecked")
		@Override
	public void inflate() throws ModelInflaterException
		public void recycle()
		{
		if (mInflated)
			datakind = null;
			titleId = -1;
			hintId = -1;
			hideCheckList = false;
		}
	}

	/**
	 * POJO to store the state of the model parser.
	 */
	private static class ModelParserState
	{
			return;
		public boolean hasDue = false;
		public boolean hasStart = false;
		public FieldDescriptor alldayDescriptor = null;
	}

		XmlResourceParser parser = getParser();
	private static final ElementDescriptor<XmlModel> XML_MODEL_DESCRIPTOR = ElementDescriptor.register(QualifiedName.get(NAMESPACE, "TaskSource"),
		new AbstractObjectBuilder<XmlModel>()
		{
			public XmlModel get(ElementDescriptor<XmlModel> descriptor, XmlModel recycle, ParserContext context) throws XmlObjectPullParserException
			{
				// ensure we have a state object
				context.setState(new ModelParserState());

		if (parser == null)
				if (recycle == null)
				{
			throw new ModelInflaterException("No model definition found for package " + mPackageName);
					throw new IllegalArgumentException("you must provide the XML model to populate as the object to recycle");
				}
				return recycle;
			};

		try

			public XmlModel update(ElementDescriptor<XmlModel> descriptor, XmlModel object, QualifiedName attribute, String value, ParserContext context)
				throws XmlObjectPullParserException
			{
			// add a field for the list
			mFields.add(new FieldDescriptor(mContext, R.string.task_list, null, new StringFieldAdapter(Tasks.LIST_NAME)).setViewLayout(new LayoutDescriptor(
				R.layout.text_field_view_nodivider_large).setOption(LayoutDescriptor.OPTION_NO_TITLE, true).setOption(
				LayoutDescriptor.OPTION_USE_TASK_LIST_BACKGROUND_COLOR, true)));
			mFields.add(new FieldDescriptor(mContext, R.string.task_list, null, new StringFieldAdapter(Tasks.ACCOUNT_NAME)).setViewLayout(new LayoutDescriptor(
				R.layout.text_field_view_nodivider_small).setOption(LayoutDescriptor.OPTION_NO_TITLE, true).setOption(
				LayoutDescriptor.OPTION_USE_TASK_LIST_BACKGROUND_COLOR, true)));
				// for now we ignore all attributes
				return object;
			};

			int eventType;

			// find first tag
			do
			@SuppressWarnings("unchecked")
			public <V extends Object> XmlModel update(ElementDescriptor<XmlModel> descriptor, XmlModel object, ElementDescriptor<V> childDescriptor, V child,
				ParserContext context) throws XmlObjectPullParserException
			{
				if (childDescriptor == XML_DATAKIND)
				{
				eventType = parser.next();
			} while (eventType != XmlResourceParser.END_DOCUMENT && eventType != XmlResourceParser.START_TAG);
					DataKind datakind = (DataKind) child;
					FieldInflater inflater = FIELD_INFLATER_MAP.get(datakind.datakind);

			if (!"TaskSource".equals(parser.getName()) || !NAMESPACE.equals(parser.getNamespace()))
					if (inflater != null)
					{
				throw new ModelInflaterException("Invalid model definition in " + mPackageName + ": root node must be 'TaskSource'");
			}
						FieldDescriptor fieldDescriptor = inflater.inflate(object.mContext, object.mModelContext, datakind);
						object.mFields.add(fieldDescriptor);

			setAllowRecurrence(parser.getAttributeBooleanValue(null, "allowRecurrence", false));
			setAllowExceptions(parser.getAttributeBooleanValue(null, "allowExceptions", false));
						ModelParserState state = (ModelParserState) context.getState();

			if (parser.getAttributeIntValue(null, "iconId", -1) != -1)
						if ("allday".equals(datakind.datakind))
						{
				setIconId(parser.getAttributeIntValue(null, "iconId", -1));
							state.alldayDescriptor = fieldDescriptor;
						}
			if (parser.getAttributeIntValue(null, "labelId", -1) != -1)
						else if ("due".equals(datakind.datakind))
						{
				setLabelId(parser.getAttributeIntValue(null, "labelId", -1));
							state.hasDue = true;
						}
						else if ("dtstart".equals(datakind.datakind))
						{
							state.hasStart = true;
						}
						else if ("description".equals(datakind.datakind) && !datakind.hideCheckList)
						{
							Log.i(TAG, "found old description data kind, adding checklist");
							object.mFields.add(FIELD_INFLATER_MAP.get("checklist").inflate(object.mContext, object.mModelContext, datakind));
						}
					}
					// we don't need the datakind object anymore, so recycle it
					context.recycle((ElementDescriptor<DataKind>) childDescriptor, datakind);

			int depth = 1;

			boolean hasDue = false;
			boolean hasStart = false;
				}
				return object;
			};

			FieldDescriptor alldayDescriptor = null;

			eventType = parser.next();
			while (eventType != XmlResourceParser.END_DOCUMENT)
			{
				if (eventType == XmlResourceParser.START_TAG)
			@SuppressWarnings("unchecked")
			public XmlModel finish(ElementDescriptor<XmlModel> descriptor, XmlModel object, ParserContext context) throws XmlObjectPullParserException
			{
					depth++;
					Log.v(TAG, "'" + parser.getName() + "'   " + depth);
					if (depth == 2 && "datakind".equals(parser.getName()) && NAMESPACE.equals(parser.getNamespace()))
				ModelParserState state = (ModelParserState) context.getState();
				if (state.alldayDescriptor != null)
				{
						// TODO: let inflateField step forward till the end tag
						FieldDescriptor descriptor = inflateField(parser);
						mFields.add(descriptor);

						FieldAdapter<?> fa = descriptor.getFieldAdapter();
						if (fa instanceof BooleanFieldAdapter && Tasks.IS_ALLDAY.equals(((BooleanFieldAdapter) fa).getFieldName()))
					// add UpdateAllDay constraint of due or start fields are missing to keep the values in sync with the allday flag
					if (!state.hasDue)
					{
							alldayDescriptor = descriptor;
						((FieldAdapter<Boolean>) state.alldayDescriptor.getFieldAdapter()).addContraint(new UpdateAllDay(TaskFieldAdapters.DUE));
					}
						else if (fa == TaskFieldAdapters.DUE)
					if (!state.hasStart)
					{
							hasDue = true;
						((FieldAdapter<Boolean>) state.alldayDescriptor.getFieldAdapter()).addContraint(new UpdateAllDay(TaskFieldAdapters.DTSTART));
					}
						else if (fa == TaskFieldAdapters.DTSTART)
				}
				return object;
			};
		});

	private final static ElementDescriptor<DataKind> XML_DATAKIND = ElementDescriptor.register(QualifiedName.get(NAMESPACE, "datakind"),
		new AbstractObjectBuilder<DataKind>()
		{
			public DataKind get(ElementDescriptor<DataKind> descriptor, DataKind recycle, ParserContext context) throws XmlObjectPullParserException
			{
							hasStart = true;
				if (recycle != null)
				{
					recycle.recycle();
					return recycle;
				}

				return new DataKind();
			};


			public DataKind update(ElementDescriptor<DataKind> descriptor, DataKind object, QualifiedName attribute, String value, ParserContext context)
				throws XmlObjectPullParserException
			{
				if (attribute == ATTR_KIND)
				{
					object.datakind = value;
				}
				else if (attribute == ATTR_HIDECHECKLIST)
				{
					object.hideCheckList = Boolean.parseBoolean(value);
				}
				else if (eventType == XmlResourceParser.END_TAG)
				return object;
			};
		});

	private final PackageManager mPackageManager;
	private final String mPackageName;
	private final Context mModelContext;
	private final Context mContext;
	private boolean mInflated = false;


	public XmlModel(Context context, String packageName) throws ModelInflaterException
	{
		mContext = context;
		mPackageName = packageName;
		mPackageManager = context.getPackageManager();
		try
		{
					depth--;
			mModelContext = context.createPackageContext(packageName, 0);
		}
				else
		catch (NameNotFoundException e)
		{
					throw new ModelInflaterException("Invalid tag " + parser.getName() + " " + mPackageName);
			throw new ModelInflaterException("No model definition found for package " + mPackageName);
		}

				eventType = parser.next();
	}

			if (alldayDescriptor != null)

	@SuppressWarnings("unchecked")
	@Override
	public void inflate() throws ModelInflaterException
	{
				if (!hasDue)
		if (mInflated)
		{
					// no due date descriptor present, ensure we update keep the all-day flag in sync
					((FieldAdapter<Boolean>) alldayDescriptor.getFieldAdapter()).addContraint(new UpdateAllDay(TaskFieldAdapters.DUE));
			return;
		}

				if (!hasStart)
		XmlResourceParser parser = getParser();

		if (parser == null)
		{
					// no start date descriptor present, ensure we update keep the all-day flag in sync
					((FieldAdapter<Boolean>) alldayDescriptor.getFieldAdapter()).addContraint(new UpdateAllDay(TaskFieldAdapters.DTSTART));
			throw new ModelInflaterException("No model definition found for package " + mPackageName);
		}

		try
		{
			// add a field for the list
			mFields.add(new FieldDescriptor(mContext, R.string.task_list, null, new StringFieldAdapter(Tasks.LIST_NAME)).setViewLayout(new LayoutDescriptor(
				R.layout.text_field_view_nodivider_large).setOption(LayoutDescriptor.OPTION_NO_TITLE, true).setOption(
				LayoutDescriptor.OPTION_USE_TASK_LIST_BACKGROUND_COLOR, true)));
			mFields.add(new FieldDescriptor(mContext, R.string.task_list, null, new StringFieldAdapter(Tasks.ACCOUNT_NAME)).setViewLayout(new LayoutDescriptor(
				R.layout.text_field_view_nodivider_small).setOption(LayoutDescriptor.OPTION_NO_TITLE, true).setOption(
				LayoutDescriptor.OPTION_USE_TASK_LIST_BACKGROUND_COLOR, true)));

			XmlObjectPull pullParser = new XmlObjectPull(parser);
			if (pullParser.pull(XML_MODEL_DESCRIPTOR, this, new XmlPath()) == null)
			{
				throw new ModelInflaterException("Invalid model definition in " + mPackageName + ": root node must be 'TaskSource'");
			}

		}
@@ -237,29 +326,8 @@ public class XmlModel extends Model
		return null;
	}


	/**
	 * Inflate the current field.
	 * 
	 * @param parser
	 *            A parser that points to a datakind.
	 * @return A {@link FieldDescriptor} for the field.
	 * @throws IllegalDataKindException
	 */
	private FieldDescriptor inflateField(XmlResourceParser parser) throws IllegalDataKindException
	{
		String kind = parser.getAttributeValue(null, "kind");
		Log.v(TAG, "inflating kind " + kind);
		FieldInflater inflater = FIELD_INFLATER_MAP.get(kind);
		if (inflater == null)
		{
			throw new IllegalDataKindException("invalid data kind " + kind);
		}
		return inflater.inflate(mContext, mModelContext, parser);
	}

	/**
	 * Basic field inflater. It does some default inflating.
	 * Basic field inflater. It does some default inflating, but also allows customization.
	 * 
	 * @author Marten Gajda <marten@dmfs.org>
	 */
@@ -282,32 +350,32 @@ public class XmlModel extends Model
		}


		public FieldDescriptor inflate(Context context, Context modelContext, XmlResourceParser parser)
		public FieldDescriptor inflate(Context context, Context modelContext, DataKind kind)
		{
			int titleId = parser.getAttributeResourceValue(null, "title_id", -1);
			int titleId = kind.titleId;
			FieldDescriptor descriptor;
			if (titleId != -1)
			{
				descriptor = new FieldDescriptor(modelContext, titleId, getContentType(), getFieldAdapter());
				descriptor = new FieldDescriptor(modelContext, titleId, getContentType(), getFieldAdapter(kind));
			}
			else
			{
				descriptor = new FieldDescriptor(context, getDefaultTitleId(), getContentType(), getFieldAdapter());
				descriptor = new FieldDescriptor(context, getDefaultTitleId(), getContentType(), getFieldAdapter(kind));
			}
			customizeDescriptor(context, modelContext, descriptor, parser);
			customizeDescriptor(context, modelContext, descriptor, kind);
			return descriptor;
		}


		public FieldAdapter<?> getFieldAdapter()
		public FieldAdapter<?> getFieldAdapter(DataKind kind)
		{
			return mAdapter;
		}


		void customizeDescriptor(Context context, Context modelContext, FieldDescriptor descriptor, XmlResourceParser parser)
		void customizeDescriptor(Context context, Context modelContext, FieldDescriptor descriptor, DataKind kind)
		{
			int hintId = parser.getAttributeResourceValue(null, "hint_id", -1);
			int hintId = kind.hintId;
			if (hintId != -1)
			{
				descriptor.setHint(modelContext.getString(hintId));
@@ -386,6 +454,8 @@ public class XmlModel extends Model
			R.layout.text_field_editor));
		FIELD_INFLATER_MAP.put("description", new FieldInflater(TaskFieldAdapters.DESCRIPTION, R.string.task_description, R.layout.text_field_view,
			R.layout.text_field_editor));
		FIELD_INFLATER_MAP.put("checklist", new FieldInflater(TaskFieldAdapters.CHECKLIST, R.string.task_checklist, R.layout.checklist_field_view,
			R.layout.checklist_field_editor));

		FIELD_INFLATER_MAP.put("dtstart", new FieldInflater(TaskFieldAdapters.DTSTART, R.string.task_start, R.layout.time_field_view,
			R.layout.time_field_editor));
@@ -400,9 +470,9 @@ public class XmlModel extends Model
			R.layout.choices_field_editor)
		{
			@Override
			void customizeDescriptor(Context context, Context modelContext, FieldDescriptor descriptor, XmlResourceParser parser)
			void customizeDescriptor(Context context, Context modelContext, FieldDescriptor descriptor, DataKind kind)
			{
				super.customizeDescriptor(context, modelContext, descriptor, parser);
				super.customizeDescriptor(context, modelContext, descriptor, kind);
				ArrayChoicesAdapter aca = new ArrayChoicesAdapter();
				aca.addHiddenChoice(null, context.getString(R.string.status_needs_action), null);
				aca.addChoice(Tasks.STATUS_NEEDS_ACTION, context.getString(R.string.status_needs_action), null);
@@ -416,9 +486,9 @@ public class XmlModel extends Model
			R.layout.choices_field_editor)
		{
			@Override
			void customizeDescriptor(Context context, Context modelContext, FieldDescriptor descriptor, XmlResourceParser parser)
			void customizeDescriptor(Context context, Context modelContext, FieldDescriptor descriptor, DataKind kind)
			{
				super.customizeDescriptor(context, modelContext, descriptor, parser);
				super.customizeDescriptor(context, modelContext, descriptor, kind);

				ArrayChoicesAdapter aca = new ArrayChoicesAdapter();
				aca.addChoice(null, context.getString(R.string.priority_undefined), null);
@@ -439,9 +509,9 @@ public class XmlModel extends Model
			R.layout.choices_field_editor)
		{
			@Override
			void customizeDescriptor(Context context, Context modelContext, FieldDescriptor descriptor, XmlResourceParser parser)
			void customizeDescriptor(Context context, Context modelContext, FieldDescriptor descriptor, DataKind kind)
			{
				super.customizeDescriptor(context, modelContext, descriptor, parser);
				super.customizeDescriptor(context, modelContext, descriptor, kind);

				ArrayChoicesAdapter aca = new ArrayChoicesAdapter();
				aca.addChoice(null, context.getString(R.string.classification_not_specified), null);
@@ -457,7 +527,7 @@ public class XmlModel extends Model
		FIELD_INFLATER_MAP.put("allday", new FieldInflater(null, R.string.task_all_day, -1, R.layout.boolean_field_editor)
		{
			@Override
			public FieldAdapter<?> getFieldAdapter()
			public FieldAdapter<?> getFieldAdapter(DataKind kind)
			{
				// return a non-static field adapter because we modify it
				return new BooleanFieldAdapter(Tasks.IS_ALLDAY);
@@ -467,9 +537,9 @@ public class XmlModel extends Model
		FIELD_INFLATER_MAP.put("timezone", new FieldInflater(TaskFieldAdapters.TIMEZONE, R.string.task_timezone, -1, R.layout.choices_field_editor)
		{
			@Override
			void customizeDescriptor(Context context, Context modelContext, FieldDescriptor descriptor, XmlResourceParser parser)
			void customizeDescriptor(Context context, Context modelContext, FieldDescriptor descriptor, DataKind kind)
			{
				super.customizeDescriptor(context, modelContext, descriptor, parser);
				super.customizeDescriptor(context, modelContext, descriptor, kind);
				TimeZoneChoicesAdapter tzaca = new TimeZoneChoicesAdapter(context);
				descriptor.setChoices(tzaca);
			}
+1 −9
Original line number Diff line number Diff line
@@ -43,8 +43,7 @@ import android.widget.EditText;


/**
 * Editor widget for checklists. It allows to switch between regular plain text and checklist mode. In checklist mode every line will be prepended by a check
 * box.
 * Editor widget for check lists.
 * 
 * @author Marten Gajda <marten@dmfs.org>
 */
@@ -193,13 +192,6 @@ public class CheckListFieldEditor extends AbstractFieldEditor implements OnCheck
			mText.setTextColor(getTextColorFromBackground(customBackgroud));
		}

		if (list == null || list.size() == 0)
		{
			setVisibility(GONE);
			return;
		}
		setVisibility(VISIBLE);

		mBuilding = true;

		int count = 0;