/*
* Copyright (c) 2015 Jonas Kalderstam.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nononsenseapps.notepad.data.model.sql;
import android.app.SearchManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.BaseColumns;
import android.text.format.Time;
import com.nononsenseapps.notepad.data.local.sql.LegacyDBHelper;
import com.nononsenseapps.notepad.data.local.sql.MyContentProvider;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.InvalidParameterException;
import java.util.Calendar;
/**
* An object that represents the task information contained in the database.
* Provides convenience methods for moving items.
*/
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;
}
}