Use storage access framework for backup files

pull/795/head
Alex Baker 7 years ago
parent 6381d6e987
commit 09156c5243

@ -19,8 +19,8 @@ android {
defaultConfig { defaultConfig {
testApplicationId "org.tasks.test" testApplicationId "org.tasks.test"
applicationId "org.tasks" applicationId "org.tasks"
versionCode 545 versionCode 546
versionName "6.3.1" versionName "6.4"
targetSdkVersion 28 targetSdkVersion 28
minSdkVersion 16 minSdkVersion 16
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

@ -8,14 +8,14 @@ package com.todoroo.astrid.backup;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.res.Resources; import android.content.res.Resources;
import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.text.TextUtils; import android.text.TextUtils;
import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import java.io.FileReader;
import java.io.IOException;
import javax.inject.Inject;
import org.tasks.LocalBroadcastManager; import org.tasks.LocalBroadcastManager;
import org.tasks.R; import org.tasks.R;
import org.tasks.backup.XmlReader; import org.tasks.backup.XmlReader;
@ -35,8 +35,18 @@ import org.tasks.dialogs.DialogBuilder;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlPullParserFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import javax.inject.Inject;
import timber.log.Timber; import timber.log.Timber;
import static org.tasks.files.FileHelper.fromUri;
public class TasksXmlImporter { public class TasksXmlImporter {
private static final String FORMAT2 = "2"; // $NON-NLS-1$ private static final String FORMAT2 = "2"; // $NON-NLS-1$
@ -57,7 +67,7 @@ public class TasksXmlImporter {
private int skipCount = 0; private int skipCount = 0;
private int errorCount = 0; private int errorCount = 0;
private ProgressDialog progressDialog; private ProgressDialog progressDialog;
private String input; private Uri input;
@Inject @Inject
public TasksXmlImporter( public TasksXmlImporter(
@ -85,7 +95,7 @@ public class TasksXmlImporter {
handler.post(() -> progressDialog.setMessage(message)); handler.post(() -> progressDialog.setMessage(message));
} }
public void importTasks(Activity activity, String input, ProgressDialog progressDialog) { public void importTasks(Activity activity, Uri input, ProgressDialog progressDialog) {
this.activity = activity; this.activity = activity;
this.input = input; this.input = input;
this.progressDialog = progressDialog; this.progressDialog = progressDialog;
@ -110,7 +120,9 @@ public class TasksXmlImporter {
private void performImport() throws IOException, XmlPullParserException { private void performImport() throws IOException, XmlPullParserException {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser xpp = factory.newPullParser(); XmlPullParser xpp = factory.newPullParser();
xpp.setInput(new FileReader(input)); InputStream inputStream = fromUri(activity, input);
InputStreamReader reader = new InputStreamReader(inputStream);
xpp.setInput(reader);
try { try {
while (xpp.next() != XmlPullParser.END_DOCUMENT) { while (xpp.next() != XmlPullParser.END_DOCUMENT) {
@ -135,6 +147,8 @@ public class TasksXmlImporter {
} }
} }
} finally { } finally {
reader.close();
inputStream.close();
localBroadcastManager.broadcastRefresh(); localBroadcastManager.broadcastRefresh();
handler.post( handler.post(
() -> { () -> {
@ -154,7 +168,7 @@ public class TasksXmlImporter {
.setMessage( .setMessage(
activity.getString( activity.getString(
R.string.import_summary_message, R.string.import_summary_message,
input, "",
r.getQuantityString(R.plurals.Ntasks, taskCount, taskCount), r.getQuantityString(R.plurals.Ntasks, taskCount, taskCount),
r.getQuantityString(R.plurals.Ntasks, importCount, importCount), r.getQuantityString(R.plurals.Ntasks, importCount, importCount),
r.getQuantityString(R.plurals.Ntasks, skipCount, skipCount), r.getQuantityString(R.plurals.Ntasks, skipCount, skipCount),

@ -9,6 +9,8 @@ import static com.google.common.base.Strings.isNullOrEmpty;
import android.content.Context; import android.content.Context;
import android.os.Environment; import android.os.Environment;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
@ -46,6 +48,7 @@ public class StartupService {
private static final int V5_3_0 = 491; private static final int V5_3_0 = 491;
private static final int V6_0_beta_1 = 522; private static final int V6_0_beta_1 = 522;
private static final int V6_0_beta_2 = 523; private static final int V6_0_beta_2 = 523;
private static final int V6_4 = 546;
private final Database database; private final Database database;
private final Preferences preferences; private final Preferences preferences;
@ -122,6 +125,9 @@ public class StartupService {
if (from < V6_0_beta_2) { if (from < V6_0_beta_2) {
migrateGoogleTaskAccount(); migrateGoogleTaskAccount();
} }
if (from < V6_4) {
migrateUris();
}
tracker.reportEvent(Tracking.Events.UPGRADE, Integer.toString(from)); tracker.reportEvent(Tracking.Events.UPGRADE, Integer.toString(from));
} }
preferences.setCurrentVersion(to); preferences.setCurrentVersion(to);
@ -191,6 +197,22 @@ public class StartupService {
} }
} }
private void migrateUris() {
String backupDirectory = preferences.getStringValue(R.string.p_backup_dir);
if (!Strings.isNullOrEmpty(backupDirectory)) {
File file = new File(backupDirectory);
try {
if (file.canWrite()) {
preferences.setUri(R.string.p_backup_dir, file.toURI());
} else {
preferences.remove(R.string.p_backup_dir);
}
} catch (SecurityException ignored) {
preferences.remove(R.string.p_backup_dir);
}
}
}
private String migrate(String input) { private String migrate(String input) {
return input return input
.replaceAll( .replaceAll(

@ -1,28 +1,19 @@
package org.tasks.backup; package org.tasks.backup;
import static org.tasks.date.DateTimeUtils.newDateTime;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import androidx.annotation.Nullable;
import android.widget.Toast; import android.widget.Toast;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.backup.BackupConstants; import com.todoroo.astrid.backup.BackupConstants;
import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.tasks.BuildConfig; import org.tasks.BuildConfig;
import org.tasks.R; import org.tasks.R;
import org.tasks.data.AlarmDao; import org.tasks.data.AlarmDao;
@ -35,9 +26,24 @@ import org.tasks.data.TagDao;
import org.tasks.data.TagDataDao; import org.tasks.data.TagDataDao;
import org.tasks.data.TaskAttachmentDao; import org.tasks.data.TaskAttachmentDao;
import org.tasks.data.UserActivityDao; import org.tasks.data.UserActivityDao;
import org.tasks.files.FileHelper;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import timber.log.Timber; import timber.log.Timber;
import static org.tasks.date.DateTimeUtils.newDateTime;
public class TasksJsonExporter { public class TasksJsonExporter {
// --- public interface // --- public interface
@ -59,7 +65,7 @@ public class TasksJsonExporter {
private int exportCount = 0; private int exportCount = 0;
private ProgressDialog progressDialog; private ProgressDialog progressDialog;
private Handler handler; private Handler handler;
private File backupDirectory; private Uri backupDirectory;
private String latestSetVersionName; private String latestSetVersionName;
@Inject @Inject
@ -128,16 +134,18 @@ public class TasksJsonExporter {
private void runBackup(ExportType exportType) { private void runBackup(ExportType exportType) {
try { try {
String output = setupFile(backupDirectory, exportType); String filename = getFileName(exportType);
List<Task> tasks = taskDao.getAll(); List<Task> tasks = taskDao.getAll();
if (tasks.size() > 0) { if (tasks.size() > 0) {
doTasksExport(output, tasks); Uri uri = FileHelper.newFile(context, backupDirectory, "application/json", filename);
OutputStream os = context.getContentResolver().openOutputStream(uri);
doTasksExport(os, tasks);
os.close();
} }
if (exportType == ExportType.EXPORT_TYPE_MANUAL) { if (exportType == ExportType.EXPORT_TYPE_MANUAL) {
onFinishExport(output); onFinishExport(filename);
} }
} catch (IOException e) { } catch (IOException e) {
Timber.e(e); Timber.e(e);
@ -153,7 +161,7 @@ public class TasksJsonExporter {
} }
} }
private void doTasksExport(String output, List<Task> tasks) throws IOException { private void doTasksExport(OutputStream os, List<Task> tasks) throws IOException {
List<BackupContainer.TaskBackup> taskBackups = new ArrayList<>(); List<BackupContainer.TaskBackup> taskBackups = new ArrayList<>();
@ -186,14 +194,10 @@ public class TasksJsonExporter {
caldavDao.getAccounts(), caldavDao.getAccounts(),
caldavDao.getCalendars())); caldavDao.getCalendars()));
File file = new File(output); OutputStreamWriter out = new OutputStreamWriter(os);
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
OutputStreamWriter out = new OutputStreamWriter(fos);
Gson gson = BuildConfig.DEBUG ? new GsonBuilder().setPrettyPrinting().create() : new Gson(); Gson gson = BuildConfig.DEBUG ? new GsonBuilder().setPrettyPrinting().create() : new Gson();
out.write(gson.toJson(data)); out.write(gson.toJson(data));
out.close(); out.close();
fos.close();
exportCount = taskBackups.size(); exportCount = taskBackups.size();
} }
@ -217,38 +221,16 @@ public class TasksJsonExporter {
}); });
} }
/** private String getFileName(ExportType type) {
* Creates directories if necessary and returns fully qualified file switch (type) {
*
* @return output file name
*/
private String setupFile(File directory, ExportType exportType) throws IOException {
if (directory != null) {
// Check for /sdcard/astrid directory. If it doesn't exist, make it.
if (directory.exists() || directory.mkdir()) {
String fileName;
switch (exportType) {
case EXPORT_TYPE_SERVICE: case EXPORT_TYPE_SERVICE:
fileName = String.format(BackupConstants.BACKUP_FILE_NAME, getDateForExport()); return String.format(BackupConstants.BACKUP_FILE_NAME, getDateForExport());
break;
case EXPORT_TYPE_MANUAL: case EXPORT_TYPE_MANUAL:
fileName = String.format(BackupConstants.EXPORT_FILE_NAME, getDateForExport()); return String.format(BackupConstants.EXPORT_FILE_NAME, getDateForExport());
break;
case EXPORT_TYPE_ON_UPGRADE: case EXPORT_TYPE_ON_UPGRADE:
fileName = String.format(BackupConstants.UPGRADE_FILE_NAME, latestSetVersionName); return String.format(BackupConstants.UPGRADE_FILE_NAME, latestSetVersionName);
break;
default: default:
throw new IllegalArgumentException("Invalid export type"); // $NON-NLS-1$ throw new UnsupportedOperationException("Unhandled export type");
}
return directory.getAbsolutePath() + File.separator + fileName;
} else {
// Unable to make the /sdcard/astrid directory.
throw new IOException(
context.getString(R.string.DLG_error_sdcard, directory.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));
} }
} }

@ -3,17 +3,16 @@ package org.tasks.backup;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.res.Resources; import android.content.res.Resources;
import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import com.google.common.io.CharStreams;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.andlib.utility.DialogUtilities;
import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import java.io.FileReader;
import java.io.IOException;
import javax.inject.Inject;
import org.tasks.LocalBroadcastManager; import org.tasks.LocalBroadcastManager;
import org.tasks.R; import org.tasks.R;
import org.tasks.data.Alarm; import org.tasks.data.Alarm;
@ -40,8 +39,17 @@ import org.tasks.data.TaskAttachmentDao;
import org.tasks.data.UserActivity; import org.tasks.data.UserActivity;
import org.tasks.data.UserActivityDao; import org.tasks.data.UserActivityDao;
import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.DialogBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import javax.inject.Inject;
import timber.log.Timber; import timber.log.Timber;
import static org.tasks.files.FileHelper.fromUri;
public class TasksJsonImporter { public class TasksJsonImporter {
private final TagDataDao tagDataDao; private final TagDataDao tagDataDao;
@ -64,7 +72,7 @@ public class TasksJsonImporter {
private int importCount = 0; private int importCount = 0;
private int skipCount = 0; private int skipCount = 0;
private ProgressDialog progressDialog; private ProgressDialog progressDialog;
private String input; private Uri input;
@Inject @Inject
public TasksJsonImporter( public TasksJsonImporter(
@ -100,30 +108,21 @@ public class TasksJsonImporter {
handler.post(() -> progressDialog.setMessage(message)); handler.post(() -> progressDialog.setMessage(message));
} }
public void importTasks(Activity activity, String input, ProgressDialog progressDialog) { public void importTasks(Activity activity, Uri input, ProgressDialog progressDialog) {
this.activity = activity; this.activity = activity;
this.input = input; this.input = input;
this.progressDialog = progressDialog; this.progressDialog = progressDialog;
handler = new Handler(); handler = new Handler();
new Thread( new Thread(this::performImport).start();
() -> {
try {
performImport();
} catch (IOException e) {
Timber.e(e);
}
})
.start();
} }
private void performImport() throws IOException { private void performImport() {
FileReader fileReader = new FileReader(input);
String string = CharStreams.toString(fileReader);
fileReader.close();
Gson gson = new Gson(); Gson gson = new Gson();
JsonObject input = gson.fromJson(string, JsonObject.class); InputStream is = fromUri(activity, this.input);
InputStreamReader reader = new InputStreamReader(is);
JsonObject input = gson.fromJson(reader, JsonObject.class);
try { try {
JsonElement data = input.get("data"); JsonElement data = input.get("data");
@ -200,6 +199,10 @@ public class TasksJsonImporter {
} }
importCount++; importCount++;
} }
reader.close();
is.close();
} catch (IOException e) {
Timber.e(e);
} finally { } finally {
localBroadcastManager.broadcastRefresh(); localBroadcastManager.broadcastRefresh();
handler.post( handler.post(
@ -220,7 +223,7 @@ public class TasksJsonImporter {
.setMessage( .setMessage(
activity.getString( activity.getString(
R.string.import_summary_message, R.string.import_summary_message,
input, "",
r.getQuantityString(R.plurals.Ntasks, taskCount, taskCount), r.getQuantityString(R.plurals.Ntasks, taskCount, taskCount),
r.getQuantityString(R.plurals.Ntasks, importCount, importCount), r.getQuantityString(R.plurals.Ntasks, importCount, importCount),
r.getQuantityString(R.plurals.Ntasks, skipCount, skipCount), r.getQuantityString(R.plurals.Ntasks, skipCount, skipCount),

@ -2,27 +2,39 @@ package org.tasks.dialogs;
import android.app.Dialog; import android.app.Dialog;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.webkit.MimeTypeMap;
import com.todoroo.astrid.backup.TasksXmlImporter; import com.todoroo.astrid.backup.TasksXmlImporter;
import javax.inject.Inject;
import org.tasks.analytics.Tracker; import org.tasks.analytics.Tracker;
import org.tasks.analytics.Tracking; import org.tasks.analytics.Tracking;
import org.tasks.backup.TasksJsonImporter; import org.tasks.backup.TasksJsonImporter;
import org.tasks.injection.ForApplication;
import org.tasks.injection.InjectingNativeDialogFragment; import org.tasks.injection.InjectingNativeDialogFragment;
import org.tasks.injection.NativeDialogFragmentComponent; import org.tasks.injection.NativeDialogFragmentComponent;
import java.io.IOException;
import javax.inject.Inject;
import timber.log.Timber;
public class ImportTasksDialog extends InjectingNativeDialogFragment { public class ImportTasksDialog extends InjectingNativeDialogFragment {
private static final String EXTRA_PATH = "extra_path"; private static final String EXTRA_URI = "extra_uri";
@Inject TasksXmlImporter xmlImporter; @Inject TasksXmlImporter xmlImporter;
@Inject TasksJsonImporter jsonImporter; @Inject TasksJsonImporter jsonImporter;
@Inject DialogBuilder dialogBuilder; @Inject DialogBuilder dialogBuilder;
@Inject Tracker tracker; @Inject Tracker tracker;
@Inject @ForApplication Context context;
public static ImportTasksDialog newImportTasksDialog(String path) { public static ImportTasksDialog newImportTasksDialog(Uri data) {
ImportTasksDialog importTasksDialog = new ImportTasksDialog(); ImportTasksDialog importTasksDialog = new ImportTasksDialog();
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putString(EXTRA_PATH, path); args.putParcelable(EXTRA_URI, data);
importTasksDialog.setArguments(args); importTasksDialog.setArguments(args);
return importTasksDialog; return importTasksDialog;
} }
@ -30,21 +42,32 @@ public class ImportTasksDialog extends InjectingNativeDialogFragment {
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle arguments = getArguments(); Bundle arguments = getArguments();
String path = arguments.getString(EXTRA_PATH); Uri data = arguments.getParcelable(EXTRA_URI);
ProgressDialog progressDialog = dialogBuilder.newProgressDialog(); ProgressDialog progressDialog = dialogBuilder.newProgressDialog();
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.setCancelable(false); progressDialog.setCancelable(false);
progressDialog.setIndeterminate(true); progressDialog.setIndeterminate(true);
progressDialog.show(); progressDialog.show();
setCancelable(false); setCancelable(false);
if (path.endsWith(".xml")) { try {
xmlImporter.importTasks(getActivity(), path, progressDialog); String extension = MimeTypeMap.getFileExtensionFromUrl(data.getPath());
tracker.reportEvent(Tracking.Events.IMPORT_XML); switch (extension) {
} else { case "json":
jsonImporter.importTasks(getActivity(), path, progressDialog); jsonImporter.importTasks(getActivity(), data, progressDialog);
tracker.reportEvent(Tracking.Events.IMPORT_JSON); tracker.reportEvent(Tracking.Events.IMPORT_JSON);
break;
case "xml":
xmlImporter.importTasks(getActivity(), data, progressDialog);
tracker.reportEvent(Tracking.Events.IMPORT_XML);
break;
default:
throw new IOException("Invalid file type");
} }
return progressDialog; return progressDialog;
} catch (IOException e) {
Timber.e(e);
}
return null;
} }
@Override @Override

@ -93,6 +93,7 @@ public class FileExplore extends InjectingAppCompatActivity {
File file = com.nononsenseapps.filepicker.Utils.getFileForUri(uri); File file = com.nononsenseapps.filepicker.Utils.getFileForUri(uri);
Intent intent = new Intent(); Intent intent = new Intent();
intent.putExtra(directoryMode ? EXTRA_DIRECTORY : EXTRA_FILE, file.getAbsolutePath()); intent.putExtra(directoryMode ? EXTRA_DIRECTORY : EXTRA_FILE, file.getAbsolutePath());
intent.setData(uri);
setResult(Activity.RESULT_OK, intent); setResult(Activity.RESULT_OK, intent);
} }
finish(); finish();

@ -1,7 +1,5 @@
package org.tasks.files; package org.tasks.files;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -10,13 +8,94 @@ import android.content.pm.ResolveInfo;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.provider.MediaStore; import android.provider.MediaStore;
import androidx.core.content.FileProvider;
import com.todoroo.astrid.utility.Constants; import com.todoroo.astrid.utility.Constants;
import org.tasks.preferences.BasicPreferences;
import org.tasks.preferences.Preferences;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.List; import java.util.List;
import javax.annotation.Nullable;
import androidx.core.content.FileProvider;
import androidx.documentfile.provider.DocumentFile;
import timber.log.Timber;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastKitKat;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop;
public class FileHelper { public class FileHelper {
public static void newFilePicker(Activity activity, int rc, @Nullable Uri initial, String... mimeTypes) {
if (atLeastKitKat()) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
if (initial != null) {
intent.setData(initial);
}
if (mimeTypes.length == 1) {
intent.setType(mimeTypes[0]);
} else {
intent.setType("*/*");
if (mimeTypes.length > 1) {
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
}
}
activity.startActivityForResult(intent, rc);
} else {
Intent intent = new Intent(activity, FileExplore.class);
if (initial != null) {
intent.putExtra(
FileExplore.EXTRA_START_PATH,
new File(initial.getPath()));
}
activity.startActivityForResult(intent, rc);
}
}
public static void newDirectoryPicker(Activity activity, int rc, @Nullable Uri initial) {
if (atLeastLollipop()) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra("android.content.extra.SHOW_ADVANCED",true);
activity.startActivityForResult(intent, rc);
} else {
Intent intent = new Intent(activity, FileExplore.class);
intent.putExtra(FileExplore.EXTRA_DIRECTORY_MODE, true);
if (initial != null) {
intent.putExtra(
FileExplore.EXTRA_START_PATH,
new File(initial.getPath()));
}
activity.startActivityForResult(intent, rc);
}
}
public static InputStream fromUri(Context context, Uri uri) {
try {
switch (uri.getScheme()) {
case "content":
return context.getContentResolver().openInputStream(uri);
case "file":
return new FileInputStream(new File(uri.getPath()));
default:
throw new IllegalArgumentException("Unhandled scheme: " + uri.getScheme());
}
} catch (FileNotFoundException e) {
Timber.e(e);
return null;
}
}
public static String getPathFromUri(Activity activity, Uri uri) { public static String getPathFromUri(Activity activity, Uri uri) {
String[] projection = {MediaStore.Images.Media.DATA}; String[] projection = {MediaStore.Images.Media.DATA};
Cursor cursor = activity.managedQuery(uri, projection, null, null, null); Cursor cursor = activity.managedQuery(uri, projection, null, null, null);
@ -57,4 +136,29 @@ public class FileHelper {
} }
} }
} }
public static Uri newFile(Context context, Uri destination, String mimeType, String filename)
throws IOException {
switch (destination.getScheme()) {
case "content":
DocumentFile tree = DocumentFile.fromTreeUri(context, destination);
DocumentFile f1 = tree.createFile(mimeType, filename);
if (f1 == null) {
throw new FileNotFoundException("Failed to create " + filename);
}
return f1.getUri();
case "file":
File dir = new File(destination.getPath());
if (!dir.exists() && !dir.mkdirs()) {
throw new IOException("Failed to create %s" + dir.getAbsolutePath());
}
File f2 = new File(dir.getAbsolutePath() + File.separator + filename);
if (f2.createNewFile()) {
return Uri.fromFile(f2);
}
throw new FileNotFoundException("Failed to create " + filename);
default:
throw new IllegalArgumentException("Unknown URI scheme: " + destination.getScheme());
}
}
} }

@ -1,33 +1,46 @@
package org.tasks.jobs; package org.tasks.jobs;
import static com.google.common.collect.Iterables.skip;
import static com.google.common.collect.Lists.newArrayList;
import static com.todoroo.andlib.utility.DateUtilities.now;
import static java.util.Collections.emptyList;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import android.net.Uri;
import androidx.work.WorkerParameters;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import org.tasks.R;
import org.tasks.backup.TasksJsonExporter;
import org.tasks.injection.ForApplication;
import org.tasks.injection.JobComponent;
import org.tasks.preferences.Preferences;
import java.io.File; import java.io.File;
import java.io.FileFilter; import java.io.FileFilter;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import org.tasks.R;
import org.tasks.backup.TasksJsonExporter; import androidx.annotation.NonNull;
import org.tasks.injection.ForApplication; import androidx.documentfile.provider.DocumentFile;
import org.tasks.injection.JobComponent; import androidx.work.WorkerParameters;
import org.tasks.preferences.Preferences;
import timber.log.Timber; import timber.log.Timber;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.skip;
import static com.google.common.collect.Lists.newArrayList;
import static com.todoroo.andlib.utility.DateUtilities.now;
import static java.util.Collections.emptyList;
public class BackupWork extends RepeatingWorker { public class BackupWork extends RepeatingWorker {
static final String BACKUP_FILE_NAME_REGEX = "auto\\.[-\\d]+\\.json"; static final String BACKUP_FILE_NAME_REGEX = "auto\\.[-\\d]+\\.json";
static final FileFilter FILE_FILTER = f -> f.getName().matches(BACKUP_FILE_NAME_REGEX); static final Predicate<String> FILENAME_FILTER = f -> f.matches(BACKUP_FILE_NAME_REGEX);
static final FileFilter FILE_FILTER = f -> FILENAME_FILTER.apply(f.getName());
private static final Comparator<File> BY_LAST_MODIFIED = private static final Comparator<File> BY_LAST_MODIFIED =
(f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()); (f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified());
private static final Comparator<DocumentFile> DOCUMENT_FILE_COMPARATOR =
(d1, d2) -> Long.compare(d2.lastModified(), d1.lastModified());
private static final int DAYS_TO_KEEP_BACKUP = 7; private static final int DAYS_TO_KEEP_BACKUP = 7;
@Inject @ForApplication Context context; @Inject @ForApplication Context context;
@ -61,6 +74,17 @@ public class BackupWork extends RepeatingWorker {
return newArrayList(skip(files, keepNewest)); return newArrayList(skip(files, keepNewest));
} }
private static List<DocumentFile> getDeleteList(DocumentFile[] fileArray) {
if (fileArray == null) {
return emptyList();
}
List<DocumentFile> files = Arrays.asList(fileArray);
files = newArrayList(filter(files, file -> FILENAME_FILTER.apply(file.getName())));
Collections.sort(files, DOCUMENT_FILE_COMPARATOR);
return newArrayList(skip(files, DAYS_TO_KEEP_BACKUP));
}
@Override @Override
protected void inject(JobComponent component) { protected void inject(JobComponent component) {
component.inject(this); component.inject(this);
@ -82,17 +106,25 @@ public class BackupWork extends RepeatingWorker {
} }
private void deleteOldBackups() { private void deleteOldBackups() {
File astridDir = preferences.getBackupDirectory(); Uri uri = preferences.getBackupDirectory();
if (astridDir == null) { switch (uri.getScheme()) {
return; case "content":
DocumentFile dir = DocumentFile.fromTreeUri(context, uri);
for (DocumentFile file : getDeleteList(dir.listFiles())) {
if (!file.delete()) {
Timber.e("Unable to delete: %s", file);
} }
}
// grab all backup files, sort by modified date, delete old ones break;
case "file":
File astridDir = new File(uri.getPath());
File[] fileArray = astridDir.listFiles(FILE_FILTER); File[] fileArray = astridDir.listFiles(FILE_FILTER);
for (File file : getDeleteList(fileArray, DAYS_TO_KEEP_BACKUP)) { for (File file : getDeleteList(fileArray, DAYS_TO_KEEP_BACKUP)) {
if (!file.delete()) { if (!file.delete()) {
Timber.e("Unable to delete: %s", file); Timber.e("Unable to delete: %s", file);
} }
} }
break;
}
} }
} }

@ -1,22 +1,17 @@
package org.tasks.preferences; package org.tasks.preferences;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastJellybeanMR1;
import static org.tasks.dialogs.ExportTasksDialog.newExportTasksDialog;
import static org.tasks.dialogs.ImportTasksDialog.newImportTasksDialog;
import static org.tasks.locale.LocalePickerDialog.newLocalePickerDialog;
import static org.tasks.themes.ThemeColor.LAUNCHERS;
import android.app.Activity; import android.app.Activity;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.Preference; import android.preference.Preference;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.todoroo.astrid.core.OldTaskPreferences; import com.todoroo.astrid.core.OldTaskPreferences;
import com.todoroo.astrid.reminders.ReminderPreferences; import com.todoroo.astrid.reminders.ReminderPreferences;
import java.io.File;
import javax.inject.Inject;
import org.tasks.BuildConfig; import org.tasks.BuildConfig;
import org.tasks.R; import org.tasks.R;
import org.tasks.activities.ColorPickerActivity; import org.tasks.activities.ColorPickerActivity;
@ -28,6 +23,7 @@ import org.tasks.billing.BillingClient;
import org.tasks.billing.Inventory; import org.tasks.billing.Inventory;
import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.DialogBuilder;
import org.tasks.files.FileExplore; import org.tasks.files.FileExplore;
import org.tasks.files.FileHelper;
import org.tasks.injection.ActivityComponent; import org.tasks.injection.ActivityComponent;
import org.tasks.injection.InjectingPreferenceActivity; import org.tasks.injection.InjectingPreferenceActivity;
import org.tasks.locale.Locale; import org.tasks.locale.Locale;
@ -37,6 +33,18 @@ import org.tasks.themes.ThemeBase;
import org.tasks.themes.ThemeCache; import org.tasks.themes.ThemeCache;
import org.tasks.themes.ThemeColor; import org.tasks.themes.ThemeColor;
import java.io.File;
import javax.inject.Inject;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastJellybeanMR1;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastKitKat;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop;
import static org.tasks.dialogs.ExportTasksDialog.newExportTasksDialog;
import static org.tasks.dialogs.ImportTasksDialog.newImportTasksDialog;
import static org.tasks.locale.LocalePickerDialog.newLocalePickerDialog;
import static org.tasks.themes.ThemeColor.LAUNCHERS;
public class BasicPreferences extends InjectingPreferenceActivity public class BasicPreferences extends InjectingPreferenceActivity
implements LocalePickerDialog.LocaleSelectionHandler { implements LocalePickerDialog.LocaleSelectionHandler {
@ -146,10 +154,7 @@ public class BasicPreferences extends InjectingPreferenceActivity
findPreference(R.string.backup_BAc_import) findPreference(R.string.backup_BAc_import)
.setOnPreferenceClickListener( .setOnPreferenceClickListener(
preference -> { preference -> {
Intent intent = new Intent(BasicPreferences.this, FileExplore.class); FileHelper.newFilePicker(BasicPreferences.this, REQUEST_PICKER, preferences.getBackupDirectory());
intent.putExtra(
FileExplore.EXTRA_START_PATH, preferences.getBackupDirectory().getAbsolutePath());
startActivityForResult(intent, REQUEST_PICKER);
return false; return false;
}); });
@ -230,13 +235,19 @@ public class BasicPreferences extends InjectingPreferenceActivity
} }
} else if (requestCode == REQUEST_CODE_BACKUP_DIR) { } else if (requestCode == REQUEST_CODE_BACKUP_DIR) {
if (resultCode == RESULT_OK && data != null) { if (resultCode == RESULT_OK && data != null) {
String dir = data.getStringExtra(FileExplore.EXTRA_DIRECTORY); Uri dir = data.getData();
preferences.setString(R.string.p_backup_dir, dir); if (atLeastLollipop()) {
getContentResolver()
.takePersistableUriPermission(
dir,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
preferences.setString(R.string.p_backup_dir, dir.toString());
updateBackupDirectory(); updateBackupDirectory();
} }
} else if (requestCode == REQUEST_PICKER) { } else if (requestCode == REQUEST_PICKER) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
newImportTasksDialog(data.getStringExtra(FileExplore.EXTRA_FILE)) newImportTasksDialog(data.getData())
.show(getFragmentManager(), FRAG_TAG_IMPORT_TASKS); .show(getFragmentManager(), FRAG_TAG_IMPORT_TASKS);
} }
} else { } else {
@ -278,10 +289,8 @@ public class BasicPreferences extends InjectingPreferenceActivity
findPreference(getString(R.string.p_backup_dir)) findPreference(getString(R.string.p_backup_dir))
.setOnPreferenceClickListener( .setOnPreferenceClickListener(
p -> { p -> {
Intent filesDir = new Intent(BasicPreferences.this, FileExplore.class); FileHelper.newDirectoryPicker(this, REQUEST_CODE_BACKUP_DIR, preferences.getBackupDirectory());
filesDir.putExtra(FileExplore.EXTRA_DIRECTORY_MODE, true); return false;
startActivityForResult(filesDir, REQUEST_CODE_BACKUP_DIR);
return true;
}); });
updateBackupDirectory(); updateBackupDirectory();
} }
@ -291,8 +300,10 @@ public class BasicPreferences extends InjectingPreferenceActivity
} }
private String getBackupDirectory() { private String getBackupDirectory() {
File dir = preferences.getBackupDirectory(); Uri uri = preferences.getBackupDirectory();
return dir == null ? "" : dir.getAbsolutePath(); return uri.getScheme().equals("file")
? new File(uri.getPath()).getAbsolutePath()
: uri.toString();
} }
private void setLauncherIcon(int index) { private void setLauncherIcon(int index) {

@ -1,36 +1,47 @@
package org.tasks.preferences; package org.tasks.preferences;
import static android.content.SharedPreferences.Editor;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Sets.newHashSet;
import static java.util.Collections.emptySet;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources; import android.content.res.Resources;
import android.media.RingtoneManager; import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
import android.os.Binder;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import androidx.core.app.NotificationCompat;
import android.text.TextUtils; import android.text.TextUtils;
import com.android.billingclient.api.Purchase; import com.android.billingclient.api.Purchase;
import com.google.common.base.Strings;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.todoroo.astrid.activity.BeastModePreferences; import com.todoroo.astrid.activity.BeastModePreferences;
import com.todoroo.astrid.api.AstridApiConstants; import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.core.SortHelper; import com.todoroo.astrid.core.SortHelper;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import java.io.File;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import org.tasks.BuildConfig; import org.tasks.BuildConfig;
import org.tasks.R; import org.tasks.R;
import org.tasks.data.TaskAttachment; import org.tasks.data.TaskAttachment;
import org.tasks.injection.ForApplication; import org.tasks.injection.ForApplication;
import org.tasks.time.DateTime; import org.tasks.time.DateTime;
import java.io.File;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import androidx.core.app.NotificationCompat;
import androidx.documentfile.provider.DocumentFile;
import timber.log.Timber; import timber.log.Timber;
import static android.content.SharedPreferences.Editor;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Sets.newHashSet;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastKitKat;
import static java.util.Collections.emptySet;
public class Preferences { public class Preferences {
private static final String P_CURRENT_VERSION = "cv"; // $NON-NLS-1$ private static final String P_CURRENT_VERSION = "cv"; // $NON-NLS-1$
@ -259,6 +270,19 @@ public class Preferences {
} }
} }
public Uri getUri(int key) {
String uri = getStringValue(key);
return Strings.isNullOrEmpty(uri) ? null : Uri.parse(uri);
}
public void setUri(int key, java.net.URI uri) {
setString(key, uri.toString());
}
public void setUri(int key, Uri uri) {
setString(key, uri.toString());
}
public void setString(int key, String newValue) { public void setString(int key, String newValue) {
setString(context.getString(key), newValue); setString(context.getString(key), newValue);
} }
@ -405,18 +429,43 @@ public class Preferences {
return dir + File.separator + name; return dir + File.separator + name;
} }
public File getBackupDirectory() { public Uri getBackupDirectory() {
File directory = null; Uri uri = getUri(R.string.p_backup_dir);
String customDir = getStringValue(R.string.p_backup_dir); if (uri != null) {
if (permissionChecker.canWriteToExternalStorage() && !TextUtils.isEmpty(customDir)) { switch (uri.getScheme()) {
directory = new File(customDir); case "file":
File file = new File(uri.getPath());
try {
if (file.canWrite()) {
return uri;
}
} catch (SecurityException ignored) {
}
break;
case "content":
if (hasWritePermission(context, uri)) {
return uri;
}
break;
}
} }
if (directory == null || !directory.exists()) { if (atLeastKitKat()) {
directory = getDefaultFileLocation("backups"); return DocumentFile.fromFile(context.getExternalFilesDir(null))
.createDirectory("backups")
.getUri();
} else {
return Uri.fromFile(getDefaultFileLocation("backups"));
}
} }
return directory; private boolean hasWritePermission(Context context, Uri uri) {
return PackageManager.PERMISSION_GRANTED
== context.checkUriPermission(
uri,
Binder.getCallingPid(),
Binder.getCallingUid(),
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} }
public int getNotificationDefaults() { public int getNotificationDefaults() {

@ -21,8 +21,6 @@
%4$s вече съществуват\n %4$s вече съществуват\n
%5$s имат грешки\n</string> %5$s имат грешки\n</string>
<string name="import_progress_read">Прочитане на задачи %d...</string> <string name="import_progress_read">Прочитане на задачи %d...</string>
<string name="DLG_error_sdcard">Нямате достъп до папка: %s</string>
<string name="DLG_error_sdcard_general">SD картата не може да бъде достъпена!</string>
<string name="read_permission_label">Tasks Разрешение</string> <string name="read_permission_label">Tasks Разрешение</string>
<string name="discard_confirmation">Сигурни ли сте че искате да отхвърлите промените си?</string> <string name="discard_confirmation">Сигурни ли сте че искате да отхвърлите промените си?</string>
<string name="keep_editing">Продължаване на редактиране</string> <string name="keep_editing">Продължаване на редактиране</string>

@ -14,8 +14,6 @@
<string name="export_toast_no_tasks">Sense tasques que exportar</string> <string name="export_toast_no_tasks">Sense tasques que exportar</string>
<string name="import_summary_title">Resum de Restauració</string> <string name="import_summary_title">Resum de Restauració</string>
<string name="import_progress_read">S\'està llegint la tasca %d</string> <string name="import_progress_read">S\'està llegint la tasca %d</string>
<string name="DLG_error_sdcard">No es pot accedir a la carpeta: %s</string>
<string name="DLG_error_sdcard_general">No s\'ha pogut accedir a la targeta SD.</string>
<string name="read_permission_label">Permís de l\'Tasks</string> <string name="read_permission_label">Permís de l\'Tasks</string>
<string name="DLG_delete_this_task_question">Voleu suprimir aquesta tasca?</string> <string name="DLG_delete_this_task_question">Voleu suprimir aquesta tasca?</string>
<string name="DLG_hour_minutes">Temps (hores : minuts)</string> <string name="DLG_hour_minutes">Temps (hores : minuts)</string>

@ -17,8 +17,6 @@
<string name="import_summary_title">Souhrn obnovy</string> <string name="import_summary_title">Souhrn obnovy</string>
<string name="import_summary_message">Soubor %1$s obsahuje %2$s.\n\n %3$s importováno,\n %4$s již existuje\n %5$s je chybných\n</string> <string name="import_summary_message">Soubor %1$s obsahuje %2$s.\n\n %3$s importováno,\n %4$s již existuje\n %5$s je chybných\n</string>
<string name="import_progress_read">Načítávání úkolu %d...</string> <string name="import_progress_read">Načítávání úkolu %d...</string>
<string name="DLG_error_sdcard">Chyba v přístupu k adresáři: %s</string>
<string name="DLG_error_sdcard_general">Chyba v přístupu k SD kartě!</string>
<string name="read_permission_label">Tasks Práva</string> <string name="read_permission_label">Tasks Práva</string>
<string name="DLG_delete_this_task_question">Smazat tento úkol?</string> <string name="DLG_delete_this_task_question">Smazat tento úkol?</string>
<string name="DLG_hour_minutes">Čas (hodin : minut)</string> <string name="DLG_hour_minutes">Čas (hodin : minut)</string>

@ -16,8 +16,6 @@
<string name="import_summary_title">Opsummering af gendannelse</string> <string name="import_summary_title">Opsummering af gendannelse</string>
<string name="import_summary_message">Filen %1$s indeholdt %2$s.\n\n %3$s importeret,\n %4$s findes allerede\n %5$s indeholdt fejl\n</string> <string name="import_summary_message">Filen %1$s indeholdt %2$s.\n\n %3$s importeret,\n %4$s findes allerede\n %5$s indeholdt fejl\n</string>
<string name="import_progress_read">Læser opgave %d...</string> <string name="import_progress_read">Læser opgave %d...</string>
<string name="DLG_error_sdcard">Kan ikke få adgang til mappen: %s</string>
<string name="DLG_error_sdcard_general">Kan ikke få adgang til dig SD-kort!</string>
<string name="read_permission_label">Tasks Tilladelser</string> <string name="read_permission_label">Tasks Tilladelser</string>
<string name="DLG_delete_this_task_question">Slet denne opgave?</string> <string name="DLG_delete_this_task_question">Slet denne opgave?</string>
<string name="DLG_hour_minutes">Tid (timer : minutter)</string> <string name="DLG_hour_minutes">Tid (timer : minutter)</string>

@ -16,8 +16,6 @@
<string name="import_summary_title">Zusammenfassung wiederherstellen</string> <string name="import_summary_title">Zusammenfassung wiederherstellen</string>
<string name="import_summary_message">Datei %1$s enthielt %2$s Aufgaben.\n\n %3$s wurden importiert,\n %4$s waren schon vorhanden (übersprungen)\n %5$s waren fehlerhaft\n</string> <string name="import_summary_message">Datei %1$s enthielt %2$s Aufgaben.\n\n %3$s wurden importiert,\n %4$s waren schon vorhanden (übersprungen)\n %5$s waren fehlerhaft\n</string>
<string name="import_progress_read">Aufgabe %d wird gelesen …</string> <string name="import_progress_read">Aufgabe %d wird gelesen …</string>
<string name="DLG_error_sdcard">Ordner konnte nicht geöffnet werden: %s</string>
<string name="DLG_error_sdcard_general">Auf die SD-Karte konnte nicht zugegriffen werden!</string>
<string name="read_permission_label">Tasks Zugriffsrechte</string> <string name="read_permission_label">Tasks Zugriffsrechte</string>
<string name="discard_confirmation">Änderungen wirklich verwerfen?</string> <string name="discard_confirmation">Änderungen wirklich verwerfen?</string>
<string name="keep_editing">Weiter bearbeiten</string> <string name="keep_editing">Weiter bearbeiten</string>

@ -19,8 +19,6 @@
%4$s υπάρχει ήδηt\n %4$s υπάρχει ήδηt\n
%5$s έχει σφάλματα\n</string> %5$s έχει σφάλματα\n</string>
<string name="import_progress_read">Ανάγνωση εργασίας %d...</string> <string name="import_progress_read">Ανάγνωση εργασίας %d...</string>
<string name="DLG_error_sdcard">Δεν υπάρχει πρόσβαση στον φάκελο: %s</string>
<string name="DLG_error_sdcard_general">Δεν υπάρχει πρόσβαση στην SD κάρτα!</string>
<string name="read_permission_label">Άδεια εργασιών</string> <string name="read_permission_label">Άδεια εργασιών</string>
<string name="DLG_delete_this_task_question">Διαγραφή εργασίας;</string> <string name="DLG_delete_this_task_question">Διαγραφή εργασίας;</string>
<string name="DLG_hour_minutes">Ώρα (ώρες : λεπτά)</string> <string name="DLG_hour_minutes">Ώρα (ώρες : λεπτά)</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">Resumen de restauración</string> <string name="import_summary_title">Resumen de restauración</string>
<string name="import_summary_message">El fichero %1$s contenia %2$s.\n\n %3$s importadas\n %4$s ya existian\n %5$s tenian errores\n</string> <string name="import_summary_message">El fichero %1$s contenia %2$s.\n\n %3$s importadas\n %4$s ya existian\n %5$s tenian errores\n</string>
<string name="import_progress_read">Leyendo tarea %d...</string> <string name="import_progress_read">Leyendo tarea %d...</string>
<string name="DLG_error_sdcard">No se puede acceder a la carpeta: %s</string>
<string name="DLG_error_sdcard_general">¡No se pudo acceder a su tarjeta de memoria SD!</string>
<string name="read_permission_label">Permisos de Tasks</string> <string name="read_permission_label">Permisos de Tasks</string>
<string name="discard_confirmation">¿Quieres descartar los cambios?</string> <string name="discard_confirmation">¿Quieres descartar los cambios?</string>
<string name="keep_editing">Seguir editando</string> <string name="keep_editing">Seguir editando</string>

@ -21,8 +21,6 @@
%4$s jo olemassa\n %4$s jo olemassa\n
%5$s virheitä\n</string> %5$s virheitä\n</string>
<string name="import_progress_read">Luetaan tehtävää %d...</string> <string name="import_progress_read">Luetaan tehtävää %d...</string>
<string name="DLG_error_sdcard">Ei pääsyä kansioon: %s</string>
<string name="DLG_error_sdcard_general">Ei pääsyä SD kortillesi!</string>
<string name="read_permission_label">Tasks käyttöoikeus</string> <string name="read_permission_label">Tasks käyttöoikeus</string>
<string name="discard_confirmation">Oletko varma että haluat hylätä muutokset?</string> <string name="discard_confirmation">Oletko varma että haluat hylätä muutokset?</string>
<string name="keep_editing">Jatka muokkausta</string> <string name="keep_editing">Jatka muokkausta</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">Résumé de la restauration</string> <string name="import_summary_title">Résumé de la restauration</string>
<string name="import_summary_message">Le fichier %1$s contenait %2$s.\n\n %3$s importées,\n %4$s existent déjà\n %5$s avaient des erreurs\n</string> <string name="import_summary_message">Le fichier %1$s contenait %2$s.\n\n %3$s importées,\n %4$s existent déjà\n %5$s avaient des erreurs\n</string>
<string name="import_progress_read">Lecture de la tâche %d…</string> <string name="import_progress_read">Lecture de la tâche %d…</string>
<string name="DLG_error_sdcard">Impossible d\'accéder au dossier : %s</string>
<string name="DLG_error_sdcard_general">Impossible d\'accéder à la carte SD !</string>
<string name="read_permission_label">Permissions de Tasks</string> <string name="read_permission_label">Permissions de Tasks</string>
<string name="discard_confirmation">Êtes-vous sûr de vouloir annuler vos modifications ?</string> <string name="discard_confirmation">Êtes-vous sûr de vouloir annuler vos modifications ?</string>
<string name="keep_editing">Continuer l\'édition</string> <string name="keep_editing">Continuer l\'édition</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">Resumen de restauración</string> <string name="import_summary_title">Resumen de restauración</string>
<string name="import_summary_message">El fichero %1$s contenia %2$s.\n\n %3$s importadas\n %4$s ya existian\n %5$s tenian errores\n</string> <string name="import_summary_message">El fichero %1$s contenia %2$s.\n\n %3$s importadas\n %4$s ya existian\n %5$s tenian errores\n</string>
<string name="import_progress_read">Leyendo tarea %d...</string> <string name="import_progress_read">Leyendo tarea %d...</string>
<string name="DLG_error_sdcard">No se puede acceder a la carpeta: %s</string>
<string name="DLG_error_sdcard_general">¡No se pudo acceder a su tarjeta de memoria SD!</string>
<string name="read_permission_label">Permisos de Tasks</string> <string name="read_permission_label">Permisos de Tasks</string>
<string name="discard_confirmation">¿Quieres descartar los cambios?</string> <string name="discard_confirmation">¿Quieres descartar los cambios?</string>
<string name="keep_editing">Seguir editando</string> <string name="keep_editing">Seguir editando</string>

@ -21,8 +21,6 @@
%4$s már létezett\n %4$s már létezett\n
%5$s hibás\n</string> %5$s hibás\n</string>
<string name="import_progress_read">%d feladat olvasása...</string> <string name="import_progress_read">%d feladat olvasása...</string>
<string name="DLG_error_sdcard">Könyvtár nem elérhető: %s</string>
<string name="DLG_error_sdcard_general">SD kártya nem elérhető</string>
<string name="read_permission_label">Tasks Engedélyek</string> <string name="read_permission_label">Tasks Engedélyek</string>
<string name="discard_confirmation">Biztosan el akarja vetni a változtatásokat?</string> <string name="discard_confirmation">Biztosan el akarja vetni a változtatásokat?</string>
<string name="keep_editing">Vissza a szerkesztéshez</string> <string name="keep_editing">Vissza a szerkesztéshez</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">Ripristina sommario</string> <string name="import_summary_title">Ripristina sommario</string>
<string name="import_summary_message">Il File %1$s contiene %2$s.\n\n %3$s importati\n %4$s esiste già\n %5$s contiene errori\n</string> <string name="import_summary_message">Il File %1$s contiene %2$s.\n\n %3$s importati\n %4$s esiste già\n %5$s contiene errori\n</string>
<string name="import_progress_read">Lettura compito %d...</string> <string name="import_progress_read">Lettura compito %d...</string>
<string name="DLG_error_sdcard">Impossibile accedere alla cartella: %s</string>
<string name="DLG_error_sdcard_general">Impossibile accedere alla scheda SD!</string>
<string name="read_permission_label">Permessi attività</string> <string name="read_permission_label">Permessi attività</string>
<string name="discard_confirmation">Sicuro di voler annullare tutte le modifiche?</string> <string name="discard_confirmation">Sicuro di voler annullare tutte le modifiche?</string>
<string name="keep_editing">Continua modifica</string> <string name="keep_editing">Continua modifica</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">סיכום פעולת השחזור</string> <string name="import_summary_title">סיכום פעולת השחזור</string>
<string name="import_summary_message">\"הקובץ \' %1$s\' הכיל %2$s משימות: \n\n %3$s יובאו,\n %4$s היו קיימות כבר,\nוב־%5$s היו שגיאות.\n\"</string> <string name="import_summary_message">\"הקובץ \' %1$s\' הכיל %2$s משימות: \n\n %3$s יובאו,\n %4$s היו קיימות כבר,\nוב־%5$s היו שגיאות.\n\"</string>
<string name="import_progress_read">קוראת משימה %d...</string> <string name="import_progress_read">קוראת משימה %d...</string>
<string name="DLG_error_sdcard">לא ניתן לגשת לתיקיה: %s</string>
<string name="DLG_error_sdcard_general">לא ניתן לגשת לכרטיס ה־SD שלך!</string>
<string name="read_permission_label">הרשאות אסטריד</string> <string name="read_permission_label">הרשאות אסטריד</string>
<string name="discard_confirmation">להתעלם מהשינויים ?</string> <string name="discard_confirmation">להתעלם מהשינויים ?</string>
<string name="keep_editing">המשך לערוך</string> <string name="keep_editing">המשך לערוך</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">復元の概要</string> <string name="import_summary_title">復元の概要</string>
<string name="import_summary_message">ファイル %1$s の %2$s 中、\n\n成功: %3$s\n既に存在: %4$s\n失敗: %5$s\n</string> <string name="import_summary_message">ファイル %1$s の %2$s 中、\n\n成功: %3$s\n既に存在: %4$s\n失敗: %5$s\n</string>
<string name="import_progress_read">タスク %d を読み込み中</string> <string name="import_progress_read">タスク %d を読み込み中</string>
<string name="DLG_error_sdcard">フォルダ %s を開けません</string>
<string name="DLG_error_sdcard_general">SDカードにアクセスできません</string>
<string name="read_permission_label">タスクの読み込み</string> <string name="read_permission_label">タスクの読み込み</string>
<string name="discard_confirmation">変更を破棄してもよろしいですか?</string> <string name="discard_confirmation">変更を破棄してもよろしいですか?</string>
<string name="keep_editing">編集を続ける</string> <string name="keep_editing">編集を続ける</string>

@ -21,8 +21,6 @@
%4$s 개 이미 존재,\n %4$s 개 이미 존재,\n
%5$s 개 에러 발생\n</string> %5$s 개 에러 발생\n</string>
<string name="import_progress_read">할일 %d 읽는 중...</string> <string name="import_progress_read">할일 %d 읽는 중...</string>
<string name="DLG_error_sdcard">폴더에 접근 불가: %s</string>
<string name="DLG_error_sdcard_general">SD 카드에 접근할 수 없습니다!</string>
<string name="read_permission_label">Tasks 권한</string> <string name="read_permission_label">Tasks 권한</string>
<string name="discard_confirmation">변경 사항을 버리시겠습니까?</string> <string name="discard_confirmation">변경 사항을 버리시겠습니까?</string>
<string name="keep_editing">계속 편집</string> <string name="keep_editing">계속 편집</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">Atstatyti suvestinę</string> <string name="import_summary_title">Atstatyti suvestinę</string>
<string name="import_summary_message">Failas %1$s turėjo %2$s.\n\n %3$s suimportuota,\n %4$s jau egzistavo\n %5$s turėjo klaidų\n</string> <string name="import_summary_message">Failas %1$s turėjo %2$s.\n\n %3$s suimportuota,\n %4$s jau egzistavo\n %5$s turėjo klaidų\n</string>
<string name="import_progress_read">Nuskaitoma užduotis %d...</string> <string name="import_progress_read">Nuskaitoma užduotis %d...</string>
<string name="DLG_error_sdcard">Nėra prieigos prie aplanko: %s</string>
<string name="DLG_error_sdcard_general">Nėra prieigos prie SD kortelės!</string>
<string name="read_permission_label">Tasks leidimas</string> <string name="read_permission_label">Tasks leidimas</string>
<string name="discard_confirmation">Ar tikrai norite atmesti pakeitimus?</string> <string name="discard_confirmation">Ar tikrai norite atmesti pakeitimus?</string>
<string name="keep_editing">Tęsti redagavimą</string> <string name="keep_editing">Tęsti redagavimą</string>

@ -14,8 +14,6 @@
<string name="import_summary_title">Gjennopprettingssammendrag</string> <string name="import_summary_title">Gjennopprettingssammendrag</string>
<string name="import_summary_message">Filen %1$s inneholder %2$s.\n\n %3$s importert,\n %4$s eksisterer allerede\n %5$s inneholdt feil\n</string> <string name="import_summary_message">Filen %1$s inneholder %2$s.\n\n %3$s importert,\n %4$s eksisterer allerede\n %5$s inneholdt feil\n</string>
<string name="import_progress_read">Leser oppgave %d...</string> <string name="import_progress_read">Leser oppgave %d...</string>
<string name="DLG_error_sdcard">Får ikke tilgang til mappen: %s</string>
<string name="DLG_error_sdcard_general">Ditt SD-kort er ikke tilgjengelig!</string>
<string name="read_permission_label">Tasks Tillatelse</string> <string name="read_permission_label">Tasks Tillatelse</string>
<string name="DLG_delete_this_task_question">Slett denne oppgaven?</string> <string name="DLG_delete_this_task_question">Slett denne oppgaven?</string>
<string name="DLG_hour_minutes">Tid (timer : minutter)</string> <string name="DLG_hour_minutes">Tid (timer : minutter)</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">Samenvatting herstellen</string> <string name="import_summary_title">Samenvatting herstellen</string>
<string name="import_summary_message">Bestand %1$s bevat %2$s.\n\n %3$s geïmporteerd,\n %4$s bestaan al\n %5$s had fouten\n</string> <string name="import_summary_message">Bestand %1$s bevat %2$s.\n\n %3$s geïmporteerd,\n %4$s bestaan al\n %5$s had fouten\n</string>
<string name="import_progress_read">Taak %d lezen...</string> <string name="import_progress_read">Taak %d lezen...</string>
<string name="DLG_error_sdcard">Toegang geweigerd tot map: %s</string>
<string name="DLG_error_sdcard_general">Toegang tot SD-kaart geweigerd!</string>
<string name="read_permission_label">Taak toestemmingen</string> <string name="read_permission_label">Taak toestemmingen</string>
<string name="discard_confirmation">Weet u zeker dat u de wijzigingen niet wilt opslaan?</string> <string name="discard_confirmation">Weet u zeker dat u de wijzigingen niet wilt opslaan?</string>
<string name="keep_editing">Blijf editen</string> <string name="keep_editing">Blijf editen</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">Podsumowanie odzyskiwania</string> <string name="import_summary_title">Podsumowanie odzyskiwania</string>
<string name="import_summary_message">Plik %1$s zawiera %2$s.\n\n %3$s zaimportowanych,\n %4$s już istnieje\n %5$s zawiera błędy\n</string> <string name="import_summary_message">Plik %1$s zawiera %2$s.\n\n %3$s zaimportowanych,\n %4$s już istnieje\n %5$s zawiera błędy\n</string>
<string name="import_progress_read">Czytanie zadania %d...</string> <string name="import_progress_read">Czytanie zadania %d...</string>
<string name="DLG_error_sdcard">Brak dostępu do folderu: %s</string>
<string name="DLG_error_sdcard_general">Brak dostępu do Twojej karty SD!</string>
<string name="read_permission_label">Uprawnienia Tasks</string> <string name="read_permission_label">Uprawnienia Tasks</string>
<string name="discard_confirmation">Czy jesteś pewien, że chcesz porzucić zmiany?</string> <string name="discard_confirmation">Czy jesteś pewien, że chcesz porzucić zmiany?</string>
<string name="keep_editing">Kontynuuj edycję</string> <string name="keep_editing">Kontynuuj edycję</string>

@ -20,8 +20,6 @@
%4$s já existiam\n %4$s já existiam\n
%5$s tinham erros\n</string> %5$s tinham erros\n</string>
<string name="import_progress_read">Lendo tarefa %d...</string> <string name="import_progress_read">Lendo tarefa %d...</string>
<string name="DLG_error_sdcard">Não foi possível acessar a pasta: %s</string>
<string name="DLG_error_sdcard_general">Não foi possível acessar seu cartão SD!</string>
<string name="read_permission_label">Permissões do Tasks</string> <string name="read_permission_label">Permissões do Tasks</string>
<string name="discard_confirmation">Você tem certeza que deseja descartar suas mudanças?</string> <string name="discard_confirmation">Você tem certeza que deseja descartar suas mudanças?</string>
<string name="keep_editing">Continuar editando</string> <string name="keep_editing">Continuar editando</string>

@ -20,8 +20,6 @@
%4$s já existiam\n %4$s já existiam\n
%5$s possuem erros\n</string> %5$s possuem erros\n</string>
<string name="import_progress_read">A ler tarefa %d...</string> <string name="import_progress_read">A ler tarefa %d...</string>
<string name="DLG_error_sdcard">Não é possível aceder à pasta: %s</string>
<string name="DLG_error_sdcard_general">Não é possível aceder ao seu cartão SD!</string>
<string name="read_permission_label">Permissões do Tasks</string> <string name="read_permission_label">Permissões do Tasks</string>
<string name="discard_confirmation">Tem a certeza que deseja descartar as alterações?</string> <string name="discard_confirmation">Tem a certeza que deseja descartar as alterações?</string>
<string name="keep_editing">Continuar a editar</string> <string name="keep_editing">Continuar a editar</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">Итог восстановления</string> <string name="import_summary_title">Итог восстановления</string>
<string name="import_summary_message">Файл %1$s содержал %2$s.\n\n %3$s импортировано,\n %4$s существовало\n %5$s с ошибками\n</string> <string name="import_summary_message">Файл %1$s содержал %2$s.\n\n %3$s импортировано,\n %4$s существовало\n %5$s с ошибками\n</string>
<string name="import_progress_read">Чтение задачи %d…</string> <string name="import_progress_read">Чтение задачи %d…</string>
<string name="DLG_error_sdcard">Нет доступа к папке:%s</string>
<string name="DLG_error_sdcard_general">Нет доступа к карте памяти!</string>
<string name="read_permission_label">Разрешения Tasks</string> <string name="read_permission_label">Разрешения Tasks</string>
<string name="discard_confirmation">Вы действительно хотите отказаться от сделанных изменений?</string> <string name="discard_confirmation">Вы действительно хотите отказаться от сделанных изменений?</string>
<string name="keep_editing">Продолжить редактировать</string> <string name="keep_editing">Продолжить редактировать</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">Obnoviť zhrnutie</string> <string name="import_summary_title">Obnoviť zhrnutie</string>
<string name="import_summary_message">Súbor %1$s obsahuje %2$s.\n\n %3$s importované,\n %4$s už existuje\n%5$s je chybných\n</string> <string name="import_summary_message">Súbor %1$s obsahuje %2$s.\n\n %3$s importované,\n %4$s už existuje\n%5$s je chybných\n</string>
<string name="import_progress_read">Čítanie úloh %d...</string> <string name="import_progress_read">Čítanie úloh %d...</string>
<string name="DLG_error_sdcard">Chyba v prístupe do priečinka. %s</string>
<string name="DLG_error_sdcard_general">Chyba v prístupe k SD karte!</string>
<string name="read_permission_label">Povolenia Úloh</string> <string name="read_permission_label">Povolenia Úloh</string>
<string name="discard_confirmation">Naozaj chceš zmazať zmeny?</string> <string name="discard_confirmation">Naozaj chceš zmazať zmeny?</string>
<string name="keep_editing">Pokračuj v úpravách</string> <string name="keep_editing">Pokračuj v úpravách</string>

@ -19,8 +19,6 @@
%4$s že obstaja\n %4$s že obstaja\n
%5$s imel napake\n</string> %5$s imel napake\n</string>
<string name="import_progress_read">Berem opravek %d...</string> <string name="import_progress_read">Berem opravek %d...</string>
<string name="DLG_error_sdcard">Datoteka:%s ni dostopna</string>
<string name="DLG_error_sdcard_general">Tvoja SD kartica ni dostopna.</string>
<string name="read_permission_label">Dovoljenje Opravkom</string> <string name="read_permission_label">Dovoljenje Opravkom</string>
<string name="DLG_delete_this_task_question">Zbrišem ta opravek?</string> <string name="DLG_delete_this_task_question">Zbrišem ta opravek?</string>
<string name="DLG_hour_minutes">Čas (ure : minute)</string> <string name="DLG_hour_minutes">Čas (ure : minute)</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">Återställningssammanfattning</string> <string name="import_summary_title">Återställningssammanfattning</string>
<string name="import_summary_message">Filen %1$s innehöll %2$s.\n\n %3$s importerade,\n %4$s existerar redan\n %5$s hade fel\n</string> <string name="import_summary_message">Filen %1$s innehöll %2$s.\n\n %3$s importerade,\n %4$s existerar redan\n %5$s hade fel\n</string>
<string name="import_progress_read">Läser uppgift %d...</string> <string name="import_progress_read">Läser uppgift %d...</string>
<string name="DLG_error_sdcard">Mappåtkomst nekad: %s</string>
<string name="DLG_error_sdcard_general">SD-kort ej tillgängligt!</string>
<string name="read_permission_label">Tasks Tillstånd</string> <string name="read_permission_label">Tasks Tillstånd</string>
<string name="discard_confirmation">Är du säker på att du inte vill spara dina ändringar?</string> <string name="discard_confirmation">Är du säker på att du inte vill spara dina ändringar?</string>
<string name="keep_editing">Fortsätt redigera</string> <string name="keep_editing">Fortsätt redigera</string>

@ -9,8 +9,6 @@
<string name="actfm_picture_clear">ล้างภาพ</string> <string name="actfm_picture_clear">ล้างภาพ</string>
<string name="backup_BPr_header">สำรองข้อมูล</string> <string name="backup_BPr_header">สำรองข้อมูล</string>
<string name="import_progress_read">กำลังอ่านแผนงาน %d...</string> <string name="import_progress_read">กำลังอ่านแผนงาน %d...</string>
<string name="DLG_error_sdcard">ไม่สามารถเข้าถึงแฟ้ม: %s</string>
<string name="DLG_error_sdcard_general">ไม่สามารถเข้าถึง SD Card ได้!</string>
<string name="DLG_delete_this_task_question">ลบงานนี้?</string> <string name="DLG_delete_this_task_question">ลบงานนี้?</string>
<string name="DLG_hour_minutes">เวลา (ชั่วโมง : นาที)</string> <string name="DLG_hour_minutes">เวลา (ชั่วโมง : นาที)</string>
<string name="WID_dateButtonUnset">คลิก เพื่อตั้งค่า</string> <string name="WID_dateButtonUnset">คลิก เพื่อตั้งค่า</string>

@ -22,8 +22,6 @@
%5$s hatalar var %5$s hatalar var
</string> </string>
<string name="import_progress_read">%d görev okunuyor..</string> <string name="import_progress_read">%d görev okunuyor..</string>
<string name="DLG_error_sdcard">%s dizinine erişilemedi.</string>
<string name="DLG_error_sdcard_general">Bellek kartınıza erişilemiyor!</string>
<string name="read_permission_label">Tasks İzni</string> <string name="read_permission_label">Tasks İzni</string>
<string name="discard_confirmation">Değişikliklerinizi gözden çıkarmak istediğinize emin misiniz?</string> <string name="discard_confirmation">Değişikliklerinizi gözden çıkarmak istediğinize emin misiniz?</string>
<string name="keep_editing">Düzenlemeyi sürdür</string> <string name="keep_editing">Düzenlemeyi sürdür</string>

@ -21,8 +21,6 @@
%4$s вже існує\n %4$s вже існує\n
%5$s має помилку\n</string> %5$s має помилку\n</string>
<string name="import_progress_read">Читання завдання %d...</string> <string name="import_progress_read">Читання завдання %d...</string>
<string name="DLG_error_sdcard">Немає доступу до папки: %s</string>
<string name="DLG_error_sdcard_general">Немає доступу до карти SD!</string>
<string name="read_permission_label">Дозволи Tasks</string> <string name="read_permission_label">Дозволи Tasks</string>
<string name="discard_confirmation">Дійсно хочете скасувати ваші зміни?</string> <string name="discard_confirmation">Дійсно хочете скасувати ваші зміни?</string>
<string name="keep_editing">Продовжити редагування</string> <string name="keep_editing">Продовжити редагування</string>

@ -18,8 +18,6 @@
<string name="import_summary_title">还原摘要</string> <string name="import_summary_title">还原摘要</string>
<string name="import_summary_message">文件 %1$s 包含 %2$s 个任务。\n\n %3$s 个已导入,\n %4$s 个已存在\n %5$s 个存在错误\n</string> <string name="import_summary_message">文件 %1$s 包含 %2$s 个任务。\n\n %3$s 个已导入,\n %4$s 个已存在\n %5$s 个存在错误\n</string>
<string name="import_progress_read">读取任务 %d...</string> <string name="import_progress_read">读取任务 %d...</string>
<string name="DLG_error_sdcard">无法开启文件夹:%s</string>
<string name="DLG_error_sdcard_general">无法访问您的 SD 卡!</string>
<string name="read_permission_label">清单小助理权限</string> <string name="read_permission_label">清单小助理权限</string>
<string name="discard_confirmation">确定放弃你的改变吗?</string> <string name="discard_confirmation">确定放弃你的改变吗?</string>
<string name="keep_editing">继续编辑</string> <string name="keep_editing">继续编辑</string>

@ -16,8 +16,6 @@
<string name="import_summary_title">還原摘要</string> <string name="import_summary_title">還原摘要</string>
<string name="import_summary_message">檔案 %1$s 已包含 %2$s.\n\n %3$s 已匯入,\n %4$s 已存在\n %5$s 有問題\n</string> <string name="import_summary_message">檔案 %1$s 已包含 %2$s.\n\n %3$s 已匯入,\n %4$s 已存在\n %5$s 有問題\n</string>
<string name="import_progress_read">讀取工作 %d...</string> <string name="import_progress_read">讀取工作 %d...</string>
<string name="DLG_error_sdcard">無法開啟資料夾: %s</string>
<string name="DLG_error_sdcard_general">無法存取您的SD卡!</string>
<string name="read_permission_label">Tasks 權限</string> <string name="read_permission_label">Tasks 權限</string>
<string name="DLG_delete_this_task_question">確認刪除?</string> <string name="DLG_delete_this_task_question">確認刪除?</string>
<string name="DLG_hour_minutes">時間 (小時:分鐘)</string> <string name="DLG_hour_minutes">時間 (小時:分鐘)</string>

@ -46,12 +46,6 @@ File %1$s contained %2$s.\n\n
<!-- Progress Dialog text for import reading task (%d -> task number)--> <!-- Progress Dialog text for import reading task (%d -> task number)-->
<string name="import_progress_read">Reading task %d…</string> <string name="import_progress_read">Reading task %d…</string>
<!-- Backup: Dialog when unable to open SD card folder (%s => folder) -->
<string name="DLG_error_sdcard">Cannot access folder: %s</string>
<!-- Backup: Dialog when unable to open SD card in general -->
<string name="DLG_error_sdcard_general">Cannot access your SD card!</string>
<!-- ================================================== AndroidManifest == --> <!-- ================================================== AndroidManifest == -->
<!-- permission title for READ_TASKS --> <!-- permission title for READ_TASKS -->

Loading…
Cancel
Save