public class Notification extends DAO {
	// These match WeekDaysView's values
	public static final int mon = 0x1;
	public static final int tue = 0x10;
	public static final int wed = 0x100;
	public static final int thu = 0x1000;
	public static final int fri = 0x10000;
	public static final int sat = 0x100000;
	public static final int sun = 0x1000000;

	// Location repeat, one left of sun
	public static final int locationRepeat = 0x10000000;

	// SQL convention says Table name should be "singular"
	public static final String TABLE_NAME = "notification";
	public static final String WITH_TASK_VIEW_NAME = "notification_with_tasks";
	public static final String WITH_TASK_PATH = TABLE_NAME + "/with_task_info";

	public static final String CONTENT_TYPE = "vnd.android.cursor.item/vnd.nononsenseapps."
									+ TABLE_NAME;

	public static final Uri URI = Uri.withAppendedPath(
									Uri.parse(MyContentProvider.SCHEME
																	+ MyContentProvider.AUTHORITY),
									TABLE_NAME);
	public static final Uri URI_WITH_TASK_PATH = Uri.withAppendedPath(
									Uri.parse(MyContentProvider.SCHEME
																	+ MyContentProvider.AUTHORITY),
									WITH_TASK_PATH);

	public static final int BASEURICODE = 301;
	public static final int BASEITEMCODE = 302;
	public static final int WITHTASKQUERYCODE = 303;
	public static final int WITHTASKQUERYITEMCODE = 304;

	public static void addMatcherUris(UriMatcher sURIMatcher) {
		sURIMatcher.addURI(MyContentProvider.AUTHORITY, TABLE_NAME, BASEURICODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY, TABLE_NAME + "/#", BASEITEMCODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY, WITH_TASK_PATH, WITHTASKQUERYCODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY, WITH_TASK_PATH + "/#",
										WITHTASKQUERYITEMCODE);
	}

	public static Uri getUri(final long id) {
		return Uri.withAppendedPath(URI, Long.toString(id));
	}

	public static class Columns implements BaseColumns {

		private Columns() {
		}

		public static final String TIME = "time";
		public static final String PERMANENT = "permanent";
		public static final String TASKID = "taskid";
		public static final String REPEATS = "repeats";
		public static final String LATITUDE = "latitude";
		public static final String LONGITUDE = "longitude";
		public static final String RADIUS = "radius";
		public static final String LOCATIONNAME = "locationname";

		public static final String[] FIELDS = { _ID, TIME, PERMANENT, TASKID, REPEATS,
										LOCATIONNAME, LATITUDE, LONGITUDE, RADIUS };
	}

	public static class ColumnsWithTask extends Columns {

		private ColumnsWithTask() {
		}

		// public static final String notificationPrefix = "n.";
		public static final String taskPrefix = "t_";
		public static final String listPrefix = "l_";

		public static final String[] FIELDS = joinArrays(
										// prefixArray(notificationPrefix,
										// Columns.FIELDS),
										Columns.FIELDS,
										prefixArray(taskPrefix, Task.Columns.SHALLOWFIELDS),
										prefixArray(listPrefix, TaskList.Columns.SHALLOWFIELDS));
	}

	/**
	 * Main table to store notification data
	 */
	public static final String CREATE_TABLE = new StringBuilder("CREATE TABLE ")
									.append(TABLE_NAME)
									.append("(")
									.append(Columns._ID)
									.append(" INTEGER PRIMARY KEY,")
									.append(Columns.TIME)
									.append(" INTEGER,")
									.append(Columns.PERMANENT)
									.append(" INTEGER NOT NULL DEFAULT 0,")
									.append(Columns.TASKID)
									.append(" INTEGER,")
									// Interpreted binary
									.append(Columns.REPEATS)
									.append(" INTEGER NOT NULL DEFAULT 0,")
									// Location data
									.append(Columns.LOCATIONNAME).append(" TEXT,")
									.append(Columns.LATITUDE).append(" REAL, ")
									.append(Columns.LONGITUDE)
									.append(" REAL, ")
									.append(Columns.RADIUS)
									.append(" REAL, ")
									// Foreign key for task
									.append("FOREIGN KEY(").append(Columns.TASKID)
									.append(") REFERENCES ").append(Task.TABLE_NAME).append("(")
									.append(Task.Columns._ID).append(") ON DELETE CASCADE")
									.append(")").toString();

	/**
	 * View that joins relevant data from tasks and lists tables
	 */
	public static final String CREATE_JOINED_VIEW = new StringBuilder()
									.append("CREATE TEMP VIEW IF NOT EXISTS ")
									.append(WITH_TASK_VIEW_NAME)
									.append(" AS ")
									.append(" SELECT ")
									// Notifications as normal column names
									.append(arrayToCommaString(TABLE_NAME + ".", Columns.FIELDS))
									.append(",")
									// Rest gets prefixed
									.append(arrayToCommaString("t.",
																	Task.Columns.SHALLOWFIELDS,
																	" AS "
																									+ ColumnsWithTask.taskPrefix
																									+ "%1$s"))
									.append(",")
									.append(arrayToCommaString("l.",
																	TaskList.Columns.SHALLOWFIELDS,
																	" AS "
																									+ ColumnsWithTask.listPrefix
																									+ "%1$s"))
									.append(" FROM ").append(TABLE_NAME).append(",")
									.append(Task.TABLE_NAME).append(" AS t,")
									.append(TaskList.TABLE_NAME).append(" AS l ").append(" WHERE ")
									.append(TABLE_NAME).append(".").append(Columns.TASKID)
									.append(" = t.").append(Task.Columns._ID).append(" AND t.")
									.append(Task.Columns.DBLIST).append(" = l.")
									.append(TaskList.Columns._ID).append(";").toString();

	// milliseconds since 1970-01-01 UTC
	public Long time = null;
	public boolean permanent = false;

	public Long taskID = null;
	public long repeats = 0;

	public String locationName = null;
	public Double latitude = null;
	public Double longitude = null;
	public Double radius = null;

	// Read only, fetched from VIEW
	public String listTitle = null;
	public Long listID = null;
	public String taskTitle = null;
	public String taskNote = null;

	// Convenience for the editor
	public View view = null;

	/**
	 * Must be associated with a task
	 */
	public Notification(final long taskID) {
		this.taskID = taskID;
	}

	public Notification(final Cursor c) {
		_id = c.getLong(0);
		time = c.isNull(1) ? null : c.getLong(1);
		permanent = 1 == c.getLong(2);
		taskID = c.isNull(3) ? null : c.getLong(3);
		repeats = c.getLong(4);
		locationName = c.isNull(5) ? null : c.getString(5);
		latitude = c.isNull(6) ? null : c.getDouble(6);
		longitude = c.isNull(7) ? null : c.getDouble(7);
		radius = c.isNull(8) ? null : c.getDouble(8);
		// if cursor has more fields, then assume it was constructed with
		// the WITH_TASKS view query
		if (c.getColumnCount() > 9) {
			listTitle = c.getString(c.getColumnIndex(ColumnsWithTask.listPrefix
											+ TaskList.Columns.TITLE));
			listID = c.getLong(c.getColumnIndex(ColumnsWithTask.listPrefix + TaskList.Columns._ID));
			taskTitle = c.getString(c.getColumnIndex(ColumnsWithTask.taskPrefix
											+ Task.Columns.TITLE));
			taskNote = c.getString(c.getColumnIndex(ColumnsWithTask.taskPrefix + Task.Columns.NOTE));
		}
	}

	public Notification(final Uri uri, final ContentValues values) {
		this(Long.parseLong(uri.getLastPathSegment()), values);
	}

	public Notification(final long id, final ContentValues values) {
		this(values);
		_id = id;
	}
	
	public Notification(final JSONObject json) throws JSONException {
		if (json.has(Columns.TIME))
			time = json.getLong(Columns.TIME);
		if (json.has(Columns.PERMANENT))
			permanent = 1 == json.getLong(Columns.PERMANENT);
		if (json.has(Columns.TASKID))
			taskID = json.getLong(Columns.TASKID);
		if (json.has(Columns.REPEATS))
			repeats = json.getLong(Columns.REPEATS);
		if (json.has(Columns.LOCATIONNAME))
			locationName = json.getString(Columns.LOCATIONNAME);
		if (json.has(Columns.LATITUDE))
			latitude = json.getDouble(Columns.LATITUDE);
		if (json.has(Columns.LONGITUDE))
			longitude = json.getDouble(Columns.LONGITUDE);
		if (json.has(Columns.RADIUS))
			radius = json.getDouble(Columns.RADIUS);
	}

	public Notification(final ContentValues values) {
		time = values.getAsLong(Columns.TIME);
		permanent = 1 == values.getAsLong(Columns.PERMANENT);
		taskID = values.getAsLong(Columns.TASKID);
		repeats = values.getAsLong(Columns.REPEATS);
		locationName = values.getAsString(Columns.LOCATIONNAME);
		latitude = values.getAsDouble(Columns.LATITUDE);
		longitude = values.getAsDouble(Columns.LONGITUDE);
		radius = values.getAsDouble(Columns.RADIUS);
	}

	@Override
	public ContentValues getContent() {
		final ContentValues values = new ContentValues();

		values.put(Columns.TIME, time);
		values.put(Columns.TASKID, taskID);
		values.put(Columns.PERMANENT, permanent ? 1 : 0);
		values.put(Columns.REPEATS, repeats);
		values.put(Columns.LOCATIONNAME, locationName);
		values.put(Columns.LATITUDE, latitude);
		values.put(Columns.LONGITUDE, longitude);
		values.put(Columns.RADIUS, radius);

		return values;

	}

	@Override
	protected String getTableName() {
		return TABLE_NAME;
	}

	@Override
	public String getContentType() {
		return CONTENT_TYPE;
	}

	/**
	 * Returns date and time formatted in text in local time zone
	 * 
	 */
	public CharSequence getLocalDateTimeText(final Context context) {
		return TimeFormatter.getLocalDateStringLong(context, time);
	}
	
	/**
	 * Returns time formatted in text in local time zone
	 * 
	 */
	public CharSequence getLocalTimeText(final Context context) {
		return TimeFormatter.getLocalTimeOnlyString(context, time);
	}
	/**
	 * Returns date formatted in text in local time zone
	 * 
	 */
	public CharSequence getLocalDateText(final Context context) {
		return TimeFormatter.getDateFormatter(context).format(new Date(time));
	}

	@Override
	public int save(final Context context) {
		int result = 0;
		if (_id < 1) {
			result += insert(context);
		} else {
			result += context.getContentResolver().update(getUri(), getContent(), null, null);
			if (result < 1) {
				// To allow editor to edit deleted notifications
				result += insert(context);
			}
		}
		return result;
	}

	private int insert(final Context context) {
		int result = 0;
		final Uri uri = context.getContentResolver().insert(getBaseUri(), getContent());
		if (uri != null) {
			_id = Long.parseLong(uri.getLastPathSegment());
			result++;
		}
		return result;
	}

	/**
	 * If true, will also schedule/notify android notifications
	 */
	public int save(final Context context, final boolean schedule) {
		int result = save(context);
		if (schedule) {
			// First cancel any potentially old versions
			NotificationHelper.cancelNotification(context, this);
			// Then reschedule
			NotificationHelper.schedule(context);
		}
		return result;
	}

	@Override
	public int delete(final Context context) {
		// Make sure existing notifications are cancelled.
		NotificationHelper.cancelNotification(context, this);
		return super.delete(context);
	}

	public void saveInBackground(final Context context, final boolean schedule) {
		final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
			@Override
			protected Void doInBackground(Void... voids) {
				save(context, schedule);
				return null;
			}
		};
		task.execute();
	}

	/**
	 * Starts a background task that removes all notifications associated with
	 * the specified tasks.
	 */
	public static void removeWithTaskIds(final Context context, final Long... ids) {
		if (ids.length > 0) {
			final AsyncTask<Long, Void, Void> task = new AsyncTask<Long, Void, Void>() {
				@Override
				protected Void doInBackground(final Long... ids) {
					removeWithTaskIdsSynced(context, ids);
					return null;
				}
			};
			task.execute(ids);
		}
	}

    /**
     * Removes all notifications associated with the specified tasks. Runs in
     * the same thread as the caller.
     * @param context
     * @param ids
     */
    public static void removeWithTaskIdsSynced(final Context context,
            final Long... ids) {
        String idStrings = "(";
        ArrayList<String> idsToClear = new ArrayList<String>();
        for (Long id : ids) {
            idStrings += id + ",";
            idsToClear.add(Long.toString(id));
        }
        idStrings = idStrings.substring(0, idStrings.length() - 1);
        idStrings += ")";

        final Cursor c = context.getContentResolver()
                .query(URI,
                        Columns.FIELDS,
                        Columns.TASKID
                        + " IN "
                        + idStrings,
                        null, null);

        while (c.moveToNext()) {
            // Yes dont just call delete in database
            // We have to remove geofences (in delete)
            Notification n = new Notification(c);
            n.delete(context);
        }
        c.close();
    }

	/**
	 * Delete or reschedule a specific notification.
	 */
	public static void deleteOrReschedule(final Context context, final Uri uri) {
		final Cursor c = context.getContentResolver().query(uri, Columns.FIELDS, null, null, null);

		while (c.moveToNext()) {
			Notification n = new Notification(c);
			n.deleteOrReschedule(context);
		}
		c.close();
	}

	/**
	 * Starts a background task that removes all notifications associated with
	 * the specified tasks up to the specified time.
	 */
	public static void removeWithMaxTimeAndTaskIds(final Context context, final long maxTime,
									final boolean reschedule, final Long... ids) {
		if (ids.length > 0) {
			final AsyncTask<Long, Void, Void> task = new AsyncTask<Long, Void, Void>() {
				@Override
				protected Void doInBackground(final Long... ids) {
					String idStrings = "(";
					for (Long id : ids) {
						idStrings += id + ",";
					}
					idStrings = idStrings.substring(0, idStrings.length() - 1);
					idStrings += ")";

					final Cursor c = context.getContentResolver().query(
													URI,
													Columns.FIELDS,
													Columns.TASKID + " IN " + idStrings + " AND "
																					+ Columns.TIME
																					+ " <= "
																					+ maxTime,
													null, null);

					ArrayList<String> idsToClear = new ArrayList<String>();
					while (c.moveToNext()) {
						Notification n = new Notification(c);
						idsToClear.add(Long.toString(n._id));
						if (reschedule) {
							n.deleteOrReschedule(context);
						} else {
							n.delete(context);
						}
					}
					c.close();

					// context.getContentResolver().delete(URI,
					// Columns.TASKID + " IN " + idStrings +
					// " AND " + Columns.TIME + " <= " + maxTime, null);
					return null;
				}
			};
			task.execute(ids);
		}
	}

	/**
	 * Starts a background task that removes all notifications associated with
	 * the specified list, occurring before the specified time
	 */
	// public static void removeWithListId(final Context context,
	// final long listId, final long maxTime) {
	// final AsyncTask<Long, Void, Void> task = new AsyncTask<Long, Void,
	// Void>() {
	// @Override
	// protected Void doInBackground(final Long... ids) {
	// // First get the list of tasks in that list
	// final Cursor c = context
	// .getContentResolver()
	// .query(Task.URI,
	// Task.Columns.FIELDS,
	// Task.Columns.DBLIST
	// + " IS ? AND "
	// + com.nononsenseapps.notepad.data.model.sql.Notification.Columns.RADIUS
	// + " IS NULL",
	// new String[] { Long.toString(listId) }, null);
	//
	// String idStrings = "(";
	// while (c.moveToNext()) {
	// idStrings += c.getLong(0) + ",";
	// }
	// c.close();
	// idStrings = idStrings.substring(0, idStrings.length() - 1);
	// idStrings += ")";
	//
	// context.getContentResolver().delete(
	// URI,
	// Columns.TIME + " <= " + maxTime + " AND "
	// + Columns.TASKID + " IN " + idStrings, null);
	// return null;
	// }
	// };
	// task.execute(listId);
	// }

	/**
	 * Returns list of notifications coupled to specified task, sorted by time
	 */
	public static List<Notification> getNotificationsOfTask(final Context context, final long taskId) {
		return getNotificationsWithTasks(
										context,
										new StringBuilder().append(Notification.Columns.TASKID)
																		+ " IS ?",
										new String[] { Long.toString(taskId) },
										new StringBuilder().append(Notification.Columns.TIME)
																		.toString());
	}

	/**
	 * Returns a list of notifications occurring after/before specified time,
	 * and which do not have a location (radius == null). Sorted by time
	 * ascending
	 */
	public static List<Notification> getNotificationsWithTime(final Context context,
									final long time, final boolean before) {
		final String comparison = before ? " <= ?" : " > ?";
		return getNotificationsWithTasks(
				context,
				new StringBuilder().append(Notification.Columns.TIME)
						.append(comparison)
						.append(" AND ")
						.append(Notification.Columns.RADIUS)
						.append(" IS NULL")
						.toString(),
				new String[]{Long.toString(time)},
				new StringBuilder().append(Notification.Columns.TIME)
						.toString());
	}

	public static List<Notification> getNotificationsWithTasks(final Context context,
									final String where, final String[] whereArgs,
									final String sortOrder) {
		ArrayList<Notification> list = new ArrayList<Notification>();

		final Cursor c = context.getContentResolver().query(URI_WITH_TASK_PATH, null, where,
										whereArgs, sortOrder);

		if (c != null) {
			while (c.moveToNext()) {
				list.add(new Notification(c));
			}

			c.close();
		}
		return list;
	}

	/**
	 * Used for snooze
	 */
	public static int setTime(final Context context, final Uri uri, final long newTime) {
		final ContentValues values = new ContentValues();
		values.put(Columns.TIME, newTime);
		// Use base ID to bypass type checks
		return context.getContentResolver().update(URI, values, Columns._ID + " IS ?",
										new String[] { uri.getLastPathSegment() });
	}

	/**
	 * Used for snooze
	 */
	public static void setTimeForListAndBefore(final Context context, final long listId,
									final long maxTime, final long newTime) {
		final AsyncTask<Long, Void, Void> task = new AsyncTask<Long, Void, Void>() {
			@Override
			protected Void doInBackground(final Long... ids) {
				// First get the list of tasks in that list
				final Cursor c = context.getContentResolver()
												.query(Task.URI,
																				Task.Columns.FIELDS,
																				Task.Columns.DBLIST
																												+ " IS ? AND "
																												+ Notification.Columns.RADIUS
																												+ " IS NULL",
																				new String[] { Long.toString(listId) },
																				null);

				String idStrings = "(";
				while (c.moveToNext()) {
					idStrings += c.getLong(0) + ",";
				}
				c.close();
				idStrings = idStrings.substring(0, idStrings.length() - 1);
				idStrings += ")";

				final ContentValues values = new ContentValues();
				values.put(Columns.TIME, newTime);

				context.getContentResolver().update(
												URI,
												values,
												Columns.TIME + " <= " + maxTime + " AND "
																				+ Columns.TASKID
																				+ " IN "
																				+ idStrings, null);
				return null;
			}
		};
		task.execute(listId);
	}

	public static void completeTasksInList(final Context context, final long listId,
									final long maxTime) {

	}

	/**
	 * Returns true if the notification repeats on the given day. Day of the
	 * week as given by Calendar.getField(DayOfWeek)
	 */
	public boolean repeatsOn(final int calendarDay) {
		int day;

		switch (calendarDay) {
		case Calendar.MONDAY:
			day = WeekDaysView.mon;
			break;
		case Calendar.TUESDAY:
			day = WeekDaysView.tue;
			break;
		case Calendar.WEDNESDAY:
			day = WeekDaysView.wed;
			break;
		case Calendar.THURSDAY:
			day = WeekDaysView.thu;
			break;
		case Calendar.FRIDAY:
			day = WeekDaysView.fri;
			break;
		case Calendar.SATURDAY:
			day = WeekDaysView.sat;
			break;
		case Calendar.SUNDAY:
			day = WeekDaysView.sun;
			break;
		default:
			day = 0;
		}

		return (0 < (day & repeats));
	}

	public void deleteOrReschedule(final Context context) {
		if (repeats == 0 || time == null) {
			delete(context);
		} else {
			// Need to set the correct time, but using today as the date
			// Because no sense in setting reminders in the past
			GregorianCalendar gcOrgTime = new GregorianCalendar();
			gcOrgTime.setTimeInMillis(time);
			// Use today's date
			GregorianCalendar gc = new GregorianCalendar();
			final long now = gc.getTimeInMillis();
			// With original time
			gc.set(GregorianCalendar.HOUR_OF_DAY, gcOrgTime.get(GregorianCalendar.HOUR_OF_DAY));
			gc.set(GregorianCalendar.MINUTE, gcOrgTime.get(GregorianCalendar.MINUTE));
			// Save as base
			final long base = gc.getTimeInMillis();

			// Check today if the time is actually in the future
			final int start = now < base ? 0 : 1;
			final long oneDay = 24 * 60 * 60 * 1000;
			boolean done = false;
			for (int i = start; i <= 7; i++) {
				gc.setTimeInMillis(base + i * oneDay);

				if (repeatsOn(gc.get(GregorianCalendar.DAY_OF_WEEK))) {
					done = true;
					time = gc.getTimeInMillis();
					save(context);
					break;
				}

			}
			// Just in case of faulty repeat codes
			if (!done) {
				delete(context);
			}
		}
	}

	public String getRepeatAsText(final Context context) {
		final StringBuilder sb = new StringBuilder();

		SimpleDateFormat weekDayFormatter = TimeFormatter.getLocalFormatterWeekdayShort(context);
		// 2013-05-13 was a monday
		GregorianCalendar gc = new GregorianCalendar(2013, GregorianCalendar.MAY, 13);
		final long base = gc.getTimeInMillis();
		final long day = 24 * 60 * 60 * 1000;
		for (int i = 0; i < 7; i++) {
			gc.setTimeInMillis(base + i * day);

			if (repeatsOn(gc.get(GregorianCalendar.DAY_OF_WEEK))) {
				if (sb.length() > 0) {
					sb.append(", ");
				}
				sb.append(weekDayFormatter.format(gc.getTime()));
			}
		}

		return sb.toString();
	}
} public class Task extends DAO {

	// Used to separate tasks with due dates from completed and from tasks with
	// no date
	public static final String SECRET_TYPEID = "secret_typeid";
	public static final String SECRET_TYPEID2 = "secret_typeid2";

	// SQL convention says Table name should be "singular"
	public static final String TABLE_NAME = "task";
	public static final String DELETE_TABLE_NAME = "deleted_task";
	public static final String FTS3_DELETE_TABLE_NAME = "fts3_deleted_task";
	public static final String HISTORY_TABLE_NAME = "history";
	private static final String SECTIONED_DATE_VIEW = "sectioned_date_view";
	public static final String FTS3_TABLE_NAME = "fts3_task";
  private static final String TAG = "Task";

  public static String getSECTION_DATE_VIEW_NAME(final String listId) {
		// listId CAN be null. Hence the string concat hack
		return new StringBuilder().append(SECTIONED_DATE_VIEW).append("_")
				.append("" + listId).toString();
	}

	// Used in sectioned view date
	static final String FAR_FUTURE = "strftime('%s','3999-01-01') * 1000";
	public static final String OVERDUE = "strftime('%s', '1970-01-01') * 1000";
	// Today should be from NOW...
	public static final String TODAY_START = "strftime('%s','now', 'utc') * 1000";

	// static final String TODAY_START =
	// "strftime('%s','now','localtime','start of day', 'utc') * 1000";

	public static final String TODAY_PLUS(final int offset) {
		return "strftime('%s','now','localtime','+" + Integer.toString(offset)
				+ " days','start of day', 'utc') * 1000";
	}

	// Code used to decode title of date header
	public static final String HEADER_KEY_TODAY = "today+0";
	public static final String HEADER_KEY_PLUS1 = "today+1";
	public static final String HEADER_KEY_PLUS2 = "today+2";
	public static final String HEADER_KEY_PLUS3 = "today+3";
	public static final String HEADER_KEY_PLUS4 = "today+4";
	public static final String HEADER_KEY_OVERDUE = "overdue";
	public static final String HEADER_KEY_LATER = "later";
	public static final String HEADER_KEY_NODATE = "nodate";
	public static final String HEADER_KEY_COMPLETE = "complete";

	public static final String CONTENT_TYPE = "vnd.android.cursor.item/vnd.nononsenseapps.note";

	public static final Uri URI = Uri.withAppendedPath(
			Uri.parse(MyContentProvider.SCHEME + MyContentProvider.AUTHORITY),
			TABLE_NAME);

	public static Uri getUri(final long id) {
		return Uri.withAppendedPath(URI, Long.toString(id));
	}

	public static final int BASEURICODE = 201;
	public static final int BASEITEMCODE = 202;
	public static final int DELETEDQUERYCODE = 209;
	public static final int DELETEDITEMCODE = 210;
	public static final int SECTIONEDDATEQUERYCODE = 211;
	public static final int SECTIONEDDATEITEMCODE = 212;
	public static final int HISTORYQUERYCODE = 213;
	public static final int MOVEITEMLEFTCODE = 214;
	public static final int MOVEITEMRIGHTCODE = 215;
	// Legacy support, these also need to use legacy projections
	public static final int LEGACYBASEURICODE = 221;
	public static final int LEGACYBASEITEMCODE = 222;
	public static final int LEGACYVISIBLEURICODE = 223;
	public static final int LEGACYVISIBLEITEMCODE = 224;
	// Search URI
	public static final int SEARCHCODE = 299;
	public static final int SEARCHSUGGESTIONSCODE = 298;

	public static void addMatcherUris(UriMatcher sURIMatcher) {
		sURIMatcher
				.addURI(MyContentProvider.AUTHORITY, TABLE_NAME, BASEURICODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY, TABLE_NAME + "/#",
				BASEITEMCODE);

		sURIMatcher.addURI(MyContentProvider.AUTHORITY, TABLE_NAME + "/"
				+ MOVEITEMLEFT + "/#", MOVEITEMLEFTCODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY, TABLE_NAME + "/"
				+ MOVEITEMRIGHT + "/#", MOVEITEMRIGHTCODE);

		sURIMatcher.addURI(MyContentProvider.AUTHORITY, TABLE_NAME + "/"
				+ DELETEDQUERY, DELETEDQUERYCODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY, TABLE_NAME + "/"
				+ DELETEDQUERY + "/#", DELETEDITEMCODE);

		sURIMatcher.addURI(MyContentProvider.AUTHORITY, TABLE_NAME + "/"
				+ SECTIONED_DATE_VIEW, SECTIONEDDATEQUERYCODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY, TABLE_NAME + "/"
				+ SECTIONED_DATE_VIEW + "/#", SECTIONEDDATEITEMCODE);

		sURIMatcher.addURI(MyContentProvider.AUTHORITY, TABLE_NAME + "/"
				+ HISTORY_TABLE_NAME, HISTORYQUERYCODE);

		// Legacy URIs
		sURIMatcher.addURI(MyContentProvider.AUTHORITY,
				LegacyDBHelper.NotePad.Notes.NOTES, LEGACYBASEURICODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY,
				LegacyDBHelper.NotePad.Notes.NOTES + "/#", LEGACYBASEITEMCODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY,
				LegacyDBHelper.NotePad.Notes.VISIBLE_NOTES,
				LEGACYVISIBLEURICODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY,
				LegacyDBHelper.NotePad.Notes.VISIBLE_NOTES + "/#",
				LEGACYVISIBLEITEMCODE);

		// Search URI
		sURIMatcher.addURI(MyContentProvider.AUTHORITY, FTS3_TABLE_NAME,
				SEARCHCODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY,
				SearchManager.SUGGEST_URI_PATH_QUERY, SEARCHSUGGESTIONSCODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY,
				SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
				SEARCHSUGGESTIONSCODE);

	}

	public static final String TARGETPOS = "targetpos";
	private static final String MOVEITEMLEFT = "moveitemleft";
	private static final String MOVEITEMRIGHT = "moveitemright";
	private static final String DELETEDQUERY = "deletedquery";

	// Special URI to look at backup table
	public static final Uri URI_DELETED_QUERY = Uri.withAppendedPath(URI,
			DELETEDQUERY);

	// Query the view with date section headers
	public static final Uri URI_SECTIONED_BY_DATE = Uri.withAppendedPath(URI,
			SECTIONED_DATE_VIEW);

	// Query for history of tasks
	public static final Uri URI_TASK_HISTORY = Uri.withAppendedPath(URI,
			HISTORY_TABLE_NAME);

	// Search URI
	public static final Uri URI_SEARCH = Uri.withAppendedPath(
			Uri.parse(MyContentProvider.SCHEME + MyContentProvider.AUTHORITY),
			FTS3_TABLE_NAME);

	// Special URI to use when a move is requested
	// public static final Uri URI_WRITE_MOVESUBTREE = Uri.withAppendedPath(URI,
	// MOVESUBTREE);
	private static final Uri URI_WRITE_MOVEITEMLEFT = Uri.withAppendedPath(URI,
			MOVEITEMLEFT);
	private static final Uri URI_WRITE_MOVEITEMRIGHT = Uri.withAppendedPath(
			URI, MOVEITEMRIGHT);

	// public Uri getMoveSubTreeUri() {
	// if (_id < 1) {
	// throw new InvalidParameterException(
	// "_ID of this object is not valid");
	// }
	// return Uri.withAppendedPath(URI_WRITE_MOVESUBTREE, Long.toString(_id));
	// }
	private Uri getMoveItemLeftUri() {
		if (_id < 1) {
			throw new InvalidParameterException(
					"_ID of this object is not valid");
		}
		return Uri.withAppendedPath(URI_WRITE_MOVEITEMLEFT, Long.toString(_id));
	}

	private Uri getMoveItemRightUri() {
		if (_id < 1) {
			throw new InvalidParameterException(
					"_ID of this object is not valid");
		}
		return Uri
				.withAppendedPath(URI_WRITE_MOVEITEMRIGHT, Long.toString(_id));
	}

    public static int delete(final long id, Context context) {
        if (id > 0) {
            return context.getContentResolver()
                    .delete(Uri.withAppendedPath(Uri.withAppendedPath(
                            Uri.parse(MyContentProvider.SCHEME + MyContentProvider.AUTHORITY),
                            TABLE_NAME), Long.toString(id)),
                            null,
                            null);
        }
        throw new IllegalArgumentException("Can only delete positive ids");
    }

	public static class Columns implements BaseColumns {

		private Columns() {
		}

		public static final String TITLE = "title";
		public static final String NOTE = "note";
		public static final String DBLIST = "dblist";
		public static final String COMPLETED = "completed";
		public static final String DUE = "due";
		public static final String UPDATED = "updated";
		public static final String LOCKED = "locked";

		public static final String LEFT = "lft";
		public static final String RIGHT = "rgt";

		public static final String[] FIELDS = { _ID, TITLE, NOTE, COMPLETED,
				DUE, UPDATED, LEFT, RIGHT, DBLIST, LOCKED };
		public static final String[] FIELDS_NO_ID = { TITLE, NOTE, COMPLETED,
				DUE, UPDATED, LEFT, RIGHT, DBLIST, LOCKED };
		public static final String[] SHALLOWFIELDS = { _ID, TITLE, NOTE,
				DBLIST, COMPLETED, DUE, UPDATED, LOCKED };
		public static final String TRIG_DELETED = "deletedtime";
		public static final String HIST_TASK_ID = "taskid";
		// Used to read the table. Deleted field set by database
		public static final String[] DELETEFIELDS = { _ID, TITLE, NOTE,
				COMPLETED, DUE, DBLIST, TRIG_DELETED };
		// Used in trigger creation
		private static final String[] DELETEFIELDS_TRIGGER = { TITLE, NOTE,
				COMPLETED, DUE, DBLIST };

		// accessible fields in history table
		public static final String[] HISTORY_COLUMNS = { Columns.HIST_TASK_ID,
				Columns.TITLE, Columns.NOTE };
		public static final String[] HISTORY_COLUMNS_UPDATED = { Columns.HIST_TASK_ID,
			Columns.TITLE, Columns.NOTE, Columns.UPDATED };

	}

	public static final String CREATE_TABLE = new StringBuilder("CREATE TABLE ")
			.append(TABLE_NAME)
			.append("(")
			.append(Columns._ID)
			.append(" INTEGER PRIMARY KEY,")
			.append(Columns.TITLE)
			.append(" TEXT NOT NULL DEFAULT '',")
			.append(Columns.NOTE)
			.append(" TEXT NOT NULL DEFAULT '',")
			// These are all msec times
			.append(Columns.COMPLETED)
			.append(" INTEGER DEFAULT NULL,")
			.append(Columns.UPDATED)
			.append(" INTEGER DEFAULT NULL,")
			.append(Columns.DUE)
			.append(" INTEGER DEFAULT NULL,")
			// boolean, 1 for locked, unlocked otherwise
			.append(Columns.LOCKED)
			.append(" INTEGER NOT NULL DEFAULT 0,")

			// position stuff
			.append(Columns.LEFT)
			.append(" INTEGER NOT NULL DEFAULT 1,")
			.append(Columns.RIGHT)
			.append(" INTEGER NOT NULL DEFAULT 2,")
			.append(Columns.DBLIST)
			.append(" INTEGER NOT NULL,")

			// Positions must be positive and ordered!
			.append(" CHECK(")
			.append(Columns.LEFT)
			.append(" > 0), ")
			.append(" CHECK(")
			.append(Columns.RIGHT)
			.append(" > 1), ")
			// Each side's value should be unique in it's list
			// Handled in trigger
			// ).append(" UNIQUE(").append(Columns.LEFT).append(", ").append(Columns.DBLIST).append(")"
			// ).append(" UNIQUE(").append(Columns.RIGHT).append(", ").append(Columns.DBLIST).append(")"

			// Foreign key for list
			.append("FOREIGN KEY(").append(Columns.DBLIST)
			.append(") REFERENCES ").append(TaskList.TABLE_NAME).append("(")
			.append(TaskList.Columns._ID).append(") ON DELETE CASCADE")
			.append(")")

			.toString();

	// Delete table has no constraints. In fact, list values and positions
	// should not even be thought of as valid.
	public static final String CREATE_DELETE_TABLE = new StringBuilder(
			"CREATE TABLE ").append(DELETE_TABLE_NAME).append("(")
			.append(Columns._ID).append(" INTEGER PRIMARY KEY,")
			.append(Columns.TITLE).append(" TEXT NOT NULL DEFAULT '',")
			.append(Columns.NOTE).append(" TEXT NOT NULL DEFAULT '',")
			.append(Columns.COMPLETED).append(" INTEGER DEFAULT NULL,")
			.append(Columns.DUE).append(" INTEGER DEFAULT NULL,")
			.append(Columns.DBLIST).append(" INTEGER DEFAULT NULL,")
			.append(Columns.TRIG_DELETED)
			.append(" TIMESTAMP NOT NULL DEFAULT current_timestamp")
			.append(")").toString();

	// Every change to a note gets saved here
	public static final String CREATE_HISTORY_TABLE = new StringBuilder(
			"CREATE TABLE ").append(HISTORY_TABLE_NAME).append("(")
			.append(Columns._ID).append(" INTEGER PRIMARY KEY,")
			.append(Columns.HIST_TASK_ID).append(" INTEGER NOT NULL,")
			.append(Columns.TITLE).append(" TEXT NOT NULL DEFAULT '',")
			.append(Columns.NOTE).append(" TEXT NOT NULL DEFAULT '',")
			.append(Columns.UPDATED)
			.append(" TIMESTAMP NOT NULL DEFAULT current_timestamp,")
			.append(" FOREIGN KEY(").append(Columns.HIST_TASK_ID)
			.append(" ) REFERENCES ").append(TABLE_NAME).append(" ( ")
			.append(Columns._ID).append(") ON DELETE CASCADE ").append(" ) ")
			.toString();
	static final String HISTORY_TRIGGER_BODY = new StringBuilder(
			" INSERT INTO ")
			.append(HISTORY_TABLE_NAME)
			.append(" (")
			.append(arrayToCommaString(Columns.HISTORY_COLUMNS))
			.append(")")
			.append(" VALUES (")
			.append(arrayToCommaString("new.", new String[] { Columns._ID,
					Columns.TITLE, Columns.NOTE })).append(");").toString();
	public static final String HISTORY_UPDATE_TRIGGER_NAME = "trigger_update_" + HISTORY_TABLE_NAME;
	public static final String CREATE_HISTORY_UPDATE_TRIGGER = new StringBuilder(
			"CREATE TRIGGER ").append(HISTORY_UPDATE_TRIGGER_NAME)
			.append(" AFTER UPDATE OF ")
			.append(arrayToCommaString(new String[] { Columns.TITLE,
					Columns.NOTE })).append(" ON ").append(TABLE_NAME)

			.append(" WHEN old.")
			.append(Columns.TITLE).append(" IS NOT new.")
			.append(Columns.TITLE).append(" OR old.")
			.append(Columns.NOTE).append(" IS NOT new.")
			.append(Columns.NOTE)

			.append(" BEGIN ").append(HISTORY_TRIGGER_BODY).append(" END;")
			.toString();
	public static final String CREATE_HISTORY_INSERT_TRIGGER = new StringBuilder(
			"CREATE TRIGGER trigger_insert_").append(HISTORY_TABLE_NAME)
			.append(" AFTER INSERT ON ").append(TABLE_NAME).append(" BEGIN ")
			.append(HISTORY_TRIGGER_BODY).append(" END;").toString();

	// Delete search table
	public static final String CREATE_FTS3_DELETE_TABLE = "CREATE VIRTUAL TABLE "
			+ FTS3_DELETE_TABLE_NAME
			+ " USING FTS3("
			+ Columns._ID
			+ ", "
			+ Columns.TITLE + ", " + Columns.NOTE + ");";
	public static final String CREATE_FTS3_DELETED_INSERT_TRIGGER = new StringBuilder()
			.append("CREATE TRIGGER deletedtask_fts3_insert AFTER INSERT ON ")
			.append(DELETE_TABLE_NAME)
			.append(" BEGIN ")
			.append(" INSERT INTO ")
			.append(FTS3_DELETE_TABLE_NAME)
			.append(" (")
			.append(arrayToCommaString(Columns._ID, Columns.TITLE, Columns.NOTE))
			.append(") VALUES (")
			.append(arrayToCommaString("new.", new String[] { Columns._ID,
					Columns.TITLE, Columns.NOTE })).append(");")
			.append(" END;").toString();

	public static final String CREATE_FTS3_DELETED_UPDATE_TRIGGER = new StringBuilder()
			.append("CREATE TRIGGER deletedtask_fts3_update AFTER UPDATE OF ")
			.append(arrayToCommaString(new String[] { Columns.TITLE,
					Columns.NOTE })).append(" ON ").append(DELETE_TABLE_NAME)
			.append(" BEGIN ").append(" UPDATE ")
			.append(FTS3_DELETE_TABLE_NAME).append(" SET ")
			.append(Columns.TITLE).append(" = new.").append(Columns.TITLE)
			.append(",").append(Columns.NOTE).append(" = new.")
			.append(Columns.NOTE).append(" WHERE ").append(Columns._ID)
			.append(" IS new.").append(Columns._ID).append(";").append(" END;")
			.toString();
	public static final String CREATE_FTS3_DELETED_DELETE_TRIGGER = new StringBuilder()
			.append("CREATE TRIGGER deletedtask_fts3_delete AFTER DELETE ON ")
			.append(DELETE_TABLE_NAME).append(" BEGIN ")
			.append(" DELETE FROM ").append(FTS3_DELETE_TABLE_NAME)
			.append(" WHERE ").append(Columns._ID).append(" IS old.")
			.append(Columns._ID).append(";").append(" END;").toString();

	// Search table
	public static final String CREATE_FTS3_TABLE = "CREATE VIRTUAL TABLE "
			+ FTS3_TABLE_NAME + " USING FTS3(" + Columns._ID + ", "
			+ Columns.TITLE + ", " + Columns.NOTE + ");";

	public static final String CREATE_FTS3_INSERT_TRIGGER = new StringBuilder()
			.append("CREATE TRIGGER task_fts3_insert AFTER INSERT ON ")
			.append(TABLE_NAME)
			.append(" BEGIN ")
			.append(" INSERT INTO ")
			.append(FTS3_TABLE_NAME)
			.append(" (")
			.append(arrayToCommaString(Columns._ID, Columns.TITLE, Columns.NOTE))
			.append(") VALUES (")
			.append(arrayToCommaString("new.", new String[] { Columns._ID,
					Columns.TITLE, Columns.NOTE })).append(");")
			.append(" END;").toString();

	public static final String CREATE_FTS3_UPDATE_TRIGGER = new StringBuilder()
			.append("CREATE TRIGGER task_fts3_update AFTER UPDATE OF ")
			.append(arrayToCommaString(new String[] { Columns.TITLE,
					Columns.NOTE })).append(" ON ").append(TABLE_NAME)
			.append(" BEGIN ").append(" UPDATE ").append(FTS3_TABLE_NAME)
			.append(" SET ").append(Columns.TITLE).append(" = new.")
			.append(Columns.TITLE).append(",").append(Columns.NOTE)
			.append(" = new.").append(Columns.NOTE).append(" WHERE ")
			.append(Columns._ID).append(" IS new.").append(Columns._ID)
			.append(";").append(" END;").toString();

	public static final String CREATE_FTS3_DELETE_TRIGGER = new StringBuilder()
			.append("CREATE TRIGGER task_fts3_delete AFTER DELETE ON ")
			.append(TABLE_NAME).append(" BEGIN ").append(" DELETE FROM ")
			.append(FTS3_TABLE_NAME).append(" WHERE ").append(Columns._ID)
			.append(" IS old.").append(Columns._ID).append(";").append(" END;")
			.toString();

	/**
	 * This is a view which returns the tasks in the specified list with headers
	 * suitable for dates, if any tasks would be sorted under them. Provider
	 * hardcodes the sort order for this.
	 *
	 * if listId is null, will return for all lists
	 */
	public static final String CREATE_SECTIONED_DATE_VIEW(final String listId) {
		final String sListId = listId == null ? " NOT NULL " : "'" + listId
				+ "'";
		return new StringBuilder()
				.append("CREATE TEMP VIEW IF NOT EXISTS ")
				.append(getSECTION_DATE_VIEW_NAME(listId))
				// Tasks WITH dates NOT completed, secret 0
				.append(" AS SELECT ")
				.append(arrayToCommaString(Columns.FIELDS))
				.append(",0")
				.append(" AS ")
				.append(SECRET_TYPEID)
				.append(",1")
				.append(" AS ")
				.append(SECRET_TYPEID2)
				.append(" FROM ")
				.append(TABLE_NAME)
				.append(" WHERE ")
				.append(Columns.COMPLETED)
				.append(" IS null ")
				.append(" AND ")
				.append(Columns.DUE)
				.append(" IS NOT null ")
				.append(" UNION ALL ")
				// Tasks NO dates NOT completed, secret 1
				.append(" SELECT ")
				.append(arrayToCommaString(Columns.FIELDS))
				.append(",1")
				.append(" AS ")
				.append(SECRET_TYPEID)
				.append(",1")
				.append(" AS ")
				.append(SECRET_TYPEID2)
				.append(" FROM ")
				.append(TABLE_NAME)
				.append(" WHERE ")
				.append(Columns.COMPLETED)
				.append(" IS null ")
				.append(" AND ")
				.append(Columns.DUE)
				.append(" IS null ")
				.append(" UNION ALL ")
				// Tasks completed, secret 2 + 1
				.append(" SELECT ")
				.append(arrayToCommaString(Columns.FIELDS))
				.append(",3")
				.append(" AS ")
				.append(SECRET_TYPEID)
				.append(",1")
				.append(" AS ")
				.append(SECRET_TYPEID2)
				.append(" FROM ")
				.append(TABLE_NAME)
				.append(" WHERE ")
				.append(Columns.COMPLETED)
				.append(" IS NOT null ")
				// TODAY
				.append(" UNION ALL ")
				.append(" SELECT -1,")
				.append(asEmptyCommaStringExcept(Columns.FIELDS_NO_ID,
						Columns.DUE, TODAY_START, Columns.TITLE,
						HEADER_KEY_TODAY, Columns.DBLIST, listId))
				.append(",0,0")
				// Only show header if there are tasks under it
				.append(" WHERE EXISTS(SELECT _ID FROM ")
				.append(TABLE_NAME)
				.append(" WHERE ")
				.append(Columns.COMPLETED)
				.append(" IS NULL ")
				.append(" AND ")
				.append(Columns.DBLIST)
				.append(" IS ")
				.append(sListId)
				.append(" AND ")
				.append(Columns.DUE)
				.append(" BETWEEN ")
				.append(TODAY_START)
				.append(" AND ")
				.append(TODAY_PLUS(1))
				.append(") ")
				// TOMORROW (Today + 1)
				.append(" UNION ALL ")
				.append(" SELECT -1,")
				.append(asEmptyCommaStringExcept(Columns.FIELDS_NO_ID,
						Columns.DUE, TODAY_PLUS(1), Columns.TITLE,
						HEADER_KEY_PLUS1, Columns.DBLIST, listId))
				.append(",0,0")
				// Only show header if there are tasks under it
				.append(" WHERE EXISTS(SELECT _ID FROM ")
				.append(TABLE_NAME)
				.append(" WHERE ")
				.append(Columns.COMPLETED)
				.append(" IS NULL ")
				.append(" AND ")
				.append(Columns.DBLIST)
				.append(" IS ")
				.append(sListId)
				.append(" AND ")
				.append(Columns.DUE)
				.append(" BETWEEN ")
				.append(TODAY_PLUS(1))
				.append(" AND ")
				.append(TODAY_PLUS(2))
				.append(") ")
				// Today + 2
				.append(" UNION ALL ")
				.append(" SELECT -1,")
				.append(asEmptyCommaStringExcept(Columns.FIELDS_NO_ID,
						Columns.DUE, TODAY_PLUS(2), Columns.TITLE,
						HEADER_KEY_PLUS2, Columns.DBLIST, listId))
				.append(",0,0")
				// Only show header if there are tasks under it
				.append(" WHERE EXISTS(SELECT _ID FROM ")
				.append(TABLE_NAME)
				.append(" WHERE ")
				.append(Columns.COMPLETED)
				.append(" IS NULL ")
				.append(" AND ")
				.append(Columns.DBLIST)
				.append(" IS ")
				.append(sListId)
				.append(" AND ")
				.append(Columns.DUE)
				.append(" BETWEEN ")
				.append(TODAY_PLUS(2))
				.append(" AND ")
				.append(TODAY_PLUS(3))
				.append(") ")
				// Today + 3
				.append(" UNION ALL ")
				.append(" SELECT -1,")
				.append(asEmptyCommaStringExcept(Columns.FIELDS_NO_ID,
						Columns.DUE, TODAY_PLUS(3), Columns.TITLE,
						HEADER_KEY_PLUS3, Columns.DBLIST, listId))
				.append(",0,0")
				// Only show header if there are tasks under it
				.append(" WHERE EXISTS(SELECT _ID FROM ")
				.append(TABLE_NAME)
				.append(" WHERE ")
				.append(Columns.COMPLETED)
				.append(" IS NULL ")
				.append(" AND ")
				.append(Columns.DBLIST)
				.append(" IS ")
				.append(sListId)
				.append(" AND ")
				.append(Columns.DUE)
				.append(" BETWEEN ")
				.append(TODAY_PLUS(3))
				.append(" AND ")
				.append(TODAY_PLUS(4))
				.append(") ")
				// Today + 4
				.append(" UNION ALL ")
				.append(" SELECT -1,")
				.append(asEmptyCommaStringExcept(Columns.FIELDS_NO_ID,
						Columns.DUE, TODAY_PLUS(4), Columns.TITLE,
						HEADER_KEY_PLUS4, Columns.DBLIST, listId))
				.append(",0,0")
				// Only show header if there are tasks under it
				.append(" WHERE EXISTS(SELECT _ID FROM ")
				.append(TABLE_NAME)
				.append(" WHERE ")
				.append(Columns.COMPLETED)
				.append(" IS NULL ")
				.append(" AND ")
				.append(Columns.DBLIST)
				.append(" IS ")
				.append(sListId)
				.append(" AND ")
				.append(Columns.DUE)
				.append(" BETWEEN ")
				.append(TODAY_PLUS(4))
				.append(" AND ")
				.append(TODAY_PLUS(5))
				.append(") ")
				// Overdue (0)
				.append(" UNION ALL ")
				.append(" SELECT -1,")
				.append(asEmptyCommaStringExcept(Columns.FIELDS_NO_ID,
						Columns.DUE, OVERDUE, Columns.TITLE,
						HEADER_KEY_OVERDUE, Columns.DBLIST, listId))
				.append(",0,0")
				// Only show header if there are tasks under it
				.append(" WHERE EXISTS(SELECT _ID FROM ")
				.append(TABLE_NAME)
				.append(" WHERE ")
				.append(Columns.COMPLETED)
				.append(" IS NULL ")
				.append(" AND ")
				.append(Columns.DBLIST)
				.append(" IS ")
				.append(sListId)
				.append(" AND ")
				.append(Columns.DUE)
				.append(" BETWEEN ")
				.append(OVERDUE)
				.append(" AND ")
				.append(TODAY_START)
				.append(") ")
				// Later
				.append(" UNION ALL ")
				.append(" SELECT '-1',")
				.append(asEmptyCommaStringExcept(Columns.FIELDS_NO_ID,
						Columns.DUE, TODAY_PLUS(5), Columns.TITLE,
						HEADER_KEY_LATER, Columns.DBLIST, listId))
				.append(",0,0")
				// Only show header if there are tasks under it
				.append(" WHERE EXISTS(SELECT _ID FROM ")
				.append(TABLE_NAME)
				.append(" WHERE ")
				.append(Columns.COMPLETED)
				.append(" IS NULL ")
				.append(" AND ")
				.append(Columns.DBLIST)
				.append(" IS ")
				.append(sListId)
				.append(" AND ")
				.append(Columns.DUE)
				.append(" >= ")
				.append(TODAY_PLUS(5))
				.append(") ")
				// No date
				.append(" UNION ALL ")
				.append(" SELECT -1,")
				.append(asEmptyCommaStringExcept(Columns.FIELDS_NO_ID,
						Columns.DUE, "null", Columns.TITLE, HEADER_KEY_NODATE,
						Columns.DBLIST, listId))
				.append(",1,0")
				// Only show header if there are tasks under it
				.append(" WHERE EXISTS(SELECT _ID FROM ")
				.append(TABLE_NAME)
				.append(" WHERE ")
				.append(Columns.DBLIST)
				.append(" IS ")
				.append(sListId)
				.append(" AND ")
				.append(Columns.DUE)
				.append(" IS null ")
				.append(" AND ")
				.append(Columns.COMPLETED)
				.append(" IS null ")
				.append(") ")
				// Complete, overdue to catch all
				// Set complete time to 1
				.append(" UNION ALL ")
				.append(" SELECT -1,")
				.append(asEmptyCommaStringExcept(Columns.FIELDS_NO_ID,
						Columns.DUE, OVERDUE, Columns.COMPLETED, "1",
						Columns.TITLE, HEADER_KEY_COMPLETE, Columns.DBLIST,
						listId))
				.append(",2,0")
				// Only show header if there are tasks under it
				.append(" WHERE EXISTS(SELECT _ID FROM ").append(TABLE_NAME)
				.append(" WHERE ").append(Columns.DBLIST).append(" IS ")
				.append(sListId).append(" AND ").append(Columns.COMPLETED)
				.append(" IS NOT null ").append(") ")

				.append(";").toString();
	}

	public String title = null;
	public String note = null;
	// All milliseconds since 1970-01-01 UTC
	public Long completed = null;
	public Long due = null;
	public Long updated = null;
	// converted from integer
	public boolean locked = false;

	// position stuff
	public Long left = null;
	public Long right = null;
	public Long dblist = null;

	public Task() {

	}

	/**
	 * Resets id and position values
	 */
	public void resetForInsertion() {
		_id = -1;
		left = null;
		right = null;
	}

	/**
	 * Set task as completed. Returns the time stamp that is set.
	 */
	public Long setAsCompleted() {

		final Time time = new Time(Time.TIMEZONE_UTC);
		time.setToNow();
		completed = new Time().toMillis(false);
		return completed;
	}

	/**
	 * Set first line as title, rest as note.
	 *
	 * @param text
	 */
	public void setText(final String text) {
		int titleEnd = text.indexOf("\n");

		if (titleEnd < 0) {
			titleEnd = text.length();
		}

		title = text.substring(0, titleEnd);
		if (titleEnd + 1 < text.length()) {
			note = text.substring(titleEnd + 1, text.length());
		}
		else {
			note = "";
		}
	}

	/**
	 * Returns a text where first line is title, rest is note
	 */
	public String getText() {
		String result = "";
		if (title != null) {
			result += title;
		}
		if (note != null && !note.isEmpty()) {
            // If title is empty, it should remain so
			result += "\n" + note;
		}
		return result;
	}

	public Task(final Cursor c) {
		this._id = c.getLong(0);
		this.title = c.getString(1);
		note = c.getString(2);
		// msec times which can be null
		if (!c.isNull(3)) completed = c.getLong(3);
		if (!c.isNull(4)) due = c.getLong(4);
		if (!c.isNull(5)) updated = c.getLong(5);

		// enforced not to be null
		left = c.getLong(6);
		right = c.getLong(7);
		dblist = c.getLong(8);
		locked = c.getInt(9) == 1;
	}

	public Task(final long id, final ContentValues values) {
		this(values);
		this._id = id;
	}

	public Task(final Uri uri, final ContentValues values) {
		this(Long.parseLong(uri.getLastPathSegment()), values);
	}

	public Task(final ContentValues values) {
		if (values != null) {
			if (values.containsKey(TARGETPOS)) {
				// Content form getMoveValues
				this.left = values.getAsLong(Columns.LEFT);
				this.right = values.getAsLong(Columns.RIGHT);
				this.dblist = values.getAsLong(Columns.DBLIST);
			}
			else {
				this.title = values.getAsString(Columns.TITLE);
				this.note = values.getAsString(Columns.NOTE);
				this.completed = values.getAsLong(Columns.COMPLETED);
				this.due = values.getAsLong(Columns.DUE);
				this.updated = values.getAsLong(Columns.UPDATED);
				this.locked = values.getAsLong(Columns.LOCKED) == 1;

				this.dblist = values.getAsLong(Columns.DBLIST);
				this.left = values.getAsLong(Columns.LEFT);
				this.right = values.getAsLong(Columns.RIGHT);
			}
		}
	}

	public Task(final JSONObject json) throws JSONException {
		if (json.has(Columns.TITLE))
			this.title = json.getString(Columns.TITLE);
		if (json.has(Columns.NOTE))
			this.note = json.getString(Columns.NOTE);
		if (json.has(Columns.COMPLETED))
			this.completed = json.getLong(Columns.COMPLETED);
		if (json.has(Columns.DUE))
			this.due = json.getLong(Columns.DUE);
		if (json.has(Columns.UPDATED))
			this.updated = json.getLong(Columns.UPDATED);
		if (json.has(Columns.LOCKED))
			this.locked = json.getLong(Columns.LOCKED) == 1;
		if (json.has(Columns.DBLIST))
			this.dblist = json.getLong(Columns.DBLIST);
		if (json.has(Columns.LEFT))
			this.left = json.getLong(Columns.LEFT);
		if (json.has(Columns.RIGHT))
			this.right = json.getLong(Columns.RIGHT);
	}

	/**
	 * A move operation should be performed alone. No other information should
	 * accompany such an update.
	 */
	public ContentValues getMoveValues(final long targetPos) {
		final ContentValues values = new ContentValues();
		values.put(TARGETPOS, targetPos);
		values.put(Columns.LEFT, left);
		values.put(Columns.RIGHT, right);
		values.put(Columns.DBLIST, dblist);
		return values;
	}

	/**
	 * Use this for regular updates of the task.
	 */
	@Override
	public ContentValues getContent() {
		final ContentValues values = new ContentValues();
		// Note that ID is NOT included here
		if (title != null) values.put(Columns.TITLE, title);
		if (note != null) values.put(Columns.NOTE, note);

		if (dblist != null) values.put(Columns.DBLIST, dblist);
		// Never write position values unless you are 110% sure that they are correct
		//if (left != null) values.put(Columns.LEFT, left);
		//if (right != null) values.put(Columns.RIGHT, right);

		values.put(Columns.UPDATED, updated);
		values.put(Columns.DUE, due);
		values.put(Columns.COMPLETED, completed);
		values.put(Columns.LOCKED, locked ? 1 : 0);

		return values;
	}

	/**
	 * Compares this task to another and returns true if their contents are the
	 * same. Content is defined as: title, note, duedate, completed != null
	 * Returns false if title or note are null.
	 *
	 * The intended usage is the editor where content and not id's or position
	 * are of importance.
	 */
	@Override
	public boolean equals(Object o) {
		boolean result = false;

		if (o instanceof Task) {
			final Task other = (Task) o;
			result = true;

			result &= (title != null && title.equals(other.title));
			result &= (note != null && note.equals(other.note));
			result &= (due == other.due);
			result &= ((completed != null) == (other.completed != null));

		}
		else {
			result = super.equals(o);
		}

		return result;
	}

	/**
	 * Convenience method for normal operations. Updates "updated" field to
	 * specified Returns number of db-rows affected. Fail if < 1
	 */
	public int save(final Context context, final long updated) {
		int result = 0;
		this.updated = updated;
		if (_id < 1) {
			final Uri uri = context.getContentResolver().insert(getBaseUri(),
					getContent());
			if (uri != null) {
				_id = Long.parseLong(uri.getLastPathSegment());
				result++;
			}
		}
		else {
			result += context.getContentResolver().update(getUri(),
					getContent(), null, null);
		}
		return result;
	}

	/**
	 * Convenience method for normal operations. Updates "updated" field.
	 * Returns number of db-rows affected. Fail if < 1
	 */
	@Override
	public int save(final Context context) {
		return save(context, Calendar.getInstance().getTimeInMillis());
	}

	/**
	 * Convenience method to complete tasks in list view for example. Starts an
	 * asynctask to do the operation in the background.
	 */
	public static void setCompleted(final Context context,
			final boolean completed, final Long... ids) {
		if (ids.length > 0) {
			final AsyncTask<Long, Void, Void> task = new AsyncTask<Long, Void, Void>() {
				@Override
				protected Void doInBackground(final Long... ids) {
					setCompletedSynced(context, completed, ids);
					return null;
				}
			};
			task.execute(ids);
		}
	}

    /**
     * Convenience method to complete tasks. Runs on the thread that called it.
     * @param context
     * @param completed
     * @param ids
     */
    public static void setCompletedSynced(final Context context,
            final boolean completed, final Long... ids) {
        if (ids.length < 1) {
            return;
        }

        final ContentValues values = new ContentValues();
        values.put(Columns.COMPLETED, completed ? Calendar
                .getInstance().getTimeInMillis() : null);
        values.put(Columns.UPDATED, Calendar.getInstance()
                .getTimeInMillis());
        String idStrings = "(";
        for (Long id : ids) {
            idStrings += id + ",";
        }
        idStrings = idStrings.substring(0, idStrings.length() - 1);
        idStrings += ")";
        context.getContentResolver().update(URI, values,
                Columns._ID + " IN " + idStrings, null);
    }

	public int moveTo(final ContentResolver resolver, final Task targetTask) {
		if (targetTask.dblist.longValue() == dblist.longValue()) {
			if (targetTask.left < left) {
				// moving left
				return resolver.update(getMoveItemLeftUri(),
						getMoveValues(targetTask.left), null, null);
			}
			else if (targetTask.right > right) {
				// moving right
				return resolver.update(getMoveItemRightUri(),
						getMoveValues(targetTask.right), null, null);
			}
		}
		return 0;
	}

	@Override
	protected String getTableName() {
		return TABLE_NAME;
	}

	/**
	 * Can't use unique constraint on positions because SQLite checks
	 * constraints after every row is updated an not after each statement like
	 * it should. So have to do the check in a trigger instead.
	 */
	static final String countVals(final String col, final String ver) {
		return String.format("SELECT COUNT(DISTINCT %2$s)"
				+ " AS ColCount FROM %1$s WHERE %3$s=%4$s.%3$s", TABLE_NAME,
				col, Columns.DBLIST, ver);
	}

	// verify that left are unique
	// count number of id and compare to number of left and right
	static final String posUniqueConstraint(final String ver, final String msg) {
		return String.format(
				" SELECT CASE WHEN ((%1$s) != (%2$s) OR (%1$s) != (%3$s)) THEN "
						+ " RAISE (ABORT, '" + msg + "')" + " END;",
				countVals(Columns._ID, ver), countVals(Columns.LEFT, ver),
				countVals(Columns.RIGHT, ver));
	};

	// public static final String TRIGGER_POST_UPDATE = String.format(
	// "CREATE TRIGGER task_post_update AFTER UPDATE ON %1$s BEGIN "
	// + posUniqueConstraint("new", "pos not unique post update")
	// + posUniqueConstraint("old", "pos not unique post update")
	// + " END;", TABLE_NAME);

	// Makes a gap in the list where the task is being inserted
	private static final String BUMP_TO_RIGHT = " UPDATE %1$s SET %2$s = %2$s + 2, %3$s = %3$s + 2 WHERE %3$s >= new.%3$s AND %4$s IS new.%4$s;";
	public static final String TRIGGER_PRE_INSERT = String.format(
			"CREATE TRIGGER task_pre_insert BEFORE INSERT ON %s BEGIN ",
			TABLE_NAME)
			+ String.format(BUMP_TO_RIGHT, TABLE_NAME, Columns.RIGHT,
					Columns.LEFT, Columns.DBLIST) + " END;";

	public static final String TRIGGER_POST_INSERT = String.format(
			"CREATE TRIGGER task_post_insert AFTER INSERT ON %s BEGIN ",
			TABLE_NAME)
			// Enforce integrity
			+ posUniqueConstraint("new", "pos not unique post insert")

			+ " END;";

	// Upgrades children and closes the gap made from the delete
	private static final String BUMP_TO_LEFT = " UPDATE %1$s SET %2$s = %2$s - 2 WHERE %2$s > old.%3$s AND %4$s IS old.%4$s;";
	// private static final String UPGRADE_CHILDREN =
	// " UPDATE %1$s SET %2$s = %2$s - 1, %3$s = %3$s - 1 WHERE %4$s IS old.%4$s AND %2$s BETWEEN old.%2$s AND old.%3$s;";
	public static final String TRIGGER_POST_DELETE = String.format(
			"CREATE TRIGGER task_post_delete AFTER DELETE ON %s BEGIN ",
			TABLE_NAME)
			// + String.format(UPGRADE_CHILDREN, TABLE_NAME, Columns.LEFT,
			// Columns.RIGHT, Columns.DBLIST)
			+ String.format(BUMP_TO_LEFT, TABLE_NAME, Columns.LEFT,
					Columns.RIGHT, Columns.DBLIST)
			+ String.format(BUMP_TO_LEFT, TABLE_NAME, Columns.RIGHT,
					Columns.RIGHT, Columns.DBLIST)

			// Enforce integrity
			+ posUniqueConstraint("old", "pos not unique post delete")

			+ " END;";

	public static final String TRIGGER_PRE_DELETE = String.format(
			"CREATE TRIGGER task_pre_delete BEFORE DELETE ON %1$s BEGIN "
					+ " INSERT INTO %2$s ("
					+ arrayToCommaString("", Columns.DELETEFIELDS_TRIGGER, "")
					+ ") "
					+ " VALUES("
					+ arrayToCommaString("old.", Columns.DELETEFIELDS_TRIGGER,
							"") + "); "

					+ " END;", TABLE_NAME, DELETE_TABLE_NAME);

	public String getSQLMoveItemLeft(final ContentValues values) {
		if (!values.containsKey(TARGETPOS)
				|| values.getAsLong(TARGETPOS) >= left) {
			return null;
		}
		return getSQLMoveItem(Columns.LEFT, values.getAsLong(TARGETPOS));
	}

	public String getSQLMoveItemRight(final ContentValues values) {
		if (!values.containsKey(TARGETPOS)
				|| values.getAsLong(TARGETPOS) <= right) {
			return null;
		}
		return getSQLMoveItem(Columns.RIGHT, values.getAsLong(TARGETPOS));
	}

	/*
	 * Trigger to move between lists
	 */
	public static final String TRIGGER_MOVE_LIST = new StringBuilder()
			.append("CREATE TRIGGER trigger_post_move_list_")
			.append(TABLE_NAME)
			.append(" AFTER UPDATE OF ")
			.append(Task.Columns.DBLIST)
			.append(" ON ")
			.append(Task.TABLE_NAME)
			.append(" WHEN old.")
			.append(Task.Columns.DBLIST)
			.append(" IS NOT new.")
			.append(Task.Columns.DBLIST)
			.append(" BEGIN ")
			// Bump everything to the right, except the item itself (in same
			// list)
			.append(String
					.format("UPDATE %1$s SET %2$s = %2$s + 2, %3$s = %3$s + 2 WHERE %4$s IS new.%4$s AND %5$s IS NOT new.%5$s;",
							TABLE_NAME, Columns.LEFT, Columns.RIGHT,
							Columns.DBLIST, Columns._ID))

			// Bump everything left in the old list, to the right of position
			.append(String
					.format("UPDATE %1$s SET %2$s = %2$s - 2, %3$s = %3$s - 2 WHERE %2$s > old.%3$s AND %4$s IS old.%4$s;",
							TABLE_NAME, Columns.LEFT, Columns.RIGHT,
							Columns.DBLIST))

			// Set positions to 1 and 2 for item
			.append(String
					.format("UPDATE %1$s SET %2$s = 1, %3$s = 2 WHERE %4$s IS new.%4$s;",
							TABLE_NAME, Columns.LEFT, Columns.RIGHT,
							Columns._ID))

			.append(posUniqueConstraint("new", "Moving list, new positions not unique/ordered"))
			.append(posUniqueConstraint("old", "Moving list, old positions not unique/ordered"))

			.append(" END;").toString();

	/**
	 * If moving left, then edgeCol is left and vice-versa. Values should come
	 * from getMoveValues
	 *
	 * 1 = table name 2 = left 3 = right 4 = edgecol 5 = old.left 6 = old.right
	 * 7 = target.pos (actually target.edgecol) 8 = dblist 9 = old.dblist
	 */
	private String getSQLMoveItem(final String edgeCol, final Long edgeVal) {
		boolean movingLeft = Columns.LEFT.equals(edgeCol);
		return String
				.format(new StringBuilder("UPDATE %1$s SET ")
						/*
						 * Left item follows Left = Left + ...
						 */
						.append("%2$s = %2$s + ")
						.append(" CASE ")
						// Moving item jumps to target pos
						.append(" WHEN %2$s IS %5$d ")
						// ex: left = 5, target = 2, --> left = 5 + (2 - 5) == 2
						// ex left = 5, target = 9(right), --> left = 5 + (9 - 5
						// - 1) = 8
						.append(" THEN ")
						.append(" (%7$d - %5$d")
						.append(movingLeft ? ") " : " -1) ")
						// Sub items take one step opposite
						// Careful if moving inside subtree, which can only
						// happen when moving right.
						// Then only left position changes
						.append(" WHEN %2$s BETWEEN (%5$d + 1) AND (%6$d - 1) ")
						.append(" THEN ")
						.append(movingLeft ? " 1 " : " -1 ")
						// Items in between from and to positions take two steps
						// opposite
						.append(" WHEN %2$s BETWEEN ")
						.append(movingLeft ? "%7$d" : "%6$d")
						.append(" AND ")
						.append(movingLeft ? "%5$d" : "%7$d")
						.append(" THEN ")
						.append(movingLeft ? " 2 " : " -2 ")
						// Not in target range, no change
						.append(" ELSE 0 END, ")
						/*
						 * Right item follows Right = Right + ...
						 */
						.append(" %3$s = %3$s + ")
						.append(" CASE ")
						// Moving item jumps to target pos
						.append(" WHEN %3$s IS %6$d ")
						// ex: right = 7, target = 3(left), --> right = 7 + (3 -
						// 7 + 1) == 4
						// ex right = 2, target = 9(right), --> right = 2 + (9 -
						// 2) = 9
						.append(" THEN ")
						.append(" (%7$d - %6$d")
						.append(movingLeft ? " +1) " : ") ")
						// Sub items take one step opposite
						.append(" WHEN %3$s BETWEEN (%5$d + 1) AND (%6$d - 1) ")
						.append(" THEN ")
						.append(movingLeft ? " 1 " : " -1 ")
						// Items in between from and to positions take two steps
						// opposite
						.append(" WHEN %3$s BETWEEN ")
						.append(movingLeft ? "%7$d" : "%6$d").append(" AND ")
						.append(movingLeft ? "%5$d" : "%7$d").append(" THEN ")
						.append(movingLeft ? " 2 " : " -2 ")
						// Not in target range, no change
						.append(" ELSE 0 END ")
						// And limit to the list in question
						.append(" WHERE %8$s IS %9$d;").toString(), TABLE_NAME,
						Columns.LEFT, Columns.RIGHT, edgeCol, left, right,
						edgeVal, Columns.DBLIST, dblist);
	}

	/*
	 * @SuppressLint("DefaultLocale") public String getSQLMoveSubTree(final
	 * ContentValues values) { return
	 * String.format("UPDATE %1$s SET %2$s = %2$s + " +
	 * 
	 * " CASE " + // Tasks are moving left " WHEN (%4$d < %6$d) " +
	 * 
	 * " THEN CASE " + " WHEN %2$s BETWEEN %4$d AND (%6$d - 1) " + // Then they
	 * must flow [width] to the right " THEN %7$d - %6$d + 1 " +
	 * " WHEN %2$s BETWEEN %6$d AND %7$d " + // Tasks in subtree jump to the
	 * left // targetleft - left " THEN %4$d - %6$d " + // Do nothing otherwise
	 * " ELSE 0 END " + // Tasks are moving right " WHEN (%4$d > %6$d) " +
	 * " THEN CASE " + " WHEN %2$s BETWEEN (%7$d + 1) AND %4$d " + // Then move
	 * them [width] to the left " THEN %6$d - %7$d - 1" +
	 * " WHEN %2$s BETWEEN %6$d AND %7$d " + // Tasks in subtree jump to the
	 * right // targetleft - left
	 * 
	 * // Depends on if we are moving inside a task or // moving an entire one
	 * " THEN CASE WHEN %5$d > (%4$d + 1) " + " THEN %4$d - %7$d " +
	 * " ELSE %4$d - %7$d + 1 END " + // Do nothing otherwise " ELSE 0 END " +
	 * // No move actually performed. comma to do right next " ELSE 0 END, " +
	 * 
	 * " %3$s = %3$s + " + " CASE " + // Tasks are moving left
	 * " WHEN (%4$d < %6$d) " +
	 * 
	 * " THEN CASE " + // but only if right is left of originleft
	 * " WHEN %3$s BETWEEN %4$d AND (%6$d - 1)" + // Then they must flow [width]
	 * to the right " THEN %7$d - %6$d + 1" +
	 * " WHEN %2$s BETWEEN %6$d AND %7$d " + // Tasks in subtree jump to the
	 * left // targetleft - left " THEN %4$d - %6$d " + // Do nothing otherwise
	 * " ELSE 0 END " + // Tasks are moving right " WHEN (%4$d > %6$d) " +
	 * " THEN CASE " + // when right is between myright + 1 and targetleft + 1
	 * " WHEN %3$s BETWEEN (%7$d + 1) AND (%4$d + 1) " + // Then move them
	 * [width] to the left " THEN %6$d - %7$d - 1" +
	 * " WHEN %2$s BETWEEN %6$d AND %7$d " + // targetleft - left // Depends on
	 * if we are moving inside a task or // moving an entire one
	 * " THEN CASE WHEN %5$d > (%4$d + 1) " + " THEN %4$d - %7$d  " +
	 * " ELSE %4$d - %7$d + 1 END " + // Do nothing otherwise " ELSE 0 END " +
	 * // No move actually performed. End update with semicolon " ELSE 0 END " +
	 * " WHERE %8$s IS %9$d; "
	 * 
	 * //Enforce integrity + posUniqueConstraint("new",
	 * "pos not unique move sub tree") ,
	 * 
	 * TABLE_NAME, Columns.LEFT, Columns.RIGHT, values.getAsLong(TARGETLEFT),
	 * values.getAsLong(TARGETRIGHT), left, right, Columns.DBLIST, dblist
	 * 
	 * ); }
	 */

	@Override
	public String getContentType() {
		return CONTENT_TYPE;
	}
} public class TaskList extends DAO {

	// SQL convention says Table name should be "singular"
	public static final String TABLE_NAME = "tasklist";
	public static final Uri URI = Uri.withAppendedPath(
			Uri.parse(MyContentProvider.SCHEME + MyContentProvider.AUTHORITY),
			TABLE_NAME);

	public static final String VIEWCOUNT_NAME = "lists_with_count";
	public static final Uri URI_WITH_COUNT = Uri.withAppendedPath(URI,
			VIEWCOUNT_NAME);

	public static Uri getUri(final long id) {
		return Uri.withAppendedPath(URI, Long.toString(id));
	}

	public static final String CONTENT_TYPE = "vnd.android.cursor.item/vnd.nononsenseapps.list";

	public static final int BASEURICODE = 101;
	public static final int BASEITEMCODE = 102;
	public static final int VIEWCOUNTCODE = 103;
	// Legacy support, these also need to use legacy projections
	public static final int LEGACYBASEURICODE = 111;
	public static final int LEGACYBASEITEMCODE = 112;
	public static final int LEGACYVISIBLEURICODE = 113;
	public static final int LEGACYVISIBLEITEMCODE = 114;

	/**
	 * TaskList URIs start at 101, up to 199
	 */
	public static void addMatcherUris(UriMatcher sURIMatcher) {
		sURIMatcher
				.addURI(MyContentProvider.AUTHORITY, TABLE_NAME, BASEURICODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY, TABLE_NAME + "/#",
				BASEITEMCODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY, TABLE_NAME + "/"
				+ VIEWCOUNT_NAME, VIEWCOUNTCODE);

		// Legacy URIs
		sURIMatcher.addURI(MyContentProvider.AUTHORITY,
				LegacyDBHelper.NotePad.Lists.LISTS, LEGACYBASEURICODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY,
				LegacyDBHelper.NotePad.Lists.LISTS + "/#", LEGACYBASEITEMCODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY,
				LegacyDBHelper.NotePad.Lists.VISIBLE_LISTS,
				LEGACYVISIBLEURICODE);
		sURIMatcher.addURI(MyContentProvider.AUTHORITY,
				LegacyDBHelper.NotePad.Lists.VISIBLE_LISTS + "/#",
				LEGACYVISIBLEITEMCODE);
	}

	public static class Columns implements BaseColumns {

		private Columns() {
		}

		public static final String TITLE = "title";
		public static final String UPDATED = "updated";
		public static final String LISTTYPE = "tasktype";
		public static final String SORTING = "sorting";

		public static final String VIEW_COUNT = "count";

		// public static final String GTASKACCOUNT = "gtaskaccount";
		// public static final String GTASKID = "gtaskid";
		//
		// // Future proofing
		// public static final String DROPBOXACCOUNT = "dropboxaccount";
		// public static final String DROPBOXID = "dropboxid";

		public static final String[] FIELDS = { _ID, TITLE, UPDATED, LISTTYPE,
				SORTING };
		// GTASKACCOUNT, GTASKID, DROPBOXACCOUNT, DROPBOXID };
		public static final String[] SHALLOWFIELDS = { _ID, TITLE, UPDATED };
	}

	public static final String CREATE_TABLE = new StringBuilder("CREATE TABLE ")
			.append(TABLE_NAME).append("(").append(Columns._ID)
			.append(" INTEGER PRIMARY KEY,").append(Columns.TITLE)
			.append(" TEXT NOT NULL DEFAULT '',").append(Columns.UPDATED)
			.append(" INTEGER,").append(Columns.LISTTYPE)
			.append(" TEXT DEFAULT NULL,").append(Columns.SORTING)
			.append(" TEXT DEFAULT NULL").append(")").toString();

	public static final String CREATE_COUNT_VIEW = new StringBuilder(
			"CREATE TEMP VIEW IF NOT EXISTS ")
			.append(VIEWCOUNT_NAME)
			.append(" AS SELECT ")
			.append(arrayToCommaString(Columns.FIELDS))
			.append(",")
			.append(Columns.VIEW_COUNT)
			.append(" FROM ")
			.append(TABLE_NAME)
			.append(" LEFT JOIN ")
			// Select count statement
			.append(" (SELECT COUNT(1) AS ").append(Columns.VIEW_COUNT)
			.append(",").append(Task.Columns.DBLIST).append(" FROM ")
			.append(Task.TABLE_NAME).append(" WHERE ")
			.append(Task.Columns.COMPLETED).append(" IS NULL ")
			.append(" GROUP BY ").append(Task.Columns.DBLIST).append(") ")
			.append(" ON ").append(TABLE_NAME).append(".").append(Columns._ID)
			.append(" = ").append(Task.Columns.DBLIST).append(";").toString();

	public String title = "";

	// milliseconds since 1970-01-01 UTC
	public Long updated = null;

	// Null, use global prefs
	public String listtype = null;
	public String sorting = null;

	// Sync stuff
	// public String gtaskaccount = null;
	// public String gtaskid = null;
	// public String dropboxaccount = null;
	// public String dropboxid = null;

	public TaskList() {
	}

	public TaskList(final Cursor c) {
		this._id = c.getLong(0);
		this.title = c.getString(1);
		this.updated = c.getLong(2);
		this.listtype = c.getString(3);
		this.sorting = c.getString(4);
		// sync stuff
		// gtaskaccount = c.getString(3);
		// gtaskid = c.getString(4);
		// dropboxaccount = c.getString(5);
		// dropboxid = c.getString(6);
	}

	public TaskList(final Uri uri, final ContentValues values) {
		this(Long.parseLong(uri.getLastPathSegment()), values);
	}

	public TaskList(final long id, final ContentValues values) {
		this(values);
		this._id = id;
	}

	public TaskList(final JSONObject json) throws JSONException {
		if (json.has(Columns.TITLE))
			title = json.getString(Columns.TITLE);
		if (json.has(Columns.UPDATED))
			updated = json.getLong(Columns.UPDATED);
		if (json.has(Columns.LISTTYPE))
			listtype = json.getString(Columns.LISTTYPE);
		if (json.has(Columns.SORTING))
			sorting = json.getString(Columns.SORTING);
	}

	public TaskList(final ContentValues values) {
		title = values.getAsString(Columns.TITLE);
		updated = values.getAsLong(Columns.UPDATED);
		listtype = values.getAsString(Columns.LISTTYPE);
		sorting = values.getAsString(Columns.SORTING);

		// gtaskaccount = values.getAsString(Columns.GTASKACCOUNT);
		// gtaskid = values.getAsString(Columns.GTASKID);
		// dropboxaccount = values.getAsString(Columns.DROPBOXACCOUNT);
		// dropboxid = values.getAsString(Columns.DROPBOXID);
	}

	public ContentValues getContent() {
		final ContentValues values = new ContentValues();
		// Note that ID is NOT included here
		values.put(Columns.TITLE, title);
		values.put(Columns.UPDATED, updated);
		values.put(Columns.LISTTYPE, listtype);
		values.put(Columns.SORTING, sorting);

		// values.put(Columns.GTASKACCOUNT, gtaskaccount);
		// values.put(Columns.GTASKID, gtaskid);
		// values.put(Columns.DROPBOXACCOUNT, dropboxaccount);
		// values.put(Columns.DROPBOXID, dropboxid);

		return values;
	}

	@Override
	protected String getTableName() {
		return TABLE_NAME;
	}

	@Override
	public String getContentType() {
		return CONTENT_TYPE;
	}

	@Override
	public int save(final Context context) {
		return save(context, Calendar.getInstance().getTimeInMillis());
	}

	public int save(final Context context, final long updateTime) {
		int result = 0;
		updated = updateTime;
		if (_id < 1) {
			final Uri uri = context.getContentResolver().insert(getBaseUri(),
					getContent());
			if (uri != null) {
				_id = Long.parseLong(uri.getLastPathSegment());
				result++;
			}
		} else {
			result += context.getContentResolver().update(getUri(),
					getContent(), null, null);
		}
		return result;
	}
} public interface DialogConfirmedListener {
    void onConfirm();
} public class DialogDeleteTask extends DialogConfirmBase {
	static final String ID = "id";
	static final String TAG = "deletetaskok";

	public static void showDialog(final FragmentManager fm, final long taskId,
			final DialogConfirmedListener listener) {
		DialogDeleteTask d = new DialogDeleteTask();
		d.setListener(listener);
		Bundle args = new Bundle();
		args.putLong(ID, taskId);
		d.setArguments(args);
		d.show(fm, TAG);
	}

	@Override
	public int getTitle() {
		return R.string.delete_question;
	}

	@Override
	public int getMessage() {
		return R.string.delete_item_message;
	}

	@Override
	public void onOKClick() {
		if (getArguments().getLong(ID, -1) > 0) {
			if (0 < getActivity().getContentResolver().delete(
					Task.getUri(getArguments().getLong(ID, -1)), null, null)) {
				Toast.makeText(getActivity(), R.string.deleted,
						Toast.LENGTH_SHORT).show();
			}
		}
		if (listener != null) {
			listener.onConfirm();
		}
		getDialog().dismiss();
	}

} public class DialogPassword extends DialogFragment {

	PasswordConfirmedListener listener = null;
	private FragmentDialogPasswordBinding binding;

	public void setListener(final PasswordConfirmedListener listener) {
		this.listener = listener;
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		final SharedPreferences settings = PreferenceManager
				.getDefaultSharedPreferences(getActivity());
		final String currentPassword = settings.getString(
				DialogPasswordSettings.KEY_PASSWORD, "");
		if (currentPassword.isEmpty()) {
			getDialog().setTitle(R.string.enter_new_password);
		}
		else {
			getDialog().setTitle(R.string.password_required);
		}

		binding = DataBindingUtil.inflate(inflater, R.layout.fragment_dialog_password, container, true);
		return binding.getRoot();
	}

	@Override
	public void onActivityCreated(Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);
		showField();
	}

	public void showField() {
		final SharedPreferences settings = PreferenceManager
				.getDefaultSharedPreferences(getActivity());
		final String currentPassword = settings.getString(
				DialogPasswordSettings.KEY_PASSWORD, "");
		if (currentPassword.isEmpty()) {
			binding.passwordVerificationField.setVisibility(View.VISIBLE);
		}
		else {
			binding.passwordVerificationField.setVisibility(View.GONE);
		}

        binding.passwordField.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }

            @Override
            public void afterTextChanged(Editable s) {
                binding.buttons.dialogYes.setEnabled(s.length() > 0);
            }
        });

        binding.buttons.dialogNo.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                dismiss();
            }
        });

        binding.buttons.dialogYes.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                confirm();
            }
        });
	}

	void confirm() {
		final SharedPreferences settings = PreferenceManager
				.getDefaultSharedPreferences(getActivity());
		final String currentPassword = settings.getString(DialogPasswordSettings.KEY_PASSWORD,
				"");
		final String enteredPassword = binding.passwordField.getText().toString();
		final String verifiedPassword = binding.passwordVerificationField.getText()
				.toString();

		if (currentPassword.isEmpty()) {
			setPassword(enteredPassword, verifiedPassword);
		}
		else {
			// We want to return true or false, user has entered correct
			// password
			checkPassword(enteredPassword, currentPassword);
		}
	}

	private void checkPassword(final String enteredPassword,
			final String currentPassword) {
		if (currentPassword.equals(enteredPassword)) {
			if (listener != null) {
				listener.onPasswordConfirmed();
			}
			dismiss();
		}
		else {
			Animation shake = AnimationUtils.loadAnimation(getActivity(),
					R.anim.shake);
			binding.passwordField.startAnimation(shake);
			Toast.makeText(getActivity(), getText(R.string.password_incorrect),
					Toast.LENGTH_SHORT).show();
		}
	}

	private void setPassword(final String pass1, final String pass2) {
		if (pass1 != null && !pass1.isEmpty() && pass1.equals(pass2)) {
			PreferenceManager.getDefaultSharedPreferences(getActivity()).edit()
					.putString(DialogPasswordSettings.KEY_PASSWORD, pass1).commit();
			if (listener != null) {
				listener.onPasswordConfirmed();
			}
			dismiss();
		}
		else {
			Animation shake = AnimationUtils.loadAnimation(getActivity(),
					R.anim.shake);
			binding.passwordVerificationField.startAnimation(shake);
			Toast.makeText(getActivity(),
					getText(R.string.passwords_dont_match), Toast.LENGTH_SHORT)
					.show();
		}
	}

	public interface PasswordConfirmedListener {
		void onPasswordConfirmed();
	}
} public interface PasswordConfirmedListener {
	void onPasswordConfirmed();
} public class NotificationItemHelper {

	private static String getDateString(final Context context, final long time) {
		return TimeFormatter.getDateFormatter(context).format(new Date(time));
	}

	private static void hideViews(final View... views) {
		for (View v : views) {
			v.setVisibility(View.GONE);
		}
	}

	private static void showViews(final View... views) {
		for (View v : views) {
			v.setVisibility(View.VISIBLE);
		}
	}

	private static void setTime(final Context context, final Notification not,
			final Task mTask) {
		final GregorianCalendar cal = TimeFormatter.getLocalCalendar(context);
		// Start with date, either due date or today (default)
        // If due date is in the past, default to today + 1hour
		if (mTask.due != null && mTask.due > cal.getTimeInMillis()) {
			cal.setTimeInMillis(mTask.due);
		} else {
			// Default to today, set time one hour from now
			cal.add(Calendar.HOUR_OF_DAY, 1);
		}

		// And set time on notification
		not.time = cal.getTimeInMillis();
	}

	public static void setup(final TaskDetailFragment fragment,
			final LinearLayout notificationList, final View nv,
			final Notification not, final Task mTask) {

		// Set time on notification if not set already
		if (not.time == null) {
			setTime(fragment.getActivity(), not, mTask);
		}

		// Set time text
		final TextView notTimeButton = (TextView) nv
				.findViewById(R.id.notificationTime);
		notTimeButton.setText(not.getLocalTimeText(fragment.getActivity()));

		// Set date text
		final TextView notDateButton = (TextView) nv
				.findViewById(R.id.notificationDate);
		notDateButton.setText(getDateString(fragment.getActivity(), not.time));

		final View notRemoveButton = nv.findViewById(R.id.notificationRemove);

		// Remove button
		notRemoveButton.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				if (!fragment.isLocked()) {
					// Remove row from UI
					notificationList.removeView((View) v.getParent());
					// Remove from database and renotify
					not.delete(fragment.getActivity());
				}
			}
		});

		// Date button
		notDateButton.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				if (!fragment.isLocked()) {

					final Calendar localTime = Calendar.getInstance();
					if (not.time != null) {
						localTime.setTimeInMillis(not.time);
					}

					final DatePickerDialog datedialog = DatePickerDialog
							.newInstance(new OnDateSetListener() {
								@Override
								public void onDateSet(DatePickerDialog dialog,
										int year, int monthOfYear,
										int dayOfMonth) {
									localTime.set(Calendar.YEAR, year);
									localTime.set(Calendar.MONTH, monthOfYear);
									localTime.set(Calendar.DAY_OF_MONTH, dayOfMonth);
									
									not.time = localTime.getTimeInMillis();
									notDateButton.setText(not.getLocalDateText(fragment
									.getActivity()));
									not.save(fragment.getActivity(), true);
								}
							}, localTime.get(Calendar.YEAR), localTime
									.get(Calendar.MONTH), localTime
									.get(Calendar.DAY_OF_MONTH));
					
					datedialog.show(fragment.getFragmentManager(), "date");

					// final DialogCalendar datePicker;
					//
					// if (mTask != null && mTask.due != null) {
					// datePicker = DialogCalendar.getInstance(mTask.due);
					// }
					// else {
					// datePicker = DialogCalendar.getInstance();
					// }
					// datePicker.setListener(new DateSetListener() {
					//
					// @Override
					// public void onDateSet(long time) {
					// final Calendar localTime = Calendar.getInstance();
					// localTime.setTimeInMillis(time);
					// if (not.time != null) {
					// final Calendar notTime = Calendar.getInstance();
					// notTime.setTimeInMillis(not.time);
					// localTime.set(Calendar.HOUR_OF_DAY,
					// notTime.get(Calendar.HOUR_OF_DAY));
					// localTime.set(Calendar.MINUTE,
					// notTime.get(Calendar.MINUTE));
					// }
					//
					// not.time = localTime.getTimeInMillis();
					// notDateButton.setText(not.getLocalDateText(fragment
					// .getActivity()));
					// not.save(fragment.getActivity(), true);
					// }
					// });
					//
					// datePicker.show(fragment.getFragmentManager(), "date");

				}
			}
		});
		// Time button
		notTimeButton.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				if (!fragment.isLocked()) {
					// Display time picker
					final Calendar localTime = Calendar.getInstance();
					if (not.time != null) {
						localTime.setTimeInMillis(not.time);
					}

					final TimePickerDialog timedialog = fragment
							.getTimePickerDialog();
					timedialog.setStartTime(
							localTime.get(Calendar.HOUR_OF_DAY),
							localTime.get(Calendar.MINUTE));
					timedialog.setOnTimeSetListener(new OnTimeSetListener() {
						@Override
						public void onTimeSet(RadialPickerLayout view,
								int hourOfDay, int minute) {
							localTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
							localTime.set(Calendar.MINUTE, minute);

							not.time = localTime.getTimeInMillis();

							notTimeButton.setText(not.getLocalTimeText(fragment
									.getActivity()));

							not.save(fragment.getActivity(), true);
						}
					});

					timedialog.show(fragment.getFragmentManager(), "time");

					// // Now display time picker
					// final TimePickerDialogFragment timePicker = fragment
					// .getTimePickerFragment();
					// timePicker.setListener(new TimePickerDialogHandler() {
					// @Override
					// public void onDialogTimeSet(int hourOfDay, int minute) {
					// final Calendar localTime = Calendar.getInstance();
					// if (not.time != null) {
					// localTime.setTimeInMillis(not.time);
					// }
					// localTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
					// localTime.set(Calendar.MINUTE, minute);
					//
					// not.time = localTime.getTimeInMillis();
					//
					// notTimeButton.setText(not.getLocalTimeText(fragment
					// .getActivity()));
					//
					// not.save(fragment.getActivity(), true);
					// }
					//
					// @Override
					// public void onDialogTimeCancel() {
					// }
					//
					// });
					//
					// timePicker.show(fragment.getFragmentManager(), "time");
				}
			}
		});

		WeekDaysView days = ((WeekDaysView) nv.findViewById(R.id.weekdays));
		days.setCheckedDays(not.repeats);
		days.setOnCheckedDaysChangedListener(new onCheckedDaysChangeListener() {

			@Override
			public void onChange(final long checkedDays) {
				not.repeats = checkedDays;
				not.saveInBackground(fragment.getActivity(), true);
			}
		});
	}
} public class MainPrefs extends PreferenceFragment {
    public static final String KEY_THEME = "preference_theme";
    // public static final String KEY_WEEK_START_DAY =
	// "preferences_week_start_day";

	public static final String SANS = "Sans";
	public static final String SERIF = "Serif";
	public static final String MONOSPACE = "Monospace";

	// public static final String THEME_DARK = "dark";
	// public static final String THEME_BLACK = "black";
	// public static final String THEME_LIGHT_ICS_AB = "light_ab";

	public static final String WEEK_START_DEFAULT = "-1";
	public static final String WEEK_START_SATURDAY = "7";
	public static final String WEEK_START_SUNDAY = "1";
	public static final String WEEK_START_MONDAY = "2";

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		// Load the preferences from an XML resource
		addPreferencesFromResource(R.xml.app_pref_main);

		// Fill listpreferences
        /*setLangEntries((ListPreference) findPreference(getString(R.string.pref_locale)));
*//*		setDateEntries(
                (ListPreference) findPreference(getString(R.string.key_pref_dateformat_short)),
				R.array.dateformat_short_values);
		setDateEntries(
				(ListPreference) findPreference(getString(R.string.key_pref_dateformat_long)),
				R.array.dateformat_long_values);*//*

		// Bind summaries
		// PrefsActivity
		// .bindPreferenceSummaryToValue(findPreference(KEY_FONT_TYPE_EDITOR));
		// PrefsActivity
		// .bindPreferenceSummaryToValue(findPreference(KEY_WEEK_START_DAY));
		PrefsActivity
				.bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_locale)));

		PrefsActivity
				.bindPreferenceSummaryToValue(findPreference(getString(R.string.key_pref_dateformat_long)));
		PrefsActivity
				.bindPreferenceSummaryToValue(findPreference(getString(R.string.key_pref_dateformat_short)));
		PrefsActivity
				.bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_editor_title_fontfamily)));
		PrefsActivity
				.bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_editor_title_fontstyle)));
		PrefsActivity
				.bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_editor_body_fontfamily)));
		PrefsActivity
				.bindPreferenceSummaryToValue(findPreference(getString(R.string.pref_editor_fontsize)));
        PrefsActivity.bindPreferenceSummaryToValue(findPreference(KEY_THEME));*/
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle
            savedInstanceState) {
        View layout = inflater.inflate(R.layout.activity_settings, container, false);
        if (layout != null) {
            PrefsActivity activity = (PrefsActivity) getActivity();
            Toolbar toolbar = (Toolbar) layout.findViewById(R.id.toolbar);
            activity.setSupportActionBar(toolbar);

            ActionBar actionBar = activity.getSupportActionBar();
            if (actionBar != null) {
                actionBar.setDisplayHomeAsUpEnabled(true);
                actionBar.setTitle(getPreferenceScreen().getTitle());
            }
        }
        return layout;
    }

	private void setDateEntries(ListPreference prefDate, int array) {
		final String[] values = getResources().getStringArray(array);

		final ArrayList<CharSequence> entries = new ArrayList<CharSequence>();

		final GregorianCalendar cal = new GregorianCalendar(2099, 2, 27, 0, 59);

		for (final String val : values) {
			entries.add(TimeFormatter.getLocalDateString(getActivity(), val,
					cal.getTimeInMillis()));
		}

		prefDate.setEntries(entries.toArray(new CharSequence[entries.size()]));
		prefDate.setEntryValues(values);
	}

	private void setLangEntries(ListPreference prefLang) {
		ArrayList<CharSequence> entries = new ArrayList<CharSequence>();
		ArrayList<CharSequence> values = new ArrayList<CharSequence>();

		entries.add(getString(R.string.localedefault));
		values.add("");

		String[] langs = getResources()
				.getStringArray(R.array.translated_langs);

		for (String lang : langs) {
			Locale l;
			if (lang.length() == 5) {
				l = new Locale(lang.substring(0, 2), lang.substring(3, 5));
			}
			else {
				l = new Locale(lang.substring(0, 2));
			}

			entries.add(l.getDisplayName(l));
			values.add(lang);
		}
		prefLang.setEntries(entries.toArray(new CharSequence[entries.size()]));
		prefLang.setEntryValues(values.toArray(new CharSequence[values.size()]));

		// Set summary
		prefLang.setSummary(prefLang.getEntry());
	}
} public class FragmentHelper {

    /**
     * Run an action using a Handler. Useful to commit fragment changes during OnLoadFinished
     * for example, where it is typically not allowed.
     *
     * @param runnable to run in handler
     */
    static public void handle(@NonNull Runnable runnable) {
        Handler handler = new Handler();
        handler.post(runnable);
    }
} public class SharedPreferencesHelper {
    private static SharedPreferences Prefs(@NonNull Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context);
    }

    private static String S(@NonNull Context context, @StringRes int id) {
        return context.getString(id);
    }

    public static boolean isSdSyncEnabled(@NonNull Context context) {
        return Prefs(context).getBoolean(S(context, R.string.const_preference_sdcard_enabled_key)
                , false);
    }

    public static void disableSdCardSync(@NonNull Context context) {
        Prefs(context).edit().putBoolean(S(context, R.string.const_preference_sdcard_enabled_key)
                , false).apply();
    }

    public static String getSdDir(@NonNull Context context) {
        return Prefs(context).getString(S(context, R.string.const_preference_sdcard_dir_key),
                SDSynchronizer.DEFAULT_ORG_DIR);
    }

    public static boolean isDropboxSyncEnabled(@NonNull Context context) {
        return Prefs(context).getBoolean(S(context, R.string
                .const_preference_dropbox_enabled_key), false);
    }

    public static void disableDropboxSync(@NonNull Context context) {
        Prefs(context).edit().putBoolean(S(context, R.string
                .const_preference_dropbox_enabled_key), false)
                .remove(S(context, R.string.const_preference_dropbox_account_key))
                .apply();
        DropboxSyncHelper dropboxSyncHelper = new DropboxSyncHelper(context);
        dropboxSyncHelper.unlinkAccount();
    }

    public static String getDropboxAccount(@NonNull Context context) {
        return Prefs(context).getString(S(context, R.string.const_preference_dropbox_account_key),
                "");
    }

    public static String getDropboxDir(@NonNull Context context) {
        return Prefs(context).getString(S(context, R.string.const_preference_dropbox_dir_key),
                DropboxSynchronizer.DEFAULT_DIR);
    }

    public static boolean isCurrentThemeLight(@NonNull Context context) {
        final String selectedTheme = getCurrentTheme(context, "");

        final String dark = S(context, R.string.const_preference_theme_dark);
        final String black = S(context, R.string.const_preference_theme_black);

       return !dark.equals(selectedTheme) && !black.equals(selectedTheme);
    }

    public static @NonNull String getCurrentTheme(@NonNull Context context, @NonNull String defaultValue) {
        final String key = S(context, R.string.const_preference_theme_key);
        return Prefs(context).getString(key, defaultValue);
    }

    public static void setSortingDue(@NonNull Context context) {
        putString(context, R.string.pref_sorttype, R.string.const_duedate);
    }

    public static void setSortingManual(@NonNull Context context) {
        putString(context, R.string.pref_sorttype, R.string.const_possubsort);
    }
    public static void setSortingAlphabetic(Context context) {
        putString(context, R.string.pref_sorttype, R.string.const_alphabetic);
    }

    public static void putString(@NonNull Context context, @StringRes int key, @StringRes int value) {
        put(context, S(context, key), S(context, value));
    }

    public static void put(@NonNull Context context, @NonNull String key, @NonNull String value) {
        Prefs(context).edit().putString(key, value).apply();
    }

    public static void put(@NonNull Context context, @NonNull String key, boolean value) {
        Prefs(context).edit().putBoolean(key, value).apply();
    }

    public static void put(@NonNull Context context, @StringRes int key, boolean value) {
        put(context, S(context, key), value);
    }

    public static @NonNull String getGoogleAccount(@NonNull Context context) {
        return Prefs(context).getString(S(context,
                R.string.const_preference_gtask_account_key), "");
    }

    public static boolean isPasswordSet(@NonNull Context context) {
        return !Prefs(context).getString(DialogPasswordSettings.KEY_PASSWORD, "").isEmpty();
    }

    public static void setDropboxAccount(@NonNull Context context, @NonNull String account) {
        put(context, S(context, R.string.const_preference_dropbox_account_key), account);
    }
} public class TimeFormatter {

	public static String WEEKDAY_SHORTEST_FORMAT = "E";

	public static Locale getLocale(final String lang) {
		final Locale locale;
		if (lang == null || lang.isEmpty()) {
			locale = Locale.getDefault();
		}
		else if (lang.length() == 5) {
			locale = new Locale(lang.substring(0, 2), lang.substring(3, 5));
		}
		else {
			locale = new Locale(lang.substring(0, 2));
		}
		return locale;
	}

	/**
	 * Formats date according to the designated locale
	 */
	public static String getLocalDateString(final Context context,
			final String lang, final String format, final long timeInMillis) {
		return getLocalFormatter(context, lang, format).format(
				new Date(timeInMillis));
	}

	/**
	 * Formats the date according to the locale the user has defined in settings
	 */
	public static String getLocalDateString(final Context context,
			final String format, final long timeInMillis) {
		return getLocalDateString(
				context,
				PreferenceManager.getDefaultSharedPreferences(context)
						.getString(context.getString(R.string.const_preference_locale_key), ""),
				format, timeInMillis);
	}

	/**
	 * Dont use for performance critical settings
	 */
	public static String getLocalDateStringLong(final Context context,
			final long time) {
		return getLocalFormatterLong(context).format(new Date(time));
	}

	public static String getLocalDateOnlyStringLong(final Context context,
			final long time) {
		return getLocalFormatterLongDateOnly(context).format(new Date(time));
	}

	public static String getLocalTimeOnlyString(final Context context,
			final long time) {
		final String format;
		if (android.text.format.DateFormat.is24HourFormat(context)) {
			// 00:59
			format = "HH:mm";
		}
		else {
			// 12:59 am
			format = "h:mm a";
		}
		return new SimpleDateFormat(format, getLocale(PreferenceManager
				.getDefaultSharedPreferences(context).getString(
						context.getString(R.string.const_preference_locale_key), "")))
				.format(new Date(time));
	}

	/**
	 * Dont use for performance critical settings
	 */
	public static String getLocalDateStringShort(final Context context,
			final long time) {
		return getLocalFormatterShort(context).format(new Date(time));
	}

	/**
	 * Replaces first "localtime" in format string to a time which respects
	 * global 24h setting.
	 */
	private static String withSuitableTime(final Context context,
			final String formatString) {
		if (android.text.format.DateFormat.is24HourFormat(context)) {
			// 00:59
			return formatString.replaceFirst("localtime", "HH:mm");
		}
		else {
			// 12:59 am
			return formatString.replaceFirst("localtime", "h:mm a");
		}
	}

	/**
	 * Removes "localtime" in format string
	 */
	private static String withSuitableDateOnly(final Context context,
			final String formatString) {
		return formatString.replaceAll("\\s*localtime\\s*", " ");
	}

	private static SimpleDateFormat getLocalFormatter(final Context context,
			final String lang, final String format) {
		final Locale locale = getLocale(lang);
		return new SimpleDateFormat(withSuitableTime(context, format), locale);
	}

	public static GregorianCalendar getLocalCalendar(final Context context) {
		final Locale locale = getLocale(PreferenceManager
				.getDefaultSharedPreferences(context).getString(
						context.getString(R.string.const_preference_locale_key), ""));
		return new GregorianCalendar(locale);
	}

	/**
	 * Good for performance critical situations, like lists
	 */
	public static SimpleDateFormat getLocalFormatterLong(final Context context) {
        // TODO
		return getLocalFormatter(
				context,
				PreferenceManager.getDefaultSharedPreferences(context)
						.getString(context.getString(R.string.const_preference_locale_key), ""),
				withSuitableTime(
						context,
                        context.getString(R.string.dateformat_long_1)));
	}

	public static SimpleDateFormat getDateFormatter(final Context context) {
		return getLocalFormatter(
				context,
				PreferenceManager.getDefaultSharedPreferences(context)
						.getString(context.getString(R.string.const_preference_locale_key), ""),
				context.getString(R.string.dateformat_just_date));
	}

	public static SimpleDateFormat getLocalFormatterLongDateOnly(
			final Context context) {
		return getLocalFormatter(
				context,
				PreferenceManager.getDefaultSharedPreferences(context)
						.getString(context.getString(R.string.const_preference_locale_key), ""),
				withSuitableDateOnly(
						context,
										context.getString(R.string.dateformat_long_1)));
	}

	/**
	 * Good for performance critical situations, like lists
	 */
	public static SimpleDateFormat getLocalFormatterShort(final Context context) {
		return getLocalFormatter(
				context,
				PreferenceManager.getDefaultSharedPreferences(context)
						.getString(context.getString(R.string.const_preference_locale_key), ""),
				withSuitableTime(
						context,
										context.getString(R.string.dateformat_short_1)));
	}

	public static SimpleDateFormat getLocalFormatterShortDateOnly(
			final Context context) {
		return getLocalFormatter(
				context,
				PreferenceManager.getDefaultSharedPreferences(context)
						.getString(context.getString(R.string.const_preference_locale_key), ""),
				withSuitableDateOnly(
						context,
										context.getString(R.string.dateformat_short_1)));
	}

	public static SimpleDateFormat getLocalFormatterMicro(final Context context) {
		return getLocalFormatter(
				context,
				PreferenceManager.getDefaultSharedPreferences(context)
						.getString(context.getString(R.string.const_preference_locale_key), ""), withSuitableTime(context, context.getString(R.string.dateformat_micro)));
	}

	/**
	 * Good for performance critical situations, like lists
	 */
	public static SimpleDateFormat getLocalFormatterWeekday(
			final Context context) {
		return getLocalFormatter(
				context,
				PreferenceManager.getDefaultSharedPreferences(context)
						.getString(context.getString(R.string.const_preference_locale_key), ""),
				context.getString(R.string.dateformat_weekday));
	}

	/**
	 * Good for performance critical situations, like lists
	 */
	public static SimpleDateFormat getLocalFormatterWeekdayShort(
			final Context context) {
		return getLocalFormatter(
				context,
				PreferenceManager.getDefaultSharedPreferences(context)
						.getString(context.getString(R.string.const_preference_locale_key), ""),
				WEEKDAY_SHORTEST_FORMAT);
	}
} public static long getARealList(final Context context, final long tempList) {
    long returnList = tempList;

    if (returnList < 1 && returnList != TaskListFragment.LIST_ID_ALL) {
        // Then check if a default list is specified
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        returnList = Long.parseLong(prefs.getString(context.getString(R.string
                .pref_defaultlist), "-1"));
    }

    if (returnList > 0) {
        // See if it exists
        final Cursor c = context.getContentResolver().query(TaskList.URI, TaskList.Columns
                .FIELDS, TaskList.Columns._ID + " IS ?", new String[]{Long.toString
                (returnList)}, null);
        if (c != null) {
            if (c.moveToFirst()) {
                returnList = c.getLong(0);
            } else {
                returnList = -1;
            }
            c.close();
        }
    }

    if (returnList < 1) {
        // Fetch a valid list from database if previous attempts are invalid
        final Cursor c = context.getContentResolver().query(TaskList.URI, TaskList.Columns
                .FIELDS, null, null, context.getResources().getString(R.string
                .const_as_alphabetic, TaskList.Columns.TITLE));
        if (c != null) {
            if (c.moveToFirst()) {
                returnList = c.getLong(0);
            }
            c.close();
        }
    }

    return returnList;
}
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.view.ActionProvider;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.NestedScrollView;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Toast;
import com.android.datetimepicker.date.DatePickerDialog;
import com.android.datetimepicker.date.DatePickerDialog.OnDateSetListener;
import com.android.datetimepicker.time.TimePickerDialog;
import com.nononsenseapps.notepad.R;
import com.nononsenseapps.notepad.data.model.sql.Notification;
import com.nononsenseapps.notepad.data.model.sql.Task;
import com.nononsenseapps.notepad.data.model.sql.TaskList;
import com.nononsenseapps.notepad.ui.base.DialogConfirmBase.DialogConfirmedListener;
import com.nononsenseapps.notepad.ui.common.DialogDeleteTask;
import com.nononsenseapps.notepad.ui.common.DialogPassword;
import com.nononsenseapps.notepad.ui.common.DialogPassword.PasswordConfirmedListener;
import com.nononsenseapps.notepad.ui.common.MenuStateController;
import com.nononsenseapps.notepad.ui.common.NotificationItemHelper;
import com.nononsenseapps.notepad.ui.common.StyledEditText;
import com.nononsenseapps.notepad.ui.settings.MainPrefs;
import com.nononsenseapps.notepad.util.FragmentHelper;
import com.nononsenseapps.notepad.util.SharedPreferencesHelper;
import com.nononsenseapps.notepad.util.TimeFormatter;
import java.util.Calendar;
import static com.nononsenseapps.notepad.util.ListHelper.getARealList;
				mListener.addTaskInList("", mTask.dblist);
			}
			return true;
		} else */
        if (itemId == R.id.menu_revert) {
            // set to null to prevent modifications
            mTask = null;
            // Request a close from activity
            if (mListener != null) {
                mListener.closeEditor(this);
            }
            return true;
        } else if (itemId == R.id.menu_delete) {
            if (mTask != null) {
                if (isLocked()) {
                    DialogPassword delpf = new DialogPassword();
                    delpf.setListener(new PasswordConfirmedListener() {
                        @Override
                        public void onPasswordConfirmed() {
                            deleteAndClose();
                        }
                    });
                    delpf.show(getFragmentManager(), "delete_verify");
                } else {
                    deleteAndClose();
                }
            }
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private void deleteAndClose() {
        if (mTask != null && mTask._id > 0 && !isLocked()) {
            DialogDeleteTask.showDialog(getFragmentManager(), mTask._id, new
                    DialogConfirmedListener() {
                @Override
                public void onConfirm() {
                    // Prevents save attempts
                    mTask = null;
                    // Request a close from activity
                    // Todo let listener handle delete, and use Snack bar.
                    if (mListener != null) {
                        mListener.closeEditor(TaskDetailFragment.this);
                    }
                }
            });
        } else {
            // Prevents save attempts
            mTask = null;
            // Request a close from activity
            if (mListener != null) {
                mListener.closeEditor(TaskDetailFragment.this);
            }
        }
    }

    private void saveTask() {
        // if mTask is null, the task has been deleted or cancelled
        // If the task is not unlocked, editing is disabled
        if (mTask != null && !isLocked()) {
            // Needed for comparison
            mTask.setText(taskText.getText().toString());
            // if new item, only save if something has been entered
            if ((mTask._id > 0 && !mTask.equals(mTaskOrg)) || (mTask._id == -1 && isThereContent
                    ())) {
                // mTask.setText(taskText.getText().toString());
                mTask.save(getActivity());
                // Set the intent to open the task.
                // So we dont create a new one on rotation for example
                fixIntent();

                // TODO, should restart notification loader for new tasks
            }
        }
    }

    void fixIntent() {
        stateId = mTask._id;
        stateListId = mTask.dblist;

        if (getActivity() == null)
            return;

        final Intent orgIntent = getActivity().getIntent();
        if (orgIntent == null || orgIntent.getAction() == null || !orgIntent.getAction().equals
                (Intent.ACTION_INSERT))
            return;

        if (mTask == null || mTask._id < 1)
            return;

        final Intent intent = new Intent().setAction(Intent.ACTION_EDIT).setClass(getActivity(),
                ActivityEditor.class).setData(mTask.getUri()).putExtra(TaskDetailFragment
                .ARG_ITEM_LIST_ID, mTask.dblist);

        getActivity().setIntent(intent);
    }

    boolean isThereContent() {
        boolean result = false;
        result |= taskText.getText().length() > 0;
        result |= dueDateBox.getText().length() > 0;

        return result;
    }

    /**
     * Inserts a notification item in the UI
     *
     * @param not
     */
    void addNotification(final Notification not) {
        if (getActivity() != null) {
            View nv = LayoutInflater.from(getActivity()).inflate(R.layout.notification_view, null);

            // So we can update the view later
            not.view = nv;

            // Setup all the listeners etc
            NotificationItemHelper.setup(this, notificationList, nv, not, mTask);