/*
* 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.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.view.View;
import com.nononsenseapps.notepad.data.receiver.NotificationHelper;
import com.nononsenseapps.notepad.data.local.sql.MyContentProvider;
import com.nononsenseapps.notepad.util.TimeFormatter;
import com.nononsenseapps.notepad.ui.common.WeekDaysView;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
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();
}
}