diff --git a/.classpath b/.classpath
index 528f1b336..28dab5b65 100644
--- a/.classpath
+++ b/.classpath
@@ -1,10 +1,13 @@
-
-
+
-
+
+
+
+
+
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 51404c98c..ac3ac06b3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -10,6 +10,7 @@
+
@@ -119,6 +120,7 @@
+
diff --git a/res/values/keys.xml b/res/values/keys.xml
index d20f23ea8..7fe1d2d08 100644
--- a/res/values/keys.xml
+++ b/res/values/keys.xml
@@ -48,6 +48,8 @@
nagging
deadline_time
+
+ backup
titleVisible
true
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c072ac31b..161137b68 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -175,7 +175,9 @@
Take Astrid\'s Survey!
Quick Tips
Clean Up Old Tasks
-
+ Export Tasks
+ Import Tasks
+
Edit Task
Delete Task
Start Timer
@@ -379,7 +381,24 @@ Astrid might not let you know when your tasks are due.\n
I Won\'t Kill Astrid!
-
+
+
+
+ Exported %s to %s.
+ Import Summary
+
+File %s contained %d tasks.\n
+Imported %d tasks.\n
+Skipped %d tasks.\n
+
+ Import
+ Opening file...
+ File opened...
+ Reading task %d...
+ Skipped task %d...
+ Imported task %d...
+ Select a File to Import
+
Astrid Tag Alert
@@ -401,6 +420,7 @@ Astrid might not let you know when your tasks are due.\n
Couldn't find this item:
Couldn't save:
+ Cannot access:
@@ -442,6 +462,9 @@ Astrid might not let you know when your tasks are due.\n
Default Deadlines
# of days from now to set new deadlines
+
+ Automatic Backups
+ Perform daily backups to sdcard.
Displayed Fields
Select the fields to show in task list
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 8812d3332..76179df25 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -107,6 +107,11 @@
+ android:summary="@string/prefs_deadlineTime_desc" />
+
\ No newline at end of file
diff --git a/src/com/timsu/astrid/activities/TaskListSubActivity.java b/src/com/timsu/astrid/activities/TaskListSubActivity.java
index 573e7c019..a1f2c6e5a 100644
--- a/src/com/timsu/astrid/activities/TaskListSubActivity.java
+++ b/src/com/timsu/astrid/activities/TaskListSubActivity.java
@@ -19,15 +19,6 @@
*/
package com.timsu.astrid.activities;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Random;
-
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
@@ -40,24 +31,12 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
-import android.view.ContextMenu;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
+import android.view.*;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.EditText;
-import android.widget.ImageButton;
-import android.widget.ListView;
-import android.widget.TextView;
-import android.widget.Toast;
+import android.widget.*;
import android.widget.AdapterView.OnItemClickListener;
-
import com.flurry.android.FlurryAgent;
import com.timsu.astrid.R;
import com.timsu.astrid.activities.TaskListAdapter.TaskListAdapterHooks;
@@ -71,16 +50,15 @@ import com.timsu.astrid.data.task.TaskModelForList;
import com.timsu.astrid.sync.SynchronizationService;
import com.timsu.astrid.sync.Synchronizer;
import com.timsu.astrid.sync.Synchronizer.SynchronizerListener;
-import com.timsu.astrid.utilities.AstridUtilities;
-import com.timsu.astrid.utilities.Constants;
-import com.timsu.astrid.utilities.DialogUtilities;
-import com.timsu.astrid.utilities.Notifications;
-import com.timsu.astrid.utilities.Preferences;
+import com.timsu.astrid.utilities.*;
+import com.timsu.astrid.widget.FilePickerBuilder;
+import com.timsu.astrid.widget.NNumberPickerDialog.OnNNumberPickedListener;
import com.timsu.astrid.widget.NumberPicker;
import com.timsu.astrid.widget.NumberPickerDialog;
-import com.timsu.astrid.widget.NNumberPickerDialog.OnNNumberPickedListener;
import com.timsu.astrid.widget.NumberPickerDialog.OnNumberPickedListener;
+import java.util.*;
+
/**
* Primary view for the Astrid Application. Lists all of the tasks in the
* system, and allows users to interact with them.
@@ -116,6 +94,8 @@ public class TaskListSubActivity extends SubActivity {
private static final int OPTIONS_HELP_ID = Menu.FIRST + 12;
private static final int OPTIONS_CLEANUP_ID = Menu.FIRST + 13;
private static final int OPTIONS_QUICK_TIPS = Menu.FIRST + 14;
+ private static final int OPTIONS_EXPORT = Menu.FIRST + 15;
+ private static final int OPTIONS_IMPORT = Menu.FIRST + 16;
private static final int CONTEXT_FILTER_HIDDEN = Menu.FIRST + 20;
private static final int CONTEXT_FILTER_DONE = Menu.FIRST + 21;
@@ -384,6 +364,12 @@ public class TaskListSubActivity extends SubActivity {
item = menu.add(Menu.NONE, OPTIONS_QUICK_TIPS, Menu.NONE,
R.string.taskList_menu_tips);
+ item = menu.add(Menu.NONE, OPTIONS_EXPORT, Menu.NONE,
+ R.string.taskList_menu_export);
+
+ item = menu.add(Menu.NONE, OPTIONS_IMPORT, Menu.NONE,
+ R.string.taskList_menu_import);
+
item = menu.add(Menu.NONE, OPTIONS_HELP_ID, Menu.NONE,
R.string.taskList_menu_help);
item.setAlphabeticShortcut('h');
@@ -1230,6 +1216,12 @@ public class TaskListSubActivity extends SubActivity {
case OPTIONS_CLEANUP_ID:
cleanOldTasks();
return true;
+ case OPTIONS_EXPORT:
+ exportTasks();
+ return true;
+ case OPTIONS_IMPORT:
+ importTasks();
+ return true;
// --- list context menu items
case TaskListAdapter.CONTEXT_EDIT_ID:
@@ -1297,6 +1289,33 @@ public class TaskListSubActivity extends SubActivity {
return false;
}
+ private void importTasks() {
+ final Runnable reloadList = new Runnable() {
+ public void run() {
+ reloadList();
+ }
+ };
+ final Context ctx = this.getParent();
+ FilePickerBuilder.OnFilePickedListener listener = new FilePickerBuilder.OnFilePickedListener() {
+ @Override
+ public void onFilePicked(String filePath) {
+ TasksXmlImporter importer = new TasksXmlImporter(ctx);
+ importer.setInput(filePath);
+ importer.importTasks(reloadList);
+ }
+ };
+ DialogUtilities.filePicker(ctx,
+ ctx.getString(R.string.import_file_prompt),
+ TasksXmlExporter.getExportDirectory(),
+ listener);
+ }
+
+ private void exportTasks() {
+ TasksXmlExporter exporter = new TasksXmlExporter(false);
+ exporter.setContext(getParent());
+ exporter.exportTasks();
+ }
+
/*
* ======================================================================
* ===================================================== getters / setters
diff --git a/src/com/timsu/astrid/data/sync/SyncDataController.java b/src/com/timsu/astrid/data/sync/SyncDataController.java
index 8e9ababc4..20eaa492e 100644
--- a/src/com/timsu/astrid/data/sync/SyncDataController.java
+++ b/src/com/timsu/astrid/data/sync/SyncDataController.java
@@ -19,21 +19,20 @@
*/
package com.timsu.astrid.data.sync;
-import java.util.HashSet;
-
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
-
import com.timsu.astrid.data.AbstractController;
import com.timsu.astrid.data.sync.SyncMapping.SyncMappingDatabaseHelper;
import com.timsu.astrid.data.task.AbstractTaskModel;
import com.timsu.astrid.data.task.TaskIdentifier;
import com.timsu.astrid.data.task.TaskModelForSync;
+import java.util.HashSet;
+
/** Controller for Tag-related operations */
public class SyncDataController extends AbstractController {
@@ -91,6 +90,30 @@ public class SyncDataController extends AbstractController {
}
}
+ /** Get all mappings for specified task for all synchronization services */
+ public HashSet getSyncMappings(TaskIdentifier taskId)
+ throws SQLException {
+ HashSet list = new HashSet();
+ Cursor cursor = syncDatabase.query(SYNC_TABLE_NAME,
+ SyncMapping.FIELD_LIST,
+ SyncMapping.TASK + " = ?",
+ new String[] { "" + taskId.getId() },
+ null, null, null);
+
+ try {
+ if(cursor.getCount() == 0)
+ return list;
+ do {
+ cursor.moveToNext();
+ list.add(new SyncMapping(cursor));
+ } while(!cursor.isLast());
+
+ return list;
+ } finally {
+ cursor.close();
+ }
+ }
+
/** Get mapping for given task */
public SyncMapping getSyncMapping(int syncServiceId, TaskIdentifier taskId)
throws SQLException {
diff --git a/src/com/timsu/astrid/data/tag/TagController.java b/src/com/timsu/astrid/data/tag/TagController.java
index d0368655f..f6b0c9325 100644
--- a/src/com/timsu/astrid/data/tag/TagController.java
+++ b/src/com/timsu/astrid/data/tag/TagController.java
@@ -19,23 +19,22 @@
*/
package com.timsu.astrid.data.tag;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
-
import com.timsu.astrid.data.AbstractController;
import com.timsu.astrid.data.tag.AbstractTagModel.TagModelDatabaseHelper;
import com.timsu.astrid.data.tag.TagToTaskMapping.TagToTaskMappingDatabaseHelper;
-import com.timsu.astrid.data.task.TaskIdentifier;
import com.timsu.astrid.data.task.AbstractTaskModel.TaskModelDatabaseHelper;
+import com.timsu.astrid.data.task.TaskIdentifier;
import com.timsu.astrid.provider.TasksProvider;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+
/** Controller for Tag-related operations */
public class TagController extends AbstractController {
diff --git a/src/com/timsu/astrid/data/task/TaskController.java b/src/com/timsu/astrid/data/task/TaskController.java
index 63bfec8b6..8a7b3c6b6 100644
--- a/src/com/timsu/astrid/data/task/TaskController.java
+++ b/src/com/timsu/astrid/data/task/TaskController.java
@@ -19,11 +19,6 @@
*/
package com.timsu.astrid.data.task;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
-
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -35,7 +30,6 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;
-
import com.timsu.astrid.activities.TaskEdit;
import com.timsu.astrid.activities.TaskListSubActivity;
import com.timsu.astrid.appwidget.AstridAppWidgetProvider.UpdateService;
@@ -49,6 +43,11 @@ import com.timsu.astrid.sync.Synchronizer;
import com.timsu.astrid.sync.Synchronizer.SynchronizerListener;
import com.timsu.astrid.utilities.Notifications;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+
/**
* Controller for task-related operations
*
@@ -124,6 +123,14 @@ public class TaskController extends AbstractController {
null, null, null, null, null, null);
}
+ /** Return a list of all tasks */
+ public Cursor getBackupTaskListCursor() {
+ return database.query(TASK_TABLE_NAME, TaskModelForXml.FIELD_LIST,
+ AbstractTaskModel.PROGRESS_PERCENTAGE + " < " +
+ AbstractTaskModel.COMPLETE_PERCENTAGE, null, null, null,
+ null, null);
+ }
+
/** Delete all completed tasks with date < older than date */
public int deleteCompletedTasksOlderThan(Date olderThanDate) {
return database.delete(TASK_TABLE_NAME, String.format("`%s` >= '%d' AND `%s` <= '%d'",
@@ -434,6 +441,31 @@ public class TaskController extends AbstractController {
return model;
}
+ /** Returns a TaskModelForXml corresponding to the given TaskIdentifier */
+ public TaskModelForXml fetchTaskForXml(TaskIdentifier taskId) throws SQLException {
+ Cursor cursor = fetchTaskCursor(taskId, TaskModelForXml.FIELD_LIST);
+ TaskModelForXml model = new TaskModelForXml(cursor);
+ cursor.close();
+ return model;
+ }
+
+ /* Attempts to return a TaskModelForXml for the given name and creation date */
+ public TaskModelForXml fetchTaskForXml(String name, Date creationDate) {
+ Cursor cursor;
+ try {
+ cursor = fetchTaskCursor(name, "" + creationDate.getTime(),
+ TaskModelForXml.FIELD_LIST);
+ } catch (SQLException e) {
+ return null;
+ }
+ if (cursor == null || cursor.getCount() == 0) {
+ return null;
+ }
+ TaskModelForXml model = new TaskModelForXml(cursor);
+ cursor.close();
+ return model;
+ }
+
/** Returns a TaskModelForReminder corresponding to the given TaskIdentifier */
public TaskModelForReminder fetchTaskForReminder(TaskIdentifier taskId) throws SQLException {
Cursor cursor = fetchTaskCursor(taskId, TaskModelForReminder.FIELD_LIST);
@@ -490,6 +522,21 @@ public class TaskController extends AbstractController {
return cursor;
}
+ /** Returns null if unsuccessful, otherwise moves cursor to the task.
+ * Don't forget to close the cursor when you're done. */
+ private Cursor fetchTaskCursor(String name, String creationDate, String[] fieldList) {
+ final String where = AbstractTaskModel.NAME + " = ? AND "
+ + AbstractTaskModel.CREATION_DATE + " = ?";
+ Cursor cursor = database.query(true, TASK_TABLE_NAME, fieldList,
+ where, new String[] {name, creationDate}, null, null, null, null);
+ if (cursor == null)
+ throw new SQLException("Returned empty set!");
+
+ if (cursor.moveToFirst()) {
+ return cursor;
+ }
+ return null;
+ }
// --- methods supporting individual features
/** Returns a TaskModelForView corresponding to the given TaskIdentifier */
diff --git a/src/com/timsu/astrid/data/task/TaskModelForXml.java b/src/com/timsu/astrid/data/task/TaskModelForXml.java
new file mode 100644
index 000000000..8778c6b2f
--- /dev/null
+++ b/src/com/timsu/astrid/data/task/TaskModelForXml.java
@@ -0,0 +1,192 @@
+package com.timsu.astrid.data.task;
+
+import android.database.Cursor;
+import com.timsu.astrid.data.AbstractController;
+import com.timsu.astrid.data.enums.Importance;
+import com.timsu.astrid.data.enums.RepeatInterval;
+import com.timsu.astrid.utilities.DateUtilities;
+
+import java.util.Date;
+import java.util.HashMap;
+
+public class TaskModelForXml extends AbstractTaskModel {
+
+ static String[] FIELD_LIST = new String[] {
+ AbstractController.KEY_ROWID,
+ NAME,
+ IMPORTANCE,
+ ELAPSED_SECONDS,
+ ESTIMATED_SECONDS,
+ TIMER_START,
+ DEFINITE_DUE_DATE,
+ PREFERRED_DUE_DATE,
+ NOTIFICATIONS,
+ PROGRESS_PERCENTAGE,
+ COMPLETION_DATE,
+ CREATION_DATE,
+ HIDDEN_UNTIL,
+ NOTES,
+ REPEAT,
+ FLAGS,
+ POSTPONE_COUNT,
+ BLOCKING_ON,
+ LAST_NOTIFIED,
+ NOTIFICATION_FLAGS,
+ CALENDAR_URI,
+ };
+ private HashMap taskAttributesMap;
+ public static final String REPEAT_VALUE = "repeat_value";
+ public static final String REPEAT_INTERVAL = "repeat_interval";
+
+
+ private RepeatInterval repeatInterval = null;
+ private Integer repeatValue = null;
+
+ // --- constructors
+
+ public TaskModelForXml() {
+ super();
+ setCreationDate(new Date());
+ taskAttributesMap = new HashMap(FIELD_LIST.length);
+ }
+
+ public TaskModelForXml(Cursor cursor) {
+ super(cursor);
+ prefetchData(FIELD_LIST);
+ taskAttributesMap = new HashMap(FIELD_LIST.length);
+ }
+
+ /* Safely add a value from a date field (in case of null values) to the
+ taskAttributesMap.
+ */
+ private void safePutDate(String field, Date value) {
+ if (value != null) {
+ taskAttributesMap.put(field, DateUtilities.getIso8601String(value));
+ }
+ }
+
+ // --- getters and setters
+
+ public Date getCreationDate() {
+ return super.getCreationDate();
+ }
+
+ /* Build a HashMap of task fields and associated values.
+ */
+ public HashMap getTaskAttributes() {
+ taskAttributesMap.put(AbstractController.KEY_ROWID, getTaskIdentifier().idAsString());
+ taskAttributesMap.put(NAME, getName());
+ taskAttributesMap.put(IMPORTANCE, getImportance().toString());
+ taskAttributesMap.put(ELAPSED_SECONDS, getElapsedSeconds().toString());
+ taskAttributesMap.put(ESTIMATED_SECONDS, getEstimatedSeconds().toString());
+ safePutDate(TIMER_START, getTimerStart());
+ safePutDate(DEFINITE_DUE_DATE, getDefiniteDueDate());
+ safePutDate(PREFERRED_DUE_DATE, getPreferredDueDate());
+ taskAttributesMap.put(NOTIFICATIONS, getNotificationIntervalSeconds().toString());
+ taskAttributesMap.put(PROGRESS_PERCENTAGE, Integer.toString(getProgressPercentage()));
+ safePutDate(COMPLETION_DATE, getCompletionDate());
+ safePutDate(CREATION_DATE, getCreationDate());
+ safePutDate(HIDDEN_UNTIL, getHiddenUntil());
+ taskAttributesMap.put(NOTES, getNotes());
+ RepeatInfo repeat = getRepeat();
+ if (repeat != null) {
+ taskAttributesMap.put(REPEAT_VALUE, Integer.toString(repeat.getValue()));
+ taskAttributesMap.put(REPEAT_INTERVAL,
+ Integer.toString(repeat.getInterval().getLabelResource()));
+ }
+ taskAttributesMap.put(FLAGS, Integer.toString(getFlags()));
+ taskAttributesMap.put(POSTPONE_COUNT, getPostponeCount().toString());
+ taskAttributesMap.put(BLOCKING_ON, Long.toString(getBlockingOn().getId()));
+ safePutDate(LAST_NOTIFIED, getLastNotificationDate());
+ taskAttributesMap.put(NOTIFICATION_FLAGS, Integer.toString(getNotificationFlags()));
+ String calendarUri = getCalendarUri();
+ if (calendarUri != null) {
+ taskAttributesMap.put(CALENDAR_URI, calendarUri);
+ }
+ return taskAttributesMap;
+ }
+
+ // --- setters
+
+ public boolean setField(String field, String value) {
+ boolean success = true;
+ if(field.equals(NAME)) {
+ setName(value);
+ }
+ else if(field.equals(NOTES)) {
+ setNotes(value);
+ }
+ else if(field.equals(PROGRESS_PERCENTAGE)) {
+ setProgressPercentage(Integer.parseInt(value));
+ }
+ else if(field.equals(IMPORTANCE)) {
+ setImportance(Importance.valueOf(value));
+ }
+ else if(field.equals(ESTIMATED_SECONDS)) {
+ setEstimatedSeconds(Integer.parseInt(value));
+ }
+ else if(field.equals(ELAPSED_SECONDS)) {
+ setElapsedSeconds(Integer.parseInt(value));
+ }
+ else if(field.equals(TIMER_START)) {
+ setTimerStart(DateUtilities.getDateFromIso8601String(value));
+ }
+ else if(field.equals(DEFINITE_DUE_DATE)) {
+ setDefiniteDueDate(DateUtilities.getDateFromIso8601String(value));
+ }
+ else if(field.equals(PREFERRED_DUE_DATE)) {
+ setPreferredDueDate(DateUtilities.getDateFromIso8601String(value));
+ }
+ else if(field.equals(HIDDEN_UNTIL)) {
+ setHiddenUntil(DateUtilities.getDateFromIso8601String(value));
+ }
+ else if(field.equals(BLOCKING_ON)) {
+ setBlockingOn(new TaskIdentifier(Long.parseLong(value)));
+ }
+ else if(field.equals(POSTPONE_COUNT)) {
+ setPostponeCount(Integer.parseInt(value));
+ }
+ else if(field.equals(NOTIFICATIONS)) {
+ setNotificationIntervalSeconds(Integer.parseInt(value));
+ }
+ else if(field.equals(CREATION_DATE)) {
+ setCreationDate(DateUtilities.getDateFromIso8601String(value));
+ }
+ else if(field.equals(COMPLETION_DATE)) {
+ setCompletionDate(DateUtilities.getDateFromIso8601String(value));
+ }
+ else if(field.equals(NOTIFICATION_FLAGS)) {
+ setNotificationFlags(Integer.parseInt(value));
+ }
+ else if(field.equals(LAST_NOTIFIED)) {
+ setLastNotificationTime(DateUtilities.getDateFromIso8601String(value));
+ }
+ else if(field.equals(REPEAT_INTERVAL)) {
+ setRepeatInterval(RepeatInterval.values()[Integer.parseInt(value)]);
+ }
+ else if(field.equals(REPEAT_VALUE)) {
+ setRepeatValue(Integer.parseInt(value));
+ }
+ else if(field.equals(FLAGS)) {
+ setFlags(Integer.parseInt(value));
+ }
+ else {
+ success = false;
+ }
+ return success;
+ }
+
+ public void setRepeatInterval(RepeatInterval repeatInterval) {
+ this.repeatInterval = repeatInterval;
+ if (repeatValue != null) {
+ setRepeat(new RepeatInfo(repeatInterval, repeatValue));
+ }
+ }
+
+ public void setRepeatValue(Integer repeatValue) {
+ this.repeatValue = repeatValue;
+ if (repeatInterval != null) {
+ setRepeat(new RepeatInfo(repeatInterval, repeatValue));
+ }
+ }
+}
diff --git a/src/com/timsu/astrid/utilities/BackupService.java b/src/com/timsu/astrid/utilities/BackupService.java
new file mode 100644
index 000000000..f7fc612ca
--- /dev/null
+++ b/src/com/timsu/astrid/utilities/BackupService.java
@@ -0,0 +1,91 @@
+package com.timsu.astrid.utilities;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+
+import java.io.File;
+import java.io.FilenameFilter;
+
+public class BackupService extends Service {
+ /* Inspired heavily by SynchronizationService
+
+ */
+
+ private static final long BACKUP_OFFSET = 5*60*1000L;
+ private static final String BACKUP_ACTION = "backup";
+ private static final String BACKUP_FILE_NAME_REGEX = "auto\\.\\d{6}\\-\\d{4}\\.xml";
+ private static final int DAYS_TO_KEEP_BACKUP = 7;
+
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ if (intent.getAction().equals(BACKUP_ACTION)) {
+ startBackup(this);
+ }
+ }
+
+ private void startBackup(Context ctx) {
+ if (ctx == null || ctx.getResources() == null) {
+ return;
+ }
+ if (!Preferences.isBackupEnabled(ctx)) {
+ return;
+ }
+ deleteOldBackups();
+ TasksXmlExporter exporter = new TasksXmlExporter(true);
+ exporter.setContext(ctx);
+ exporter.exportTasks();
+ }
+
+ public static void scheduleService(Context ctx) {
+ AlarmManager am = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);
+ PendingIntent pendingIntent = PendingIntent.getService(ctx, 0,
+ createAlarmIntent(ctx), PendingIntent.FLAG_UPDATE_CURRENT);
+ am.cancel(pendingIntent);
+ if (!Preferences.isBackupEnabled(ctx)) {
+ return;
+ }
+ am.setInexactRepeating(AlarmManager.RTC, System.currentTimeMillis() + BACKUP_OFFSET,
+ AlarmManager.INTERVAL_DAY, pendingIntent);
+ }
+
+ public static void unscheduleService(Context ctx) {
+ AlarmManager am = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);
+ PendingIntent pendingIntent = PendingIntent.getService(ctx, 0,
+ createAlarmIntent(ctx), PendingIntent.FLAG_UPDATE_CURRENT);
+ am.cancel(pendingIntent);
+ }
+
+ private static Intent createAlarmIntent(Context ctx) {
+ Intent intent = new Intent(ctx, BackupService.class);
+ intent.setAction(BACKUP_ACTION);
+ return intent;
+ }
+
+ private void deleteOldBackups() {
+ FilenameFilter filter = new FilenameFilter() {
+ @Override
+ public boolean accept(File file, String s) {
+ if (s.matches(BACKUP_FILE_NAME_REGEX)) {
+ String dateString = s.substring(12, 18);
+ return DateUtilities.wasCreatedBefore(dateString, DAYS_TO_KEEP_BACKUP);
+ }
+ return false;
+ }
+ };
+ File astridDir = TasksXmlExporter.getExportDirectory();
+ String[] files = astridDir.list(filter);
+ for (String file : files) {
+ new File(astridDir, file).delete();
+ }
+ }
+}
diff --git a/src/com/timsu/astrid/utilities/DateUtilities.java b/src/com/timsu/astrid/utilities/DateUtilities.java
index c8af6fb19..3886fefd0 100644
--- a/src/com/timsu/astrid/utilities/DateUtilities.java
+++ b/src/com/timsu/astrid/utilities/DateUtilities.java
@@ -19,16 +19,20 @@
*/
package com.timsu.astrid.utilities;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-
import android.content.res.Resources;
-
+import android.util.Log;
import com.timsu.astrid.R;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
public class DateUtilities {
private static SimpleDateFormat format = null;
+ private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ssz";
/** Format a time into a medium length absolute format */
public static String getFormattedDate(Resources r, Date date) {
@@ -190,4 +194,51 @@ public class DateUtilities {
return result.toString();
}
+
+ /* Format a Date into ISO 8601 Compliant format.
+
+ */
+ public static String getIso8601String(Date d) {
+ SimpleDateFormat sdf = new SimpleDateFormat(ISO_8601_FORMAT);
+ String result = "";
+ if (d != null) {
+ result = sdf.format(d);
+ }
+ return result;
+ }
+
+ /* Take an ISO 8601 string and return a Date object.
+ On failure, returns null.
+ */
+ public static Date getDateFromIso8601String(String s) {
+ SimpleDateFormat df = new SimpleDateFormat(ISO_8601_FORMAT);
+ try {
+ return df.parse(s);
+ } catch (ParseException e) {
+ Log.e("DateUtilities", "Error parsing ISO 8601 date");
+ return null;
+ }
+ }
+
+ /* Get current date and time as a string.
+ Used in TasksXmlExporter
+ */
+ public static String getDateForExport() {
+ DateFormat df = new SimpleDateFormat("yyMMdd-HHmm");
+ return df.format(new Date());
+ }
+
+ public static boolean wasCreatedBefore(String s, int daysAgo) {
+ DateFormat df = new SimpleDateFormat("yyMMdd");
+ Date date;
+ try {
+ date = df.parse(s);
+ } catch (ParseException e) {
+ return false;
+ }
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DATE, -daysAgo);
+ Date calDate = cal.getTime();
+ return date.before(calDate);
+ }
}
diff --git a/src/com/timsu/astrid/utilities/DialogUtilities.java b/src/com/timsu/astrid/utilities/DialogUtilities.java
index 3040fe8ca..c26844ba6 100644
--- a/src/com/timsu/astrid/utilities/DialogUtilities.java
+++ b/src/com/timsu/astrid/utilities/DialogUtilities.java
@@ -4,11 +4,13 @@ import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
-
import com.timsu.astrid.R;
+import com.timsu.astrid.widget.FilePickerBuilder;
import com.timsu.astrid.widget.NNumberPickerDialog;
import com.timsu.astrid.widget.NNumberPickerDialog.OnNNumberPickedListener;
+import java.io.File;
+
public class DialogUtilities {
/**
@@ -87,4 +89,12 @@ public class DialogUtilities {
new int[] {0, 0}, new int[] {1, 5}, new int[] {0, 0},
new int[] {99, 59}, new String[] {":", null}).show();
}
+
+ /** Display a dialog box with a list of files to pick.
+ *
+ */
+ public static void filePicker(Context context, String title, File path,
+ FilePickerBuilder.OnFilePickedListener listener) {
+ new FilePickerBuilder(context, title, path, listener).show();
+ }
}
diff --git a/src/com/timsu/astrid/utilities/Preferences.java b/src/com/timsu/astrid/utilities/Preferences.java
index bd74cfa2a..d554ccd35 100644
--- a/src/com/timsu/astrid/utilities/Preferences.java
+++ b/src/com/timsu/astrid/utilities/Preferences.java
@@ -1,17 +1,16 @@
package com.timsu.astrid.utilities;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.net.Uri;
import android.preference.PreferenceManager;
-
import com.timsu.astrid.R;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
public class Preferences {
// pref keys
@@ -60,6 +59,9 @@ public class Preferences {
if(!prefs.contains(r.getString(R.string.p_notif_vibrate))) {
editor.putBoolean(r.getString(R.string.p_notif_vibrate), true);
}
+ if (!prefs.contains(r.getString(R.string.p_backup))) {
+ editor.putBoolean(r.getString(R.string.p_backup), true);
+ }
setVisibilityPreferences(prefs, editor, r);
@@ -278,6 +280,12 @@ public class Preferences {
editor.commit();
}
+ // --- backup preferences
+ public static boolean isBackupEnabled(Context context) {
+ Resources r = context.getResources();
+ return getPrefs(context).getBoolean(r.getString(R.string.p_backup), true);
+ }
+
// --- synchronization preferences
/** RTM authentication token, or null if doesn't exist */
diff --git a/src/com/timsu/astrid/utilities/StartupReceiver.java b/src/com/timsu/astrid/utilities/StartupReceiver.java
index 0df073e6f..679b0e9c0 100644
--- a/src/com/timsu/astrid/utilities/StartupReceiver.java
+++ b/src/com/timsu/astrid/utilities/StartupReceiver.java
@@ -1,7 +1,5 @@
package com.timsu.astrid.utilities;
-import java.util.List;
-
import android.Manifest;
import android.app.AlarmManager;
import android.app.AlertDialog;
@@ -9,17 +7,18 @@ import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
-import android.content.Intent;
import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;
-
import com.timsu.astrid.R;
import com.timsu.astrid.activities.SyncPreferences;
import com.timsu.astrid.appwidget.AstridAppWidgetProvider.UpdateService;
import com.timsu.astrid.sync.SynchronizationService;
+import java.util.List;
+
public class StartupReceiver extends BroadcastReceiver {
private static boolean hasStartedUp = false;
@@ -85,6 +84,9 @@ public class StartupReceiver extends BroadcastReceiver {
// start synchronization service
SynchronizationService.scheduleService(context);
+
+ // start backup service
+ BackupService.scheduleService(context);
}
}).start();
diff --git a/src/com/timsu/astrid/utilities/TasksXmlExporter.java b/src/com/timsu/astrid/utilities/TasksXmlExporter.java
new file mode 100644
index 000000000..e676ecbb7
--- /dev/null
+++ b/src/com/timsu/astrid/utilities/TasksXmlExporter.java
@@ -0,0 +1,268 @@
+package com.timsu.astrid.utilities;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Environment;
+import android.os.Looper;
+import android.util.Log;
+import android.util.Xml;
+import android.widget.Toast;
+import com.timsu.astrid.R;
+import com.timsu.astrid.data.alerts.AlertController;
+import com.timsu.astrid.data.sync.SyncDataController;
+import com.timsu.astrid.data.sync.SyncMapping;
+import com.timsu.astrid.data.tag.TagController;
+import com.timsu.astrid.data.tag.TagIdentifier;
+import com.timsu.astrid.data.tag.TagModelForView;
+import com.timsu.astrid.data.task.TaskController;
+import com.timsu.astrid.data.task.TaskIdentifier;
+import com.timsu.astrid.data.task.TaskModelForXml;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.*;
+
+public class TasksXmlExporter {
+
+ private TaskController taskController;
+ private TagController tagController;
+ private AlertController alertController;
+ private SyncDataController syncDataController;
+ private Context ctx;
+ private String output;
+ private boolean isService;
+ private int exportCount;
+ private XmlSerializer xml;
+ private HashMap tagMap;
+
+ public static final String ASTRID_TAG = "astrid";
+ public static final String ASTRID_ATTR_VERSION = "version";
+ public static final String TASK_TAG = "task";
+ public static final String TAG_TAG = "tag";
+ public static final String TAG_ATTR_NAME = "name";
+ public static final String ALERT_TAG = "alert";
+ public static final String ALERT_ATTR_DATE = "date";
+ public static final String SYNC_TAG = "sync";
+ public static final String SYNC_ATTR_SERVICE = "service";
+ public static final String SYNC_ATTR_REMOTE_ID = "remote_id";
+ public static final String XML_ENCODING = "utf-8";
+ public static final String ASTRID_DIR = "/astrid";
+ private static final String EXPORT_FILE_NAME = "user.%s.xml";
+ private static final String BACKUP_FILE_NAME = "auto.%s.xml";
+
+ public TasksXmlExporter(boolean isService) {
+ this.isService = isService;
+ this.exportCount = 0;
+ }
+
+ private void initTagMap() {
+ tagMap = tagController.getAllTagsAsMap();
+ }
+
+ private void serializeTags(TaskIdentifier task)
+ throws IOException {
+ LinkedList tags = tagController.getTaskTags(task);
+ for (TagIdentifier tag : tags) {
+ xml.startTag(null, TAG_TAG);
+ xml.attribute(null, TAG_ATTR_NAME, tagMap.get(tag).toString());
+ xml.endTag(null, TAG_TAG);
+ }
+ }
+
+ private void serializeSyncMappings(TaskIdentifier task)
+ throws IOException {
+ HashSet syncMappings = syncDataController.getSyncMappings(task);
+ for (SyncMapping sync : syncMappings) {
+ xml.startTag(null, SYNC_TAG);
+ xml.attribute(null, SYNC_ATTR_SERVICE,
+ Integer.toString(sync.getSyncServiceId()));
+ xml.attribute(null, SYNC_ATTR_REMOTE_ID, sync.getRemoteId());
+ xml.endTag(null, SYNC_TAG);
+ }
+ }
+
+ private void serializeAlerts(TaskIdentifier task)
+ throws IOException {
+ List alerts = alertController.getTaskAlerts(task);
+ for (Date alert : alerts) {
+ xml.startTag(null, ALERT_TAG);
+ xml.attribute(null, ALERT_ATTR_DATE, DateUtilities.getIso8601String(alert));
+ xml.endTag(null, ALERT_TAG);
+ }
+ }
+
+ private void serializeTasks()
+ throws IOException {
+ Cursor c = taskController.getBackupTaskListCursor();
+ if (! c.moveToFirst()) {
+ return; // No tasks.
+ }
+ do {
+ TaskModelForXml task = new TaskModelForXml(c);
+ TaskIdentifier taskId = task.getTaskIdentifier();
+ xml.startTag(null, TASK_TAG);
+ HashMap taskAttributes = task.getTaskAttributes();
+ for (String key : taskAttributes.keySet()) {
+ String value = taskAttributes.get(key);
+ xml.attribute(null, key, value);
+ }
+ serializeTags(taskId);
+ serializeAlerts(taskId);
+ serializeSyncMappings(taskId);
+ xml.endTag(null, TASK_TAG);
+ this.exportCount++;
+ } while (c.moveToNext());
+ c.close();
+ }
+
+ private void doTasksExport() throws IOException {
+ File xmlFile = new File(this.output);
+ xmlFile.createNewFile();
+ FileOutputStream fos = new FileOutputStream(xmlFile);
+ xml = Xml.newSerializer();
+ xml.setOutput(fos, XML_ENCODING);
+
+ xml.startDocument(null, null);
+ xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+
+ xml.startTag(null, ASTRID_TAG);
+ xml.attribute(null, ASTRID_ATTR_VERSION,
+ Integer.toString(Preferences.getCurrentVersion(ctx)));
+
+ openControllers();
+ initTagMap();
+ serializeTasks();
+ closeControllers();
+
+ xml.endTag(null, ASTRID_TAG);
+ xml.endDocument();
+ xml.flush();
+ fos.close();
+
+ if (!isService) {
+ displayToast();
+ }
+ }
+
+ private void displayToast() {
+ CharSequence text = String.format(ctx.getString(R.string.export_toast),
+ ctx.getResources().getQuantityString(R.plurals.Ntasks, exportCount,
+ exportCount), output);
+ Toast.makeText(ctx, text, Toast.LENGTH_LONG).show();
+ }
+
+ private void displayErrorToast(String error) {
+ Toast.makeText(ctx, error, Toast.LENGTH_LONG).show();
+ }
+
+ private void closeControllers() {
+ tagController.close();
+ taskController.close();
+ alertController.close();
+ syncDataController.close();
+ }
+
+ private void openControllers() {
+ taskController.open();
+ tagController.open();
+ alertController.open();
+ syncDataController.open();
+ }
+
+ public void exportTasks() {
+ if (isService && !Preferences.isBackupEnabled(ctx)) {
+ // Automatic backups are disabled.
+ return;
+ }
+ if (setupFile()) {
+ Thread thread = new Thread(doBackgroundExport);
+ thread.start();
+ }
+ }
+
+ public static File getExportDirectory() {
+ String storageState = Environment.getExternalStorageState();
+ if (storageState.equals(Environment.MEDIA_MOUNTED)) {
+ String path = Environment.getExternalStorageDirectory().getAbsolutePath();
+ path = path + ASTRID_DIR;
+ return new File(path);
+ }
+ return null;
+ }
+
+ private boolean setupFile() {
+ File astridDir = getExportDirectory();
+ if (astridDir != null) {
+ // Check for /sdcard/astrid directory. If it doesn't exist, make it.
+ if (astridDir.exists() || astridDir.mkdir()) {
+ String fileName;
+ if (isService) {
+ fileName = BACKUP_FILE_NAME;
+ } else {
+ fileName = EXPORT_FILE_NAME;
+ }
+ fileName = String.format(fileName, DateUtilities.getDateForExport());
+ setOutput(astridDir.getAbsolutePath() + "/" + fileName);
+ return true;
+ } else {
+ // Unable to make the /sdcard/astrid directory.
+ String error = ctx.getString(R.string.error_sdcard) + astridDir.getAbsolutePath();
+ Log.e("TasksXmlExporter", error);
+ if (!isService) {
+ displayErrorToast(error);
+ }
+ return false;
+ }
+ } else {
+ // Unable to access the sdcard because it's not in the mounted state.
+ String error = ctx.getString(R.string.error_sdcard);
+ Log.e("TasksXmlExporter", error);
+ if (!isService) {
+ displayErrorToast(error);
+ }
+ return false;
+ }
+ }
+
+ private void setOutput(String file) {
+ this.output = file;
+ }
+
+ private Runnable doBackgroundExport = new Runnable() {
+ public void run() {
+ Looper.prepare();
+ try {
+ doTasksExport();
+ } catch (IOException e) {
+ Log.e("TasksXmlExporter", "IOException in doTasksExport " + e.getMessage());
+ }
+ Looper.loop();
+ }
+ };
+
+ public void setTaskController(TaskController taskController) {
+ this.taskController = taskController;
+ }
+
+ public void setTagController(TagController tagController) {
+ this.tagController = tagController;
+ }
+
+ public void setAlertController(AlertController alertController) {
+ this.alertController = alertController;
+ }
+
+ public void setSyncDataController(SyncDataController syncDataController) {
+ this.syncDataController = syncDataController;
+ }
+
+ public void setContext(Context ctx) {
+ this.ctx = ctx;
+ setTaskController(new TaskController(ctx));
+ setTagController(new TagController(ctx));
+ setAlertController(new AlertController(ctx));
+ setSyncDataController(new SyncDataController(ctx));
+ }
+}
diff --git a/src/com/timsu/astrid/utilities/TasksXmlImporter.java b/src/com/timsu/astrid/utilities/TasksXmlImporter.java
new file mode 100644
index 000000000..b13a942f2
--- /dev/null
+++ b/src/com/timsu/astrid/utilities/TasksXmlImporter.java
@@ -0,0 +1,298 @@
+package com.timsu.astrid.utilities;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import com.timsu.astrid.R;
+import com.timsu.astrid.data.AbstractController;
+import com.timsu.astrid.data.alerts.AlertController;
+import com.timsu.astrid.data.sync.SyncDataController;
+import com.timsu.astrid.data.sync.SyncMapping;
+import com.timsu.astrid.data.tag.TagController;
+import com.timsu.astrid.data.tag.TagIdentifier;
+import com.timsu.astrid.data.tag.TagModelForView;
+import com.timsu.astrid.data.task.TaskController;
+import com.timsu.astrid.data.task.TaskIdentifier;
+import com.timsu.astrid.data.task.TaskModelForXml;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.util.Date;
+
+public class TasksXmlImporter {
+ public static final String TAG = "TasksXmlImporter";
+ public static final String ASTRID_TAG = TasksXmlExporter.ASTRID_TAG;
+ public static final String ASTRID_ATTR_VERSION = TasksXmlExporter.ASTRID_ATTR_VERSION;
+ public static final String TASK_TAG = TasksXmlExporter.TASK_TAG;
+ public static final String TAG_TAG = TasksXmlExporter.TAG_TAG;
+ public static final String ALERT_TAG = TasksXmlExporter.ALERT_TAG;
+ public static final String SYNC_TAG = TasksXmlExporter.SYNC_TAG;
+ public static final String TASK_ID = AbstractController.KEY_ROWID;
+ public static final String TASK_NAME = TaskModelForXml.NAME;
+ public static final String TASK_CREATION_DATE = TaskModelForXml.CREATION_DATE;
+ public static final String TAG_ATTR_NAME = TasksXmlExporter.TAG_ATTR_NAME;
+ public static final String ALERT_ATTR_DATE = TasksXmlExporter.ALERT_ATTR_DATE;
+ public static final String SYNC_ATTR_SERVICE = TasksXmlExporter.SYNC_ATTR_SERVICE;
+ public static final String SYNC_ATTR_REMOTE_ID = TasksXmlExporter.SYNC_ATTR_REMOTE_ID;
+
+ private TaskController taskController;
+ private TagController tagController;
+ private AlertController alertController;
+ private SyncDataController syncDataController;
+ private XmlPullParser xpp;
+ private String input;
+ private Handler importHandler;
+ private final Context context;
+ private int taskCount;
+ private int importCount;
+ private int skipCount;
+
+ static ProgressDialog progressDialog;
+
+ public TasksXmlImporter(Context context) {
+ this.context = context;
+ setContext(context);
+ }
+
+ private void setProgressMessage(final String message) {
+ importHandler.post(new Runnable() {
+ public void run() {
+ progressDialog.setMessage(message);
+ }
+ });
+ }
+
+ public void importTasks(final Runnable runAfterImport) {
+ importHandler = new Handler();
+ importHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ TasksXmlImporter.progressDialog = new ProgressDialog(context);
+ progressDialog.setIcon(android.R.drawable.ic_dialog_info);
+ progressDialog.setTitle(R.string.import_progress_title);
+ progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ progressDialog.setMessage(context.getString(R.string.import_progress_open));
+ progressDialog.setCancelable(false);
+ progressDialog.setIndeterminate(true);
+ progressDialog.show();
+ }
+ });
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Looper.prepare();
+ try {
+ performImport();
+ if (runAfterImport != null) {
+ importHandler.post(runAfterImport);
+ }
+ } catch (FileNotFoundException e) {
+ Log.e("TasksXmlImporter", e.getMessage());
+ } catch (XmlPullParserException e) {
+ Log.e("TasksXmlImporter", e.getMessage());
+ }
+ Looper.loop();
+ }
+ }).start();
+ }
+
+ private void performImport() throws FileNotFoundException, XmlPullParserException {
+ taskCount = 0;
+ importCount = 0;
+ skipCount = 0;
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ xpp = factory.newPullParser();
+ xpp.setInput(new FileReader(input));
+ setProgressMessage(context.getString(R.string.import_progress_opened));
+
+ openControllers();
+ try {
+ TaskModelForXml currentTask = null;
+ while (xpp.next() != XmlPullParser.END_DOCUMENT) {
+ String tag = xpp.getName();
+ if (xpp.getEventType() == XmlPullParser.END_TAG) {
+ // Ignore end tag.
+ continue;
+ }
+ if (tag != null) {
+ if (tag.equals(ASTRID_TAG)) {
+ // Process
+ // Perform version compatibility check?
+ }
+ else if (tag.equals(TASK_TAG)) {
+ // Parse
+ currentTask = parseTask();
+ } else if (currentTask != null) {
+ // These tags all require that we have a task to associate them with.
+ if (tag.equals(TAG_TAG)) {
+ // Process
+ parseTag(currentTask.getTaskIdentifier());
+ } else if (tag.equals(ALERT_TAG)) {
+ // Process
+ parseAlert(currentTask.getTaskIdentifier());
+ } else if (tag.equals(SYNC_TAG)) {
+ // Process
+ parseSync(currentTask.getTaskIdentifier());
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "import error " + e.getMessage());
+ } finally {
+ closeControllers();
+ progressDialog.dismiss();
+ showSummary();
+ }
+ }
+
+ private boolean parseSync(TaskIdentifier taskId) {
+ String service = xpp.getAttributeValue(null, SYNC_ATTR_SERVICE);
+ String remoteId = xpp.getAttributeValue(null, SYNC_ATTR_REMOTE_ID);
+ if (service != null && remoteId != null) {
+ int serviceInt = Integer.parseInt(service);
+ SyncMapping sm = new SyncMapping(taskId, serviceInt, remoteId);
+ syncDataController.saveSyncMapping(sm);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean parseAlert(TaskIdentifier taskId) {
+ String alert = xpp.getAttributeValue(null, ALERT_ATTR_DATE);
+ if (alert != null) {
+ Date alertDate = DateUtilities.getDateFromIso8601String(alert);
+ if (alertDate != null) {
+ if (! alertController.addAlert(taskId, alertDate)) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ return true;
+ }
+
+ private boolean parseTag(TaskIdentifier taskId) {
+ String tagName = xpp.getAttributeValue(null, TAG_ATTR_NAME);
+ if (tagName != null) {
+ TagIdentifier tagId;
+ TagModelForView tagModel;
+ tagModel = tagController.fetchTagFromName(tagName);
+ if (tagModel == null) {
+ // Tag not found, create a new one.
+ tagId = tagController.createTag(tagName);
+ } else {
+ tagId = tagModel.getTagIdentifier();
+ }
+ if (! tagController.addTag(taskId, tagId)) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ return true;
+ }
+
+ private TaskModelForXml parseTask() {
+ taskCount++;
+ setProgressMessage(context.getString(R.string.import_progress_read, taskCount));
+ TaskModelForXml task = null;
+ String taskName = xpp.getAttributeValue(null, TASK_NAME);
+
+ Date creationDate = null;
+ String createdString = xpp.getAttributeValue(null, TASK_CREATION_DATE);
+ if (createdString != null) {
+ creationDate = DateUtilities.getDateFromIso8601String(createdString);
+ }
+ // If the task's name and creation date match an existing task, skip it.
+ if (creationDate != null && taskName != null) {
+ task = taskController.fetchTaskForXml(taskName, creationDate);
+ }
+ if (task != null) {
+ // Skip this task.
+ skipCount++;
+ setProgressMessage(context.getString(R.string.import_progress_skip, taskCount));
+ // Set currentTask to null so we skip its alerts/syncs/tags, too.
+ return null;
+ }
+ // Else, make a new task model and add away.
+ task = new TaskModelForXml();
+ int numAttributes = xpp.getAttributeCount();
+ for (int i = 0; i < numAttributes; i++) {
+ String fieldName = xpp.getAttributeName(i);
+ String fieldValue = xpp.getAttributeValue(i);
+ task.setField(fieldName, fieldValue);
+ }
+ // Save the task to the database.
+ taskController.saveTask(task, false);
+ importCount++;
+ setProgressMessage(context.getString(R.string.import_progress_add, taskCount));
+ return task;
+ }
+
+ private void showSummary() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.import_summary_title);
+ String message = context.getString(R.string.import_summary_message,
+ input, taskCount, importCount, skipCount);
+ builder.setMessage(message);
+ builder.setPositiveButton(context.getString(android.R.string.ok),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ }
+ });
+ builder.show();
+ }
+
+ public void setTaskController(TaskController taskController) {
+ this.taskController = taskController;
+ }
+
+ public void setTagController(TagController tagController) {
+ this.tagController = tagController;
+ }
+
+ public void setAlertController(AlertController alertController) {
+ this.alertController = alertController;
+ }
+
+ public void setSyncDataController(SyncDataController syncDataController) {
+ this.syncDataController = syncDataController;
+ }
+
+ public void setContext(Context ctx) {
+ setTaskController(new TaskController(ctx));
+ setTagController(new TagController(ctx));
+ setAlertController(new AlertController(ctx));
+ setSyncDataController(new SyncDataController(ctx));
+ }
+
+ private void closeControllers() {
+ taskController.close();
+ tagController.close();
+ alertController.close();
+ syncDataController.close();
+ }
+
+ private void openControllers() {
+ taskController.open();
+ tagController.open();
+ alertController.open();
+ syncDataController.open();
+ }
+
+ public void setInput(String input) {
+ this.input = input;
+ }
+}
diff --git a/src/com/timsu/astrid/widget/FilePickerBuilder.java b/src/com/timsu/astrid/widget/FilePickerBuilder.java
new file mode 100644
index 000000000..bd4c5d8e1
--- /dev/null
+++ b/src/com/timsu/astrid/widget/FilePickerBuilder.java
@@ -0,0 +1,58 @@
+package com.timsu.astrid.widget;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class FilePickerBuilder extends AlertDialog.Builder implements DialogInterface.OnClickListener {
+
+ public interface OnFilePickedListener {
+ void onFilePicked(String filePath);
+ }
+
+ private OnFilePickedListener callback;
+ private String[] files;
+ private String path;
+ private FilenameFilter filter;
+
+ public FilePickerBuilder(Context ctx, String title, File path, OnFilePickedListener callback) {
+ super(ctx);
+ filter = new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String s) {
+ File file = new File(dir, s);
+ return file.isFile();
+ }
+ };
+ setTitle(title);
+ setPath(path);
+ this.callback = callback;
+ }
+
+ public void setFilter(FilenameFilter filter) {
+ this.filter = filter;
+ }
+
+ private void setPath(File path) {
+ this.path = path.getAbsolutePath();
+ // Reverse the order of the file list so newest timestamped file is first.
+ List fileList = Arrays.asList(path.list(filter));
+ Collections.sort(fileList);
+ Collections.reverse(fileList);
+ files = (String[])fileList.toArray();
+ setItems(files, this);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ if (callback != null) {
+ callback.onFilePicked(path + "/" + files[i]);
+ }
+ }
+}