mirror of https://github.com/tasks/tasks
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
581 lines
24 KiB
Java
581 lines
24 KiB
Java
package com.todoroo.astrid.backup;
|
|
|
|
import java.io.FileReader;
|
|
import java.io.IOException;
|
|
import java.util.Date;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.StringTokenizer;
|
|
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
import org.xmlpull.v1.XmlPullParserFactory;
|
|
|
|
import android.app.Activity;
|
|
import android.app.AlertDialog;
|
|
import android.app.ProgressDialog;
|
|
import android.content.Context;
|
|
import android.content.DialogInterface;
|
|
import android.content.Intent;
|
|
import android.content.res.Resources;
|
|
import android.os.Handler;
|
|
import android.text.TextUtils;
|
|
import android.util.Log;
|
|
|
|
import com.google.ical.values.RRule;
|
|
import com.timsu.astrid.R;
|
|
import com.todoroo.andlib.data.AbstractModel;
|
|
import com.todoroo.andlib.data.Property;
|
|
import com.todoroo.andlib.data.Property.PropertyVisitor;
|
|
import com.todoroo.andlib.data.TodorooCursor;
|
|
import com.todoroo.andlib.service.ContextManager;
|
|
import com.todoroo.andlib.service.ExceptionService;
|
|
import com.todoroo.andlib.sql.Criterion;
|
|
import com.todoroo.andlib.sql.Query;
|
|
import com.todoroo.andlib.utility.DateUtilities;
|
|
import com.todoroo.astrid.api.AstridApiConstants;
|
|
import com.todoroo.astrid.core.PluginServices;
|
|
import com.todoroo.astrid.data.Metadata;
|
|
import com.todoroo.astrid.data.Task;
|
|
import com.todoroo.astrid.legacy.LegacyImportance;
|
|
import com.todoroo.astrid.legacy.LegacyRepeatInfo;
|
|
import com.todoroo.astrid.legacy.LegacyRepeatInfo.LegacyRepeatInterval;
|
|
import com.todoroo.astrid.legacy.LegacyTaskModel;
|
|
import com.todoroo.astrid.service.MetadataService;
|
|
import com.todoroo.astrid.service.TaskService;
|
|
import com.todoroo.astrid.tags.TagService;
|
|
|
|
public class TasksXmlImporter {
|
|
|
|
// --- public interface
|
|
|
|
/**
|
|
* Import tasks from the given file
|
|
*
|
|
* @param input
|
|
* @param runAfterImport
|
|
*/
|
|
public static void importTasks(Context context, String input, Runnable runAfterImport) {
|
|
new TasksXmlImporter(context, input, runAfterImport);
|
|
}
|
|
|
|
// --- implementation
|
|
|
|
private final Handler handler;
|
|
private int taskCount;
|
|
private int importCount = 0;
|
|
private int skipCount = 0;
|
|
private int errorCount = 0;
|
|
private final String input;
|
|
|
|
private final Context context;
|
|
private final TaskService taskService = PluginServices.getTaskService();
|
|
private final MetadataService metadataService = PluginServices.getMetadataService();
|
|
private final ExceptionService exceptionService = PluginServices.getExceptionService();
|
|
private final ProgressDialog progressDialog;
|
|
private final Runnable runAfterImport;
|
|
|
|
private void setProgressMessage(final String message) {
|
|
handler.post(new Runnable() {
|
|
public void run() {
|
|
progressDialog.setMessage(message);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Import tasks.
|
|
* @param runAfterImport optional runnable after import
|
|
*/
|
|
private TasksXmlImporter(final Context context, String input, Runnable runAfterImport) {
|
|
this.input = input;
|
|
this.context = context;
|
|
this.runAfterImport = runAfterImport;
|
|
|
|
handler = new Handler();
|
|
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.setCancelable(false);
|
|
progressDialog.setIndeterminate(true);
|
|
progressDialog.show();
|
|
if(context instanceof Activity)
|
|
progressDialog.setOwnerActivity((Activity)context);
|
|
|
|
new Thread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
performImport();
|
|
} catch (IOException e) {
|
|
exceptionService.displayAndReportError(context,
|
|
context.getString(R.string.backup_TXI_error), e);
|
|
} catch (XmlPullParserException e) {
|
|
exceptionService.displayAndReportError(context,
|
|
context.getString(R.string.backup_TXI_error), e);
|
|
}
|
|
}
|
|
}).start();
|
|
}
|
|
|
|
@SuppressWarnings("nls")
|
|
private void performImport() throws IOException, XmlPullParserException {
|
|
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
|
XmlPullParser xpp = factory.newPullParser();
|
|
xpp.setInput(new FileReader(input));
|
|
|
|
try {
|
|
while (xpp.next() != XmlPullParser.END_DOCUMENT) {
|
|
String tag = xpp.getName();
|
|
if (xpp.getEventType() == XmlPullParser.END_TAG) {
|
|
// Ignore end tags
|
|
continue;
|
|
}
|
|
if (tag != null) {
|
|
// Process <astrid ... >
|
|
if (tag.equals(BackupConstants.ASTRID_TAG)) {
|
|
String format = xpp.getAttributeValue(null, BackupConstants.ASTRID_ATTR_FORMAT);
|
|
if(TextUtils.equals(format, FORMAT1))
|
|
new Format1TaskImporter(xpp);
|
|
else if(TextUtils.equals(format, FORMAT2))
|
|
new Format2TaskImporter(xpp);
|
|
else
|
|
throw new UnsupportedOperationException(
|
|
"Did not know how to import tasks with xml format '" +
|
|
format + "'");
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_REFRESH);
|
|
ContextManager.getContext().sendBroadcast(broadcastIntent, AstridApiConstants.PERMISSION_READ);
|
|
handler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if(progressDialog != null && progressDialog.isShowing())
|
|
progressDialog.dismiss();
|
|
showSummary();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private void showSummary() {
|
|
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
|
builder.setTitle(R.string.import_summary_title);
|
|
Resources r = context.getResources();
|
|
String message = context.getString(R.string.import_summary_message,
|
|
input,
|
|
r.getQuantityString(R.plurals.Ntasks, taskCount, taskCount),
|
|
r.getQuantityString(R.plurals.Ntasks, importCount, importCount),
|
|
r.getQuantityString(R.plurals.Ntasks, skipCount, skipCount),
|
|
r.getQuantityString(R.plurals.Ntasks, errorCount, errorCount));
|
|
builder.setMessage(message);
|
|
builder.setPositiveButton(context.getString(android.R.string.ok),
|
|
new DialogInterface.OnClickListener() {
|
|
public void onClick(DialogInterface dialog, int id) {
|
|
dialog.dismiss();
|
|
if (runAfterImport != null) {
|
|
handler.post(runAfterImport);
|
|
}
|
|
}
|
|
});
|
|
|
|
builder.show();
|
|
}
|
|
|
|
// --- importers
|
|
|
|
// =============================================================== FORMAT2
|
|
|
|
private static final String FORMAT2 = "2"; //$NON-NLS-1$
|
|
private class Format2TaskImporter {
|
|
|
|
private final XmlPullParser xpp;
|
|
private final Task currentTask = new Task();
|
|
private final Metadata metadata = new Metadata();
|
|
|
|
public Format2TaskImporter(XmlPullParser xpp) throws XmlPullParserException, IOException {
|
|
this.xpp = xpp;
|
|
while (xpp.next() != XmlPullParser.END_DOCUMENT) {
|
|
String tag = xpp.getName();
|
|
if (tag == null || xpp.getEventType() == XmlPullParser.END_TAG)
|
|
continue;
|
|
|
|
try {
|
|
if (tag.equals(BackupConstants.TASK_TAG)) {
|
|
// Parse <task ... >
|
|
parseTask();
|
|
} else if (tag.equals(BackupConstants.METADATA_TAG)) {
|
|
// Process <metadata ... >
|
|
parseMetadata();
|
|
}
|
|
} catch (Exception e) {
|
|
errorCount++;
|
|
Log.e("astrid-importer", //$NON-NLS-1$
|
|
"Caught exception while reading from " + //$NON-NLS-1$
|
|
xpp.getText(), e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void parseTask() {
|
|
taskCount++;
|
|
setProgressMessage(context.getString(R.string.import_progress_read,
|
|
taskCount));
|
|
currentTask.clear();
|
|
|
|
String title = xpp.getAttributeValue(null, Task.TITLE.name);
|
|
String created = xpp.getAttributeValue(null, Task.CREATION_DATE.name);
|
|
String dueDate = xpp.getAttributeValue(null, Task.DUE_DATE.name);
|
|
String completionDate = xpp.getAttributeValue(null, Task.COMPLETION_DATE.name);
|
|
|
|
// if we don't have task name or creation date, skip
|
|
if (created == null || title == null || dueDate == null
|
|
|| completionDate == null) {
|
|
skipCount++;
|
|
return;
|
|
}
|
|
|
|
// if the task's name and creation date match an existing task, skip
|
|
TodorooCursor<Task> cursor = taskService.query(Query.select(Task.ID).
|
|
where(Criterion.and(Task.TITLE.eq(title),
|
|
Task.CREATION_DATE.eq(created),
|
|
Task.DUE_DATE.eq(dueDate),
|
|
Task.COMPLETION_DATE.eq(completionDate))));
|
|
try {
|
|
if(cursor.getCount() > 0) {
|
|
skipCount++;
|
|
return;
|
|
}
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
|
|
// else, make a new task model and add away.
|
|
deserializeModel(currentTask, Task.PROPERTIES);
|
|
currentTask.setId(Task.NO_ID);
|
|
|
|
// Save the task to the database.
|
|
taskService.save(currentTask);
|
|
importCount++;
|
|
}
|
|
|
|
private void parseMetadata() {
|
|
if(!currentTask.isSaved())
|
|
return;
|
|
metadata.clear();
|
|
deserializeModel(metadata, Metadata.PROPERTIES);
|
|
metadata.setId(Metadata.NO_ID);
|
|
metadata.setValue(Metadata.TASK, currentTask.getId());
|
|
metadataService.save(metadata);
|
|
}
|
|
|
|
/**
|
|
* Turn a model into xml attributes
|
|
* @param model
|
|
*/
|
|
private void deserializeModel(AbstractModel model, Property<?>[] properties) {
|
|
for(Property<?> property : properties) {
|
|
try {
|
|
property.accept(xmlReadingVisitor, model);
|
|
} catch (Exception e) {
|
|
Log.e("astrid-importer", //$NON-NLS-1$
|
|
"Caught exception while writing " + property.name + //$NON-NLS-1$
|
|
" from " + xpp.getText(), e); //$NON-NLS-1$
|
|
}
|
|
}
|
|
}
|
|
|
|
private final XmlReadingPropertyVisitor xmlReadingVisitor = new XmlReadingPropertyVisitor();
|
|
|
|
private class XmlReadingPropertyVisitor implements PropertyVisitor<Void, AbstractModel> {
|
|
@Override
|
|
public Void visitInteger(Property<Integer> property,
|
|
AbstractModel data) {
|
|
String value = xpp.getAttributeValue(null, property.name);
|
|
if(value != null)
|
|
data.setValue(property, Integer.parseInt(value));
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public Void visitLong(Property<Long> property, AbstractModel data) {
|
|
String value = xpp.getAttributeValue(null, property.name);
|
|
if(value != null)
|
|
data.setValue(property, Long.parseLong(value));
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public Void visitDouble(Property<Double> property,
|
|
AbstractModel data) {
|
|
String value = xpp.getAttributeValue(null, property.name);
|
|
if(value != null)
|
|
data.setValue(property, Double.parseDouble(value));
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public Void visitString(Property<String> property,
|
|
AbstractModel data) {
|
|
String value = xpp.getAttributeValue(null, property.name);
|
|
if(value != null)
|
|
data.setValue(property, value);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================== FORMAT1
|
|
|
|
private static final String FORMAT1 = null;
|
|
private class Format1TaskImporter {
|
|
|
|
private final XmlPullParser xpp;
|
|
private Task currentTask = null;
|
|
private String upgradeNotes = null;
|
|
private boolean syncOnComplete = false;
|
|
|
|
private final LinkedHashSet<String> tags = new LinkedHashSet<String>();
|
|
|
|
public Format1TaskImporter(XmlPullParser xpp) throws XmlPullParserException, IOException {
|
|
this.xpp = xpp;
|
|
|
|
while (xpp.next() != XmlPullParser.END_DOCUMENT) {
|
|
String tag = xpp.getName();
|
|
|
|
try {
|
|
if(BackupConstants.TASK_TAG.equals(tag) && xpp.getEventType() == XmlPullParser.END_TAG)
|
|
saveTags();
|
|
else if (tag == null || xpp.getEventType() == XmlPullParser.END_TAG)
|
|
continue;
|
|
else if (tag.equals(BackupConstants.TASK_TAG)) {
|
|
// Parse <task ... >
|
|
currentTask = parseTask();
|
|
} else if (currentTask != null) {
|
|
// These tags all require that we have a task to associate
|
|
// them with.
|
|
if (tag.equals(BackupConstants.TAG_TAG)) {
|
|
// Process <tag ... >
|
|
parseTag();
|
|
} else if (tag.equals(BackupConstants.ALERT_TAG)) {
|
|
// Process <alert ... >
|
|
parseAlert();
|
|
} else if (tag.equals(BackupConstants.SYNC_TAG)) {
|
|
// Process <sync ... >
|
|
parseSync();
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
errorCount++;
|
|
Log.e("astrid-importer", //$NON-NLS-1$
|
|
"Caught exception while reading from " + //$NON-NLS-1$
|
|
xpp.getText(), e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean parseSync() {
|
|
String service = xpp.getAttributeValue(null, BackupConstants.SYNC_ATTR_SERVICE);
|
|
String remoteId = xpp.getAttributeValue(null, BackupConstants.SYNC_ATTR_REMOTE_ID);
|
|
if (service != null && remoteId != null) {
|
|
StringTokenizer strtok = new StringTokenizer(remoteId, "|"); //$NON-NLS-1$
|
|
String taskId = strtok.nextToken();
|
|
String taskSeriesId = strtok.nextToken();
|
|
String listId = strtok.nextToken();
|
|
|
|
Metadata metadata = new Metadata();
|
|
metadata.setValue(Metadata.TASK, currentTask.getId());
|
|
metadata.setValue(Metadata.VALUE1, (listId));
|
|
metadata.setValue(Metadata.VALUE2, (taskSeriesId));
|
|
metadata.setValue(Metadata.VALUE3, (taskId));
|
|
metadata.setValue(Metadata.VALUE4, syncOnComplete ? "1" : "0"); //$NON-NLS-1$ //$NON-NLS-2$
|
|
metadataService.save(metadata);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private boolean parseAlert() {
|
|
// drop it
|
|
return false;
|
|
}
|
|
|
|
private boolean parseTag() {
|
|
String tagName = xpp.getAttributeValue(null, BackupConstants.TAG_ATTR_NAME);
|
|
tags.add(tagName);
|
|
return true;
|
|
}
|
|
|
|
private void saveTags() {
|
|
if(currentTask != null && tags.size() > 0) {
|
|
TagService.getInstance().synchronizeTags(currentTask.getId(), tags);
|
|
}
|
|
tags.clear();
|
|
}
|
|
|
|
@SuppressWarnings("nls")
|
|
private Task parseTask() {
|
|
taskCount++;
|
|
setProgressMessage(context.getString(R.string.import_progress_read,
|
|
taskCount));
|
|
|
|
String taskName = xpp.getAttributeValue(null, LegacyTaskModel.NAME);
|
|
Date creationDate = null;
|
|
String createdString = xpp.getAttributeValue(null,
|
|
LegacyTaskModel.CREATION_DATE);
|
|
if (createdString != null) {
|
|
creationDate = BackupDateUtilities.getDateFromIso8601String(createdString);
|
|
}
|
|
|
|
// if we don't have task name or creation date, skip
|
|
if (creationDate == null || taskName == null) {
|
|
skipCount++;
|
|
return null;
|
|
}
|
|
|
|
// if the task's name and creation date match an existing task, skip
|
|
TodorooCursor<Task> cursor = taskService.query(Query.select(Task.ID).
|
|
where(Criterion.and(Task.TITLE.eq(taskName),
|
|
Task.CREATION_DATE.like(creationDate.getTime()/1000L + "%"))));
|
|
try {
|
|
if(cursor.getCount() > 0) {
|
|
skipCount++;
|
|
return null;
|
|
}
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
|
|
// else, make a new task model and add away.
|
|
Task task = new Task();
|
|
int numAttributes = xpp.getAttributeCount();
|
|
for (int i = 0; i < numAttributes; i++) {
|
|
String fieldName = xpp.getAttributeName(i);
|
|
String fieldValue = xpp.getAttributeValue(i);
|
|
if(!setTaskField(task, fieldName, fieldValue)) {
|
|
Log.i("astrid-xml-import", "Task: " + taskName + ": Unknown field '" +
|
|
fieldName + "' with value '" + fieldValue + "' disregarded.");
|
|
}
|
|
}
|
|
|
|
if(upgradeNotes != null) {
|
|
if(task.containsValue(Task.NOTES) && task.getValue(Task.NOTES).length() > 0)
|
|
task.setValue(Task.NOTES, task.getValue(Task.NOTES) + "\n" + upgradeNotes);
|
|
else
|
|
task.setValue(Task.NOTES, upgradeNotes);
|
|
upgradeNotes = null;
|
|
}
|
|
|
|
// Save the task to the database.
|
|
taskService.save(task);
|
|
importCount++;
|
|
return task;
|
|
}
|
|
|
|
/** helper method to set field on a task */
|
|
@SuppressWarnings("nls")
|
|
private final boolean setTaskField(Task task, String field, String value) {
|
|
if(field.equals(LegacyTaskModel.ID)) {
|
|
// ignore
|
|
}
|
|
else if(field.equals(LegacyTaskModel.NAME)) {
|
|
task.setValue(Task.TITLE, value);
|
|
}
|
|
else if(field.equals(LegacyTaskModel.NOTES)) {
|
|
task.setValue(Task.NOTES, value);
|
|
}
|
|
else if(field.equals(LegacyTaskModel.PROGRESS_PERCENTAGE)) {
|
|
// ignore
|
|
}
|
|
else if(field.equals(LegacyTaskModel.IMPORTANCE)) {
|
|
task.setValue(Task.IMPORTANCE, LegacyImportance.valueOf(value).ordinal());
|
|
}
|
|
else if(field.equals(LegacyTaskModel.ESTIMATED_SECONDS)) {
|
|
task.setValue(Task.ESTIMATED_SECONDS, Integer.parseInt(value));
|
|
}
|
|
else if(field.equals(LegacyTaskModel.ELAPSED_SECONDS)) {
|
|
task.setValue(Task.ELAPSED_SECONDS, Integer.parseInt(value));
|
|
}
|
|
else if(field.equals(LegacyTaskModel.TIMER_START)) {
|
|
task.setValue(Task.TIMER_START,
|
|
BackupDateUtilities.getDateFromIso8601String(value).getTime());
|
|
}
|
|
else if(field.equals(LegacyTaskModel.DEFINITE_DUE_DATE)) {
|
|
String preferred = xpp.getAttributeValue(null, LegacyTaskModel.PREFERRED_DUE_DATE);
|
|
if(preferred != null) {
|
|
Date preferredDate = BackupDateUtilities.getDateFromIso8601String(value);
|
|
upgradeNotes = "Project Deadline: " +
|
|
DateUtilities.getDateString(ContextManager.getContext(),
|
|
preferredDate);
|
|
}
|
|
task.setValue(Task.DUE_DATE,
|
|
BackupDateUtilities.getDateFromIso8601String(value).getTime());
|
|
}
|
|
else if(field.equals(LegacyTaskModel.PREFERRED_DUE_DATE)) {
|
|
String definite = xpp.getAttributeValue(null, LegacyTaskModel.DEFINITE_DUE_DATE);
|
|
if(definite != null)
|
|
; // handled above
|
|
else
|
|
task.setValue(Task.DUE_DATE,
|
|
BackupDateUtilities.getDateFromIso8601String(value).getTime());
|
|
}
|
|
else if(field.equals(LegacyTaskModel.HIDDEN_UNTIL)) {
|
|
task.setValue(Task.HIDE_UNTIL,
|
|
BackupDateUtilities.getDateFromIso8601String(value).getTime());
|
|
}
|
|
else if(field.equals(LegacyTaskModel.BLOCKING_ON)) {
|
|
// ignore
|
|
}
|
|
else if(field.equals(LegacyTaskModel.POSTPONE_COUNT)) {
|
|
task.setValue(Task.POSTPONE_COUNT, Integer.parseInt(value));
|
|
}
|
|
else if(field.equals(LegacyTaskModel.NOTIFICATIONS)) {
|
|
task.setValue(Task.REMINDER_PERIOD, Integer.parseInt(value) * 1000L);
|
|
}
|
|
else if(field.equals(LegacyTaskModel.CREATION_DATE)) {
|
|
task.setValue(Task.CREATION_DATE,
|
|
BackupDateUtilities.getDateFromIso8601String(value).getTime());
|
|
}
|
|
else if(field.equals(LegacyTaskModel.COMPLETION_DATE)) {
|
|
String completion = xpp.getAttributeValue(null, LegacyTaskModel.PROGRESS_PERCENTAGE);
|
|
if("100".equals(completion)) {
|
|
task.setValue(Task.COMPLETION_DATE,
|
|
BackupDateUtilities.getDateFromIso8601String(value).getTime());
|
|
}
|
|
}
|
|
else if(field.equals(LegacyTaskModel.NOTIFICATION_FLAGS)) {
|
|
task.setValue(Task.REMINDER_FLAGS, Integer.parseInt(value));
|
|
}
|
|
else if(field.equals(LegacyTaskModel.LAST_NOTIFIED)) {
|
|
task.setValue(Task.REMINDER_LAST,
|
|
BackupDateUtilities.getDateFromIso8601String(value).getTime());
|
|
}
|
|
else if(field.equals("repeat_interval")) {
|
|
// handled below
|
|
}
|
|
else if(field.equals("repeat_value")) {
|
|
int repeatValue = Integer.parseInt(value);
|
|
String repeatInterval = xpp.getAttributeValue(null, "repeat_interval");
|
|
if(repeatValue > 0 && repeatInterval != null) {
|
|
LegacyRepeatInterval interval = LegacyRepeatInterval.valueOf(repeatInterval);
|
|
LegacyRepeatInfo repeatInfo = new LegacyRepeatInfo(interval, repeatValue);
|
|
RRule rrule = repeatInfo.toRRule();
|
|
task.setValue(Task.RECURRENCE, rrule.toIcal());
|
|
}
|
|
}
|
|
else if(field.equals(LegacyTaskModel.FLAGS)) {
|
|
if(Integer.parseInt(value) == LegacyTaskModel.FLAG_SYNC_ON_COMPLETE)
|
|
syncOnComplete = true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
}
|