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]); + } + } +}