@Override
  public void onReceive(Context context, Intent intent) {
    ContextManager.setContext(context);
    DependencyInjectionService.getInstance().inject(this);
    long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1);
    if (taskId == -1) return;

    Task task = PluginServices.getTaskService().fetchById(taskId, Task.PROPERTIES);
    if (task == null || !task.isCompleted()) return;

    String recurrence = task.getValue(Task.RECURRENCE);
    if (recurrence != null && recurrence.length() > 0) {
      long newDueDate;
      try {
        newDueDate = computeNextDueDate(task, recurrence);
        if (newDueDate == -1) return;
      } catch (ParseException e) {
        PluginServices.getExceptionService().reportError("repeat-parse", e); // $NON-NLS-1$
        return;
      }

      StatisticsService.reportEvent(StatisticsConstants.V2_TASK_REPEAT);

      long oldDueDate = task.getValue(Task.DUE_DATE);
      long repeatUntil = task.getValue(Task.REPEAT_UNTIL);

      boolean repeatFinished = repeatUntil > 0 && newDueDate >= repeatUntil;
      if (repeatFinished) {
        Intent repeatFinishedIntent =
            new Intent(AstridApiConstants.BROADCAST_EVENT_TASK_REPEAT_FINISHED);
        repeatFinishedIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, task.getId());
        repeatFinishedIntent.putExtra(AstridApiConstants.EXTRAS_OLD_DUE_DATE, oldDueDate);
        repeatFinishedIntent.putExtra(AstridApiConstants.EXTRAS_NEW_DUE_DATE, newDueDate);
        context.sendOrderedBroadcast(repeatFinishedIntent, null);
        return;
      }

      rescheduleTask(task, newDueDate);

      // send a broadcast
      Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_TASK_REPEATED);
      broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, task.getId());
      broadcastIntent.putExtra(AstridApiConstants.EXTRAS_OLD_DUE_DATE, oldDueDate);
      broadcastIntent.putExtra(AstridApiConstants.EXTRAS_NEW_DUE_DATE, newDueDate);
      context.sendOrderedBroadcast(broadcastIntent, null);
      Flags.set(Flags.REFRESH);
      return;
    }
  }
public class TasksXmlExporter {

  // --- public interface

  /**
   * Import tasks from the given file
   *
   * @param context context
   * @param isService if false, displays ui dialogs
   * @param runAfterExport runnable to run after exporting
   * @param backupDirectoryOverride new backupdirectory, or null to use default
   */
  public static void exportTasks(
      Context context, boolean isService, Runnable runAfterExport, File backupDirectoryOverride) {
    new TasksXmlExporter(context, isService, runAfterExport, backupDirectoryOverride);
  }

  // --- implementation

  private static final int FORMAT = 2;

  private final Context context;
  private int exportCount = 0;
  private XmlSerializer xml;
  private final TaskService taskService = PluginServices.getTaskService();
  private final MetadataService metadataService = PluginServices.getMetadataService();
  private final ExceptionService exceptionService = PluginServices.getExceptionService();

  private final ProgressDialog progressDialog;
  private final Handler handler;
  private final File backupDirectory;

  private void setProgress(final int taskNumber, final int total) {
    handler.post(
        new Runnable() {
          public void run() {
            progressDialog.setMax(total);
            progressDialog.setProgress(taskNumber);
          }
        });
  }

  private TasksXmlExporter(
      final Context context,
      final boolean isService,
      final Runnable runAfterExport,
      File backupDirectoryOverride) {
    this.context = context;
    this.exportCount = 0;
    this.backupDirectory =
        backupDirectoryOverride == null
            ? BackupConstants.defaultExportDirectory()
            : backupDirectoryOverride;

    handler = new Handler();
    progressDialog = new ProgressDialog(context);
    if (!isService) {
      progressDialog.setIcon(android.R.drawable.ic_dialog_info);
      progressDialog.setTitle(R.string.export_progress_title);
      progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
      progressDialog.setProgress(0);
      progressDialog.setCancelable(false);
      progressDialog.setIndeterminate(false);
      progressDialog.show();
      if (context instanceof Activity) progressDialog.setOwnerActivity((Activity) context);
    }

    new Thread(
            new Runnable() {
              @Override
              public void run() {
                try {
                  String output = setupFile(backupDirectory, isService);
                  int tasks = taskService.countTasks();

                  if (tasks > 0) doTasksExport(output);

                  Preferences.setLong(BackupPreferences.PREF_BACKUP_LAST_DATE, DateUtilities.now());
                  Preferences.setString(BackupPreferences.PREF_BACKUP_LAST_ERROR, null);

                  if (!isService) onFinishExport(output);
                } catch (IOException e) {
                  if (!isService)
                    exceptionService.displayAndReportError(
                        context, context.getString(R.string.backup_TXI_error), e);
                  else {
                    exceptionService.reportError("background-backup", e); // $NON-NLS-1$
                    Preferences.setString(BackupPreferences.PREF_BACKUP_LAST_ERROR, e.toString());
                  }
                } finally {
                  if (runAfterExport != null) runAfterExport.run();
                }
              }
            })
        .start();
  }

  @SuppressWarnings("nls")
  private void doTasksExport(String output) throws IOException {
    File xmlFile = new File(output);
    xmlFile.createNewFile();
    FileOutputStream fos = new FileOutputStream(xmlFile);
    xml = Xml.newSerializer();
    xml.setOutput(fos, BackupConstants.XML_ENCODING);

    xml.startDocument(null, null);
    xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);

    xml.startTag(null, BackupConstants.ASTRID_TAG);
    xml.attribute(
        null,
        BackupConstants.ASTRID_ATTR_VERSION,
        Integer.toString(AstridPreferences.getCurrentVersion()));
    xml.attribute(null, BackupConstants.ASTRID_ATTR_FORMAT, Integer.toString(FORMAT));

    serializeTasks();

    xml.endTag(null, BackupConstants.ASTRID_TAG);
    xml.endDocument();
    xml.flush();
    fos.close();
  }

  private void serializeTasks() throws IOException {
    TodorooCursor<Task> cursor =
        taskService.query(Query.select(Task.PROPERTIES).orderBy(Order.asc(Task.ID)));
    try {
      Task task = new Task();
      int length = cursor.getCount();
      for (int i = 0; i < length; i++) {
        cursor.moveToNext();
        task.readFromCursor(cursor);

        setProgress(i, length);

        xml.startTag(null, BackupConstants.TASK_TAG);
        serializeModel(task, Task.PROPERTIES, Task.ID);
        serializeMetadata(task);
        xml.endTag(null, BackupConstants.TASK_TAG);
        this.exportCount++;
      }
    } finally {
      cursor.close();
    }
  }

  private synchronized void serializeMetadata(Task task) throws IOException {
    TodorooCursor<Metadata> cursor =
        metadataService.query(
            Query.select(Metadata.PROPERTIES).where(MetadataCriteria.byTask(task.getId())));
    try {
      Metadata metadata = new Metadata();
      for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
        metadata.readFromCursor(cursor);

        xml.startTag(null, BackupConstants.METADATA_TAG);
        serializeModel(metadata, Metadata.PROPERTIES, Metadata.ID, Metadata.TASK);
        xml.endTag(null, BackupConstants.METADATA_TAG);
      }
    } finally {
      cursor.close();
    }
  }

  /**
   * Turn a model into xml attributes
   *
   * @param model
   */
  private void serializeModel(
      AbstractModel model, Property<?>[] properties, Property<?>... excludes) {
    outer:
    for (Property<?> property : properties) {
      for (Property<?> exclude : excludes) if (property.name.equals(exclude.name)) continue outer;

      try {
        property.accept(xmlWritingVisitor, model);
      } catch (Exception e) {
        Log.e(
            "astrid-exporter", //$NON-NLS-1$
            "Caught exception while reading "
                + property.name
                + //$NON-NLS-1$
                " from "
                + model.getDatabaseValues(),
            e); //$NON-NLS-1$
      }
    }
  }

  private final XmlWritingPropertyVisitor xmlWritingVisitor = new XmlWritingPropertyVisitor();

  private class XmlWritingPropertyVisitor implements PropertyVisitor<Void, AbstractModel> {
    @Override
    public Void visitInteger(Property<Integer> property, AbstractModel data) {
      try {
        xml.attribute(null, property.name, data.getValue(property).toString());
      } catch (UnsupportedOperationException e) {
        // didn't read this value, do nothing
      } catch (IllegalArgumentException e) {
        throw new RuntimeException(e);
      } catch (IllegalStateException e) {
        throw new RuntimeException(e);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
      return null;
    }

    @Override
    public Void visitLong(Property<Long> property, AbstractModel data) {
      try {
        xml.attribute(null, property.name, data.getValue(property).toString());
      } catch (UnsupportedOperationException e) {
        // didn't read this value, do nothing
      } catch (IllegalArgumentException e) {
        throw new RuntimeException(e);
      } catch (IllegalStateException e) {
        throw new RuntimeException(e);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
      return null;
    }

    @Override
    public Void visitDouble(Property<Double> property, AbstractModel data) {
      try {
        xml.attribute(null, property.name, data.getValue(property).toString());
      } catch (UnsupportedOperationException e) {
        // didn't read this value, do nothing
      } catch (IllegalArgumentException e) {
        throw new RuntimeException(e);
      } catch (IllegalStateException e) {
        throw new RuntimeException(e);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
      return null;
    }

    @Override
    public Void visitString(Property<String> property, AbstractModel data) {
      try {
        String value = data.getValue(property);
        if (value == null) return null;
        xml.attribute(null, property.name, value);
      } catch (UnsupportedOperationException e) {
        // didn't read this value, do nothing
      } catch (IllegalArgumentException e) {
        throw new RuntimeException(e);
      } catch (IllegalStateException e) {
        throw new RuntimeException(e);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
      return null;
    }
  }

  private void onFinishExport(final String outputFile) {
    handler.post(
        new Runnable() {
          @Override
          public void run() {

            if (exportCount == 0)
              Toast.makeText(
                      context, context.getString(R.string.export_toast_no_tasks), Toast.LENGTH_LONG)
                  .show();
            else {
              CharSequence text =
                  String.format(
                      context.getString(R.string.export_toast),
                      context
                          .getResources()
                          .getQuantityString(R.plurals.Ntasks, exportCount, exportCount),
                      outputFile);
              Toast.makeText(context, text, Toast.LENGTH_LONG).show();
              if (progressDialog.isShowing() && context instanceof Activity)
                DialogUtilities.dismissDialog((Activity) context, progressDialog);
            }
          }
        });
  }

  /**
   * Creates directories if necessary and returns fully qualified file
   *
   * @param directory
   * @return output file name
   * @throws IOException
   */
  private String setupFile(File directory, boolean isService) throws IOException {
    File astridDir = directory;
    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 = BackupConstants.BACKUP_FILE_NAME;
        } else {
          fileName = BackupConstants.EXPORT_FILE_NAME;
        }
        fileName = String.format(fileName, BackupDateUtilities.getDateForExport());
        return astridDir.getAbsolutePath() + File.separator + fileName;
      } else {
        // Unable to make the /sdcard/astrid directory.
        throw new IOException(
            context.getString(R.string.DLG_error_sdcard, astridDir.getAbsolutePath()));
      }
    } else {
      // Unable to access the sdcard because it's not in the mounted state.
      throw new IOException(context.getString(R.string.DLG_error_sdcard_general));
    }
  }
}