/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.service;
import java.util.Date;
import java.util.HashMap;
import java.util.Map.Entry;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import com.google.ical.values.RRule;
import com.timsu.astrid.R;
import com.timsu.astrid.utilities.LegacyTasksXmlExporter;
import com.todoroo.andlib.data.AbstractModel;
import com.todoroo.andlib.data.DatabaseDao;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Property.PropertyVisitor;
import com.todoroo.andlib.data.TodorooCursor;
import com.todoroo.andlib.service.Autowired;
import com.todoroo.andlib.service.ContextManager;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.ExceptionService;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.andlib.utility.Preferences;
import com.todoroo.astrid.alarms.AlarmFields;
import com.todoroo.astrid.backup.TasksXmlImporter;
import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.dao.MetadataDao;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.legacy.AlarmDatabase;
import com.todoroo.astrid.legacy.LegacyAlertModel;
import com.todoroo.astrid.legacy.LegacyRepeatInfo;
import com.todoroo.astrid.legacy.LegacyTaskModel;
import com.todoroo.astrid.legacy.TransitionalAlarm;
import com.todoroo.astrid.tags.TaskToTagMetadata;
public class Astrid2To3UpgradeHelper {
@Autowired
private TaskDao taskDao;
@Autowired
private MetadataDao metadataDao;
@Autowired
private MetadataService metadataService;
@Autowired
private Database database;
@Autowired
private String tasksTable;
@Autowired
private String tagsTable;
@Autowired
private String tagTaskTable;
@Autowired
private String alertsTable;
@Autowired
private String syncTable;
@Autowired
private ExceptionService exceptionService;
// --- implementation
public Astrid2To3UpgradeHelper() {
DependencyInjectionService.getInstance().inject(this);
}
/**
* Upgrade helper class that reads a database
*/
private static class Astrid2UpgradeHelper extends SQLiteOpenHelper {
public Astrid2UpgradeHelper(Context context, String name,
CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
// do nothing
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// do nothing
}
}
// ---------------------------------------------------------------- 3 => 3.1
/**
* Perform the upgrade from Astrid 3 to 3.1
* @param context
* @param upgradeService
* @param from
*/
public void upgrade3To3_1(final Context context, final int from) {
if(!checkIfDatabaseExists(context, alertsTable))
return;
database.openForWriting();
migrateAlarmsToMetadata();
}
// ----------------------------------------------------------------- 2 => 3
/**
* Perform the upgrade from Astrid 2 to Astrid 3
* @param context2
*/
@SuppressWarnings("deprecation")
public void upgrade2To3(final Context context, final int from) {
// if from < 1 (we don't know what version, and database exists, leave it alone)
if(from < 1 && checkIfDatabaseExists(context, database.getName()))
return;
// if you don't have a legacy task table, skip this step
if(!checkIfDatabaseExists(context, tasksTable))
return;
// else, if there's already a database table, clear it out (!!!)
if(checkIfDatabaseExists(context, database.getName()))
context.deleteDatabase(database.getName());
database.openForWriting();
// initiate a backup
String backupFile = legacyBackup();
try {
// --- upgrade tasks table
HashMap<String, Property<?>> propertyMap =
new HashMap<String, Property<?>>();
propertyMap.put("_id", Task.ID); //$NON-NLS-1$
propertyMap.put(LegacyTaskModel.NAME, Task.TITLE);
propertyMap.put(LegacyTaskModel.NOTES, Task.NOTES);
// (don't update progress percentage, we don't use this anymore)
propertyMap.put(LegacyTaskModel.IMPORTANCE, Task.IMPORTANCE);
propertyMap.put(LegacyTaskModel.ESTIMATED_SECONDS, Task.ESTIMATED_SECONDS);
propertyMap.put(LegacyTaskModel.ELAPSED_SECONDS, Task.ELAPSED_SECONDS);
propertyMap.put(LegacyTaskModel.TIMER_START, Task.TIMER_START);
propertyMap.put(LegacyTaskModel.DEFINITE_DUE_DATE, Task.DUE_DATE);
propertyMap.put(LegacyTaskModel.HIDDEN_UNTIL, Task.HIDE_UNTIL);
propertyMap.put(LegacyTaskModel.POSTPONE_COUNT, Task.POSTPONE_COUNT);
propertyMap.put(LegacyTaskModel.NOTIFICATIONS, Task.REMINDER_PERIOD);
propertyMap.put(LegacyTaskModel.NOTIFICATION_FLAGS, Task.REMINDER_FLAGS);
propertyMap.put(LegacyTaskModel.LAST_NOTIFIED, Task.REMINDER_LAST);
propertyMap.put(LegacyTaskModel.REPEAT, Task.RECURRENCE);
propertyMap.put(LegacyTaskModel.CREATION_DATE, Task.CREATION_DATE);
propertyMap.put(LegacyTaskModel.COMPLETION_DATE, Task.COMPLETION_DATE);
propertyMap.put(LegacyTaskModel.CALENDAR_URI, Task.CALENDAR_URI);
propertyMap.put(LegacyTaskModel.FLAGS, Task.FLAGS);
upgradeTable(context, tasksTable,
propertyMap, new Task(), taskDao);
// --- upgrade tags tables
migrateTagsToMetadata();
// --- upgrade alerts
AlarmDatabase alarmsDatabase = new AlarmDatabase();
alarmsDatabase.openForWriting();
propertyMap.clear();
propertyMap.put("_id", TransitionalAlarm.ID); //$NON-NLS-1$
propertyMap.put(LegacyAlertModel.TASK, TransitionalAlarm.TASK);
propertyMap.put(LegacyAlertModel.DATE, TransitionalAlarm.TIME);
upgradeTable(context, alertsTable, propertyMap, new TransitionalAlarm(),
alarmsDatabase.getDao());
alarmsDatabase.close();
// --- clean up database
metadataService.cleanup();
// --- upgrade properties
SharedPreferences prefs = Preferences.getPrefs(context);
Editor editor = prefs.edit();
int random = Preferences.getIntegerFromString(R.string.p_rmd_default_random_hours, -1);
if(random != -1) {
// convert days => hours
editor.putString(context.getString(R.string.p_rmd_default_random_hours),
Integer.toString(random * 24));
}
} catch (Exception e) {
exceptionService.reportError("backup-error", e); //$NON-NLS-1$
if(backupFile != null) {
// try to restore the latest XML
TasksXmlImporter.importTasks(context, backupFile, null);
}
}
}
// --- database upgrade helpers
/**
* Create a legacy backup file
*/
private String legacyBackup() {
try {
LegacyTasksXmlExporter exporter = new LegacyTasksXmlExporter(true);
exporter.setContext(ContextManager.getContext());
return exporter.exportTasks(LegacyTasksXmlExporter.getExportDirectory());
} catch (Exception e) {
// unable to create a backup before upgrading :(
return null;
}
}
protected static final class UpgradeVisitorContainer<TYPE extends AbstractModel> {
public int columnIndex;
public Cursor cursor;
public TYPE model;
public StringBuilder upgradeNotes;
}
/**
* Visitor that reads from a visitor container and writes to the model
* @author Tim Su <tim@todoroo.com>
*
*/
@SuppressWarnings("nls")
protected static final class ColumnUpgradeVisitor implements PropertyVisitor<Void, UpgradeVisitorContainer<?>> {
@Override
public Void visitDouble(Property<Double> property, UpgradeVisitorContainer<?> data) {
double value = data.cursor.getDouble(data.columnIndex);
data.model.setValue(property, value);
Log.d("upgrade", "wrote " + value + " to -> " + property + " of model id " + data.cursor.getLong(1));
return null;
}
@Override
public Void visitInteger(Property<Integer> property, UpgradeVisitorContainer<?> data) {
int value = data.cursor.getInt(data.columnIndex);
data.model.setValue(property, value);
Log.d("upgrade", "wrote " + value + " to -> " + property + " of model id " + data.cursor.getLong(1));
return null;
}
@Override
public Void visitLong(Property<Long> property, UpgradeVisitorContainer<?> data) {
long value = data.cursor.getLong(data.columnIndex);
// special handling for due date
if(property == Task.DUE_DATE) {
long preferredDueDate = data.cursor.getLong(data.cursor.getColumnIndex(LegacyTaskModel.PREFERRED_DUE_DATE));
if(value == 0)
value = preferredDueDate;
else if(preferredDueDate != 0) {
// had both absolute and preferred due dates. write
// preferred due date into notes field
if(data.upgradeNotes == null)
data.upgradeNotes = new StringBuilder();
data.upgradeNotes.append("Goal Deadline: " +
DateUtilities.getDateString(ContextManager.getContext(),
new Date(preferredDueDate)));
}
} else if(property == Task.REMINDER_PERIOD) {
// old period was stored in seconds
value *= 1000L;
} else if(property == Task.COMPLETION_DATE) {
// check if the task was actually completed
int progress = data.cursor.getInt(data.cursor.getColumnIndex(LegacyTaskModel.PROGRESS_PERCENTAGE));
if(progress < 100)
value = 0;
}
data.model.setValue(property, value);
Log.d("upgrade", "wrote " + value + " to -> " + property + " of model id " + data.cursor.getLong(1));
return null;
}
@Override
public Void visitString(Property<String> property, UpgradeVisitorContainer<?> data) {
String value = data.cursor.getString(data.columnIndex);
if(property == Task.RECURRENCE) {
LegacyRepeatInfo repeatInfo = LegacyRepeatInfo.fromSingleField(data.cursor.getInt(data.columnIndex));
if(repeatInfo == null)
data.model.setValue(property, "");
else {
RRule rrule = repeatInfo.toRRule();
data.model.setValue(property, rrule.toIcal());
}
} else {
data.model.setValue(property, value);
}
Log.d("upgrade", "wrote " + value + " to -> " + property + " of model id " + data.cursor.getLong(1));
return null;
}
}
/**
* Helper that reads entries from legacy database and row-by-row
* creates new models and saves them.
*
* @param context
* @param legacyTable
* @param propertyMap
* @param model
* @param dao
*/
@SuppressWarnings("nls")
private static final <TYPE extends AbstractModel> void upgradeTable(Context context, String legacyTable,
HashMap<String, Property<?>> propertyMap, TYPE model,
DatabaseDao<TYPE> dao) {
if(!checkIfDatabaseExists(context, legacyTable))
return;
SQLiteDatabase upgradeDb = new Astrid2UpgradeHelper(context, legacyTable,
null, 1).getReadableDatabase();
Cursor cursor = upgradeDb.rawQuery("SELECT * FROM " + legacyTable, null);
UpgradeVisitorContainer<TYPE> container = new UpgradeVisitorContainer<TYPE>();
container.cursor = cursor;
container.model = model;
ColumnUpgradeVisitor visitor = new ColumnUpgradeVisitor();
for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
model.clear();
for(Entry<String, Property<?>> entry : propertyMap.entrySet()) {
container.columnIndex = cursor.getColumnIndex(entry.getKey());
entry.getValue().accept(visitor, container);
}
// special tweak for adding upgrade notes to tasks
if(container.upgradeNotes != null) {
if(container.model.getValue(Task.NOTES).length() == 0)
container.model.setValue(Task.NOTES, container.upgradeNotes.toString());
else {
container.model.setValue(Task.NOTES,
container.model.getValue(Task.NOTES) + "\n\n" +
container.upgradeNotes);
}
container.upgradeNotes = null;
}
dao.createNew(container.model);
}
cursor.close();
upgradeDb.close();
}
private static boolean checkIfDatabaseExists(Context context, String legacyTable) {
return context.getDatabasePath(legacyTable).exists();
}
/**
* Move data from tags tables into metadata table. We do this by looping
* through both the tags and tagTaskMap databases, reading data from
* both and adding to the Metadata table. This way, we are able to
* do everything in one pass without loading too much into memory
*/
@SuppressWarnings("nls")
private void migrateTagsToMetadata() {
Context context = ContextManager.getContext();
if(!checkIfDatabaseExists(context, tagsTable) ||
!checkIfDatabaseExists(context, tagTaskTable))
return;
SQLiteDatabase tagsDb = new Astrid2UpgradeHelper(context, tagsTable,
null, 1).getReadableDatabase();
SQLiteDatabase tagTaskDb = new Astrid2UpgradeHelper(context, tagTaskTable,
null, 1).getReadableDatabase();
Cursor tagCursor = tagsDb.rawQuery("SELECT _id, name FROM " + tagsTable +
" ORDER BY _id ASC", null);
Cursor mapCursor = null;
try {
mapCursor = tagTaskDb.rawQuery("SELECT tag, task FROM " + tagTaskTable +
" ORDER BY tag ASC", null);
if(tagCursor.getCount() == 0)
return;
Metadata metadata = new Metadata();
metadata.setValue(Metadata.KEY, TaskToTagMetadata.KEY);
long tagId = -1;
String tag = null;
for(mapCursor.moveToFirst(); !mapCursor.isAfterLast(); mapCursor.moveToNext()) {
long mapTagId = mapCursor.getLong(0);
while(mapTagId > tagId && !tagCursor.isLast()) {
tagCursor.moveToNext();
tagId = tagCursor.getLong(0);
tag = null;
}
if(mapTagId == tagId) {
if(tag == null)
tag = tagCursor.getString(1);
long task = mapCursor.getLong(1);
metadata.setValue(Metadata.TASK, task);
metadata.setValue(Metadata.KEY, TaskToTagMetadata.KEY);
metadata.setValue(TaskToTagMetadata.TAG_NAME, tag);
metadataDao.createNew(metadata);
metadata.clearValue(Metadata.ID);
}
}
} finally {
tagCursor.close();
if(mapCursor != null)
mapCursor.close();
tagsDb.close();
tagTaskDb.close();
}
}
/**
* Move data from alert table into metadata table.
*/
private void migrateAlarmsToMetadata() {
Context context = ContextManager.getContext();
if(!checkIfDatabaseExists(context, AlarmDatabase.NAME))
return;
AlarmDatabase alarmsDatabase = new AlarmDatabase();
DatabaseDao<TransitionalAlarm> dao = new DatabaseDao<TransitionalAlarm>(
TransitionalAlarm.class, alarmsDatabase);
TodorooCursor<TransitionalAlarm> cursor = dao.query(Query.select(TransitionalAlarm.PROPERTIES));
try {
if(cursor.getCount() == 0)
return;
Metadata metadata = new Metadata();
metadata.setValue(Metadata.KEY, AlarmFields.METADATA_KEY);
for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
long task = cursor.get(TransitionalAlarm.TASK);
long time = cursor.get(TransitionalAlarm.TIME);
metadata.setValue(Metadata.TASK, task);
metadata.setValue(AlarmFields.TIME, time);
metadata.setValue(AlarmFields.TYPE, AlarmFields.TYPE_SINGLE);
metadataDao.createNew(metadata);
metadata.clearValue(Metadata.ID);
}
} finally {
cursor.close();
alarmsDatabase.close();
}
}
}