package com.todoroo.astrid.service; import java.util.HashMap; import java.util.Map.Entry; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.util.Log; import com.timsu.astrid.data.AbstractController; import com.timsu.astrid.data.task.AbstractTaskModel; import com.todoroo.andlib.data.AbstractModel; import com.todoroo.andlib.data.GenericDao; import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Property.PropertyVisitor; import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.dao.MetadataDao; import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.model.Metadata; import com.todoroo.astrid.model.Task; import com.todoroo.astrid.tags.DataService; public final class UpgradeService { @Autowired private TaskDao taskDao; @Autowired private MetadataDao metadataDao; @Autowired private Database database; @Autowired private String tasksTable; @Autowired private String tagsTable; @Autowired private String tagTaskTable; // --- implementation public UpgradeService() { DependencyInjectionService.getInstance().inject(this); } /** * Perform upgrade from one version to the next. Needs to be called * on the UI thread so it can display a progress bar and then * show users a change log. * * @param from * @param to */ public void performUpgrade(int from, int to) { if(from == to) return; if(from < 150) { upgrade2To3(); } } // --- database upgrade logic /** * 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 } } /** * Perform the upgrade from Astrid 2 to Astrid 3 */ private void upgrade2To3() { Context context = ContextManager.getContext(); database.openForWriting(); // --- upgrade tasks table HashMap> propertyMap = new HashMap>(); propertyMap.put(AbstractController.KEY_ROWID, Task.ID); propertyMap.put(AbstractTaskModel.NAME, Task.TITLE); propertyMap.put(AbstractTaskModel.NOTES, Task.NOTES); // don't update progress percentage, we don't use this anymore propertyMap.put(AbstractTaskModel.IMPORTANCE, Task.IMPORTANCE); propertyMap.put(AbstractTaskModel.ESTIMATED_SECONDS, Task.ESTIMATED_SECONDS); propertyMap.put(AbstractTaskModel.ELAPSED_SECONDS, Task.ELAPSED_SECONDS); propertyMap.put(AbstractTaskModel.TIMER_START, Task.TIMER_START); propertyMap.put(AbstractTaskModel.DEFINITE_DUE_DATE, Task.DUE_DATE); propertyMap.put(AbstractTaskModel.PREFERRED_DUE_DATE, Task.PREFERRED_DUE_DATE); propertyMap.put(AbstractTaskModel.HIDDEN_UNTIL, Task.HIDDEN_UNTIL); propertyMap.put(AbstractTaskModel.POSTPONE_COUNT, Task.POSTPONE_COUNT); propertyMap.put(AbstractTaskModel.NOTIFICATIONS, Task.NOTIFICATIONS); propertyMap.put(AbstractTaskModel.NOTIFICATION_FLAGS, Task.NOTIFICATION_FLAGS); propertyMap.put(AbstractTaskModel.LAST_NOTIFIED, Task.LAST_NOTIFIED); propertyMap.put(AbstractTaskModel.REPEAT, Task.REPEAT); propertyMap.put(AbstractTaskModel.CREATION_DATE, Task.CREATION_DATE); propertyMap.put(AbstractTaskModel.COMPLETION_DATE, Task.COMPLETION_DATE); propertyMap.put(AbstractTaskModel.CALENDAR_URI, Task.CALENDAR_URI); propertyMap.put(AbstractTaskModel.FLAGS, Task.FLAGS); upgradeTable(context, tasksTable, propertyMap, new Task(), taskDao); // --- upgrade tags tables migrateTagsToMetadata(); database.close(); } // --- database upgrade helpers protected static final class UpgradeVisitorContainer { public int columnIndex; public Cursor cursor; public AbstractModel model; } /** * Visitor that reads from a visitor container and writes to the model * @author Tim Su * */ protected static final class ColumnUpgradeVisitor implements PropertyVisitor { @Override public Void visitDouble(Property 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 property, UpgradeVisitorContainer data) { int value; // convert long date -> integer if(property == Task.COMPLETION_DATE || property == Task.CREATION_DATE || property == Task.DELETION_DATE || property == Task.DUE_DATE || property == Task.HIDDEN_UNTIL || property == Task.LAST_NOTIFIED || property == Task.MODIFICATION_DATE || property == Task.PREFERRED_DUE_DATE) value = (int) (data.cursor.getLong(data.columnIndex) / 1000L); else 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 property, UpgradeVisitorContainer data) { long value = data.cursor.getLong(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 visitString(Property property, UpgradeVisitorContainer data) { String value = data.cursor.getString(data.columnIndex); 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 void upgradeTable(Context context, String legacyTable, HashMap> propertyMap, TYPE model, GenericDao dao) { if(!checkIfDatabaseExists(context, legacyTable)) return; SQLiteDatabase upgradeDb = new Astrid2UpgradeHelper(context, legacyTable, null, 1).getReadableDatabase(); Cursor cursor = upgradeDb.rawQuery("SELECT * FROM " + legacyTable, null); UpgradeVisitorContainer container = new UpgradeVisitorContainer(); container.cursor = cursor; container.model = model; ColumnUpgradeVisitor visitor = new ColumnUpgradeVisitor(); for(cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { model.clear(); for(Entry> entry : propertyMap.entrySet()) { container.columnIndex = cursor.getColumnIndex(entry.getKey()); entry.getValue().accept(visitor, container); } dao.createItem(container.model); } 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 = 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, DataService.KEY); long tagId = -1; String tag = null; for(mapCursor.moveToFirst(); !mapCursor.isAfterLast(); mapCursor.moveToNext()) { long mapTagId = mapCursor.getLong(1); while(mapTagId > tagId && !tagCursor.isLast()) { tagCursor.moveToNext(); tagId = tagCursor.getLong(1); } if(mapTagId == tagId) { if(tag == null) tag = tagCursor.getString(2); long task = mapCursor.getLong(2); metadata.setValue(Metadata.TASK, task); metadata.setValue(Metadata.VALUE, tag); metadataDao.createItem(metadata); } } tagCursor.close(); mapCursor.close(); } }