Use storage access framework for attachments

pull/795/head
Alex Baker 7 years ago
parent 09156c5243
commit 593b5bc5c5

@ -14,11 +14,6 @@ public class PermissivePermissionChecker extends PermissionChecker {
return true; return true;
} }
@Override
public boolean canWriteToExternalStorage() {
return true;
}
@Override @Override
public boolean canAccessAccounts() { public boolean canAccessAccounts() {
return true; return true;

@ -134,40 +134,6 @@ public class AndroidUtilities {
} }
} }
/** Copy a file from one place to another */
public static void copyFile(File in, File out) throws Exception {
FileInputStream fis = new FileInputStream(in);
FileOutputStream fos = new FileOutputStream(out);
try {
copyStream(fis, fos);
} finally {
fis.close();
fos.close();
}
}
/** Copy stream from source to destination */
private static void copyStream(InputStream source, OutputStream dest) throws IOException {
int bytes;
byte[] buffer;
int BUFFER_SIZE = 1024;
buffer = new byte[BUFFER_SIZE];
while ((bytes = source.read(buffer)) != -1) {
if (bytes == 0) {
bytes = source.read();
if (bytes < 0) {
break;
}
dest.write(bytes);
dest.flush();
continue;
}
dest.write(buffer, 0, bytes);
dest.flush();
}
}
public static int convertDpToPixels(DisplayMetrics displayMetrics, int dp) { public static int convertDpToPixels(DisplayMetrics displayMetrics, int dp) {
// developer.android.com/guide/practices/screens_support.html#dips-pels // developer.android.com/guide/practices/screens_support.html#dips-pels
return (int) (dp * displayMetrics.density + 0.5f); return (int) (dp * displayMetrics.density + 0.5f);

@ -14,6 +14,7 @@ import android.annotation.SuppressLint;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
@ -464,7 +465,7 @@ public class MainActivity extends InjectingAppCompatActivity
} }
@Override @Override
public void addComment(String message, String picture) { public void addComment(String message, Uri picture) {
TaskEditFragment taskEditFragment = getTaskEditFragment(); TaskEditFragment taskEditFragment = getTaskEditFragment();
if (taskEditFragment != null) { if (taskEditFragment != null) {
taskEditFragment.addComment(message, picture); taskEditFragment.addComment(message, picture);

@ -5,23 +5,17 @@
*/ */
package com.todoroo.astrid.activity; package com.todoroo.astrid.activity;
import static org.tasks.date.DateTimeUtils.newDateTime;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.core.content.ContextCompat;
import androidx.appcompat.widget.Toolbar;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskDao;
@ -33,8 +27,7 @@ import com.todoroo.astrid.service.TaskDeleter;
import com.todoroo.astrid.timers.TimerPlugin; import com.todoroo.astrid.timers.TimerPlugin;
import com.todoroo.astrid.ui.EditTitleControlSet; import com.todoroo.astrid.ui.EditTitleControlSet;
import com.todoroo.astrid.utility.Flags; import com.todoroo.astrid.utility.Flags;
import java.util.List;
import javax.inject.Inject;
import org.tasks.LocalBroadcastManager; import org.tasks.LocalBroadcastManager;
import org.tasks.R; import org.tasks.R;
import org.tasks.analytics.Tracker; import org.tasks.analytics.Tracker;
@ -50,6 +43,22 @@ import org.tasks.preferences.Preferences;
import org.tasks.ui.MenuColorizer; import org.tasks.ui.MenuColorizer;
import org.tasks.ui.TaskEditControlFragment; import org.tasks.ui.TaskEditControlFragment;
import java.io.IOException;
import java.util.List;
import javax.inject.Inject;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import butterknife.BindView;
import butterknife.ButterKnife;
import timber.log.Timber;
import static org.tasks.date.DateTimeUtils.newDateTime;
import static org.tasks.files.FileHelper.copyToUri;
public final class TaskEditFragment extends InjectingFragment public final class TaskEditFragment extends InjectingFragment
implements Toolbar.OnMenuItemClickListener { implements Toolbar.OnMenuItemClickListener {
@ -320,14 +329,15 @@ public final class TaskEditFragment extends InjectingFragment
} }
} }
public void addComment(String message, String picture) { void addComment(String message, Uri picture) {
UserActivity userActivity = new UserActivity(); UserActivity userActivity = new UserActivity();
if (picture != null) {
Uri output = copyToUri(context, preferences.getAttachmentsDirectory(), picture);
userActivity.setPicture(output);
}
userActivity.setMessage(message); userActivity.setMessage(message);
userActivity.setTargetId(model.getUuid()); userActivity.setTargetId(model.getUuid());
userActivity.setCreated(DateUtilities.now()); userActivity.setCreated(DateUtilities.now());
if (picture != null) {
userActivity.setPicture(picture);
}
userActivityDao.createNew(userActivity); userActivityDao.createNew(userActivity);
commentsController.reloadView(); commentsController.reloadView();
} }

@ -39,14 +39,11 @@ import org.xmlpull.v1.XmlPullParserFactory;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.StringReader;
import javax.inject.Inject; 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$
@ -120,7 +117,7 @@ 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();
InputStream inputStream = fromUri(activity, input); InputStream inputStream = activity.getContentResolver().openInputStream(input);
InputStreamReader reader = new InputStreamReader(inputStream); InputStreamReader reader = new InputStreamReader(inputStream);
xpp.setInput(reader); xpp.setInput(reader);

@ -5,55 +5,43 @@
*/ */
package com.todoroo.astrid.files; package com.todoroo.astrid.files;
import static android.app.Activity.RESULT_OK;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.tasks.dialogs.AddAttachmentDialog.REQUEST_STORAGE;
import static org.tasks.dialogs.AddAttachmentDialog.newAddAttachmentDialog;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.media.MediaPlayer;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.webkit.MimeTypeMap;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import butterknife.BindView;
import butterknife.OnClick;
import com.google.common.base.Strings;
import com.todoroo.andlib.utility.AndroidUtilities;
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.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import org.tasks.R; import org.tasks.R;
import org.tasks.activities.CameraActivity;
import org.tasks.data.TaskAttachment; import org.tasks.data.TaskAttachment;
import org.tasks.data.TaskAttachmentDao; import org.tasks.data.TaskAttachmentDao;
import org.tasks.dialogs.AddAttachmentDialog;
import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.DialogBuilder;
import org.tasks.files.FileExplore;
import org.tasks.files.FileHelper; import org.tasks.files.FileHelper;
import org.tasks.injection.ForActivity; import org.tasks.injection.ForActivity;
import org.tasks.injection.FragmentComponent; import org.tasks.injection.FragmentComponent;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.ui.TaskEditControlFragment; import org.tasks.ui.TaskEditControlFragment;
import timber.log.Timber;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.OnClick;
import static android.app.Activity.RESULT_OK;
import static org.tasks.data.TaskAttachment.createNewAttachment;
import static org.tasks.dialogs.AddAttachmentDialog.REQUEST_AUDIO;
import static org.tasks.dialogs.AddAttachmentDialog.REQUEST_CAMERA;
import static org.tasks.dialogs.AddAttachmentDialog.REQUEST_GALLERY;
import static org.tasks.dialogs.AddAttachmentDialog.REQUEST_STORAGE;
import static org.tasks.dialogs.AddAttachmentDialog.newAddAttachmentDialog;
import static org.tasks.files.FileHelper.copyToUri;
public class FilesControlSet extends TaskEditControlFragment { public class FilesControlSet extends TaskEditControlFragment {
@ -75,19 +63,6 @@ public class FilesControlSet extends TaskEditControlFragment {
private String taskUuid; private String taskUuid;
private static void play(String file, PlaybackExceptionHandler handler) {
MediaPlayer mediaPlayer = new MediaPlayer();
try {
mediaPlayer.setDataSource(file);
mediaPlayer.prepare();
mediaPlayer.start();
} catch (Exception e) {
Timber.e(e);
handler.playbackFailed();
}
}
@Nullable @Nullable
@Override @Override
public View onCreateView( public View onCreateView(
@ -96,12 +71,9 @@ public class FilesControlSet extends TaskEditControlFragment {
taskUuid = task.getUuid(); taskUuid = task.getUuid();
final List<TaskAttachment> files = new ArrayList<>();
for (TaskAttachment attachment : taskAttachmentDao.getAttachments(taskUuid)) { for (TaskAttachment attachment : taskAttachmentDao.getAttachments(taskUuid)) {
files.add(attachment);
addAttachment(attachment); addAttachment(attachment);
} }
validateFiles(files);
return view; return view;
} }
@ -131,46 +103,12 @@ public class FilesControlSet extends TaskEditControlFragment {
@Override @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == AddAttachmentDialog.REQUEST_CAMERA) { if (requestCode == REQUEST_CAMERA
if (resultCode == RESULT_OK) { || requestCode == REQUEST_STORAGE
Uri uri = data.getParcelableExtra(CameraActivity.EXTRA_URI); || requestCode == REQUEST_GALLERY
final File file = new File(uri.getPath()); || requestCode == REQUEST_AUDIO) {
String path = file.getPath();
Timber.i("Saved %s", file.getAbsolutePath());
final String extension = path.substring(path.lastIndexOf('.') + 1);
createNewFileAttachment(file, TaskAttachment.FILE_TYPE_IMAGE + extension);
}
} else if (requestCode == AddAttachmentDialog.REQUEST_AUDIO) {
if (resultCode == Activity.RESULT_OK) {
String path = data.getStringExtra(AddAttachmentDialog.EXTRA_PATH);
String type = data.getStringExtra(AddAttachmentDialog.EXTRA_TYPE);
createNewFileAttachment(new File(path), type);
}
} else if (requestCode == AddAttachmentDialog.REQUEST_GALLERY) {
if (resultCode == RESULT_OK) {
Uri uri = data.getData();
ContentResolver contentResolver = context.getContentResolver();
MimeTypeMap mime = MimeTypeMap.getSingleton();
final String extension = mime.getExtensionFromMimeType(contentResolver.getType(uri));
final File tempFile = getFilename(extension);
Timber.i("Writing %s to %s", uri, tempFile);
try {
InputStream inputStream = contentResolver.openInputStream(uri);
copyFile(inputStream, tempFile.getPath());
} catch (IOException e) {
throw new RuntimeException(e);
}
createNewFileAttachment(tempFile, TaskAttachment.FILE_TYPE_IMAGE + extension);
}
} else if (requestCode == REQUEST_STORAGE) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
String path = data.getStringExtra(FileExplore.EXTRA_FILE); copyToAttachmentDirectory(data.getData());
final String destination = copyToAttachmentDirectory(path);
if (destination != null) {
Timber.i("Copied %s to %s", path, destination);
final String extension = destination.substring(path.lastIndexOf('.') + 1);
createNewFileAttachment(new File(path), TaskAttachment.FILE_TYPE_IMAGE + extension);
}
} }
} else { } else {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
@ -199,32 +137,13 @@ public class FilesControlSet extends TaskEditControlFragment {
android.R.string.ok, android.R.string.ok,
(dialog, which) -> { (dialog, which) -> {
taskAttachmentDao.delete(taskAttachment); taskAttachmentDao.delete(taskAttachment);
if (!Strings.isNullOrEmpty(taskAttachment.getPath())) { FileHelper.delete(context, taskAttachment.parseUri());
File f = new File(taskAttachment.getPath());
f.delete();
}
attachmentContainer.removeView(fileRow); attachmentContainer.removeView(fileRow);
}) })
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show()); .show());
} }
private void validateFiles(List<TaskAttachment> files) {
for (int i = 0; i < files.size(); i++) {
TaskAttachment m = files.get(i);
if (!Strings.isNullOrEmpty(m.getPath())) {
File f = new File(m.getPath());
if (!f.exists()) {
m.setPath(""); // $NON-NLS-1$
// No local file and no url -- delete the metadata
taskAttachmentDao.delete(m);
files.remove(i);
i--;
}
}
}
}
@Override @Override
protected void inject(FragmentComponent component) { protected void inject(FragmentComponent component) {
component.inject(this); component.inject(this);
@ -232,113 +151,17 @@ public class FilesControlSet extends TaskEditControlFragment {
@SuppressLint("NewApi") @SuppressLint("NewApi")
private void showFile(final TaskAttachment m) { private void showFile(final TaskAttachment m) {
final String fileType = final Uri uri = m.parseUri();
!Strings.isNullOrEmpty(m.getContentType()) if (uri != null) {
? m.getContentType() FileHelper.startActionView(getActivity(), uri);
: TaskAttachment.FILE_TYPE_OTHER;
final String filePath = m.getPath();
if (fileType.startsWith(TaskAttachment.FILE_TYPE_AUDIO)) {
play(m.getPath(), () -> showFromIntent(filePath, fileType));
} else if (fileType.startsWith(TaskAttachment.FILE_TYPE_IMAGE)) {
try {
Intent intent =
FileHelper.getReadableActionView(
context, filePath, TaskAttachment.FILE_TYPE_IMAGE + "*");
getActivity().startActivity(intent);
} catch (ActivityNotFoundException e) {
Timber.e(e);
Toast.makeText(context, R.string.no_application_found, Toast.LENGTH_SHORT).show();
}
} else {
String useType = fileType;
if (fileType.equals(TaskAttachment.FILE_TYPE_OTHER)) {
String extension = AndroidUtilities.getFileExtension(filePath);
MimeTypeMap map = MimeTypeMap.getSingleton();
String guessedType = map.getMimeTypeFromExtension(extension);
if (!TextUtils.isEmpty(guessedType)) {
useType = guessedType;
}
if (!useType.equals(guessedType)) {
m.setContentType(useType);
taskAttachmentDao.update(m);
}
}
showFromIntent(filePath, useType);
}
}
private void showFromIntent(String file, String type) {
try {
Intent intent = FileHelper.getReadableActionView(context, file, type);
getActivity().startActivity(intent);
} catch (ActivityNotFoundException e) {
Timber.e(e);
Toast.makeText(context, R.string.file_type_unhandled, Toast.LENGTH_LONG).show();
} }
} }
private void createNewFileAttachment(File file, String fileType) { private void copyToAttachmentDirectory(Uri input) {
Uri output = copyToUri(context, preferences.getAttachmentsDirectory(), input);
TaskAttachment attachment = TaskAttachment attachment =
TaskAttachment.createNewAttachment( createNewAttachment(taskUuid, output, FileHelper.getFilename(context, output));
taskUuid, file.getAbsolutePath(), file.getName(), fileType);
taskAttachmentDao.createNew(attachment); taskAttachmentDao.createNew(attachment);
addAttachment(attachment); addAttachment(attachment);
} }
private File getFilename(String extension) {
AtomicReference<String> nameRef = new AtomicReference<>();
if (isNullOrEmpty(extension)) {
extension = "";
} else if (!extension.startsWith(".")) {
extension = "." + extension;
}
try {
String path = preferences.getNewAttachmentPath(extension, nameRef);
File file = new File(path);
file.getParentFile().mkdirs();
if (!file.createNewFile()) {
throw new RuntimeException("Failed to create " + file.getPath());
}
return file;
} catch (IOException e) {
Timber.e(e);
}
return null;
}
private void copyFile(InputStream inputStream, String to) throws IOException {
FileOutputStream fos = new FileOutputStream(to);
byte[] buf = new byte[1024];
int len;
while ((len = inputStream.read(buf)) != -1) {
fos.write(buf, 0, len);
}
fos.close();
}
private String copyToAttachmentDirectory(String file) {
File src = new File(file);
if (!src.exists()) {
Toast.makeText(context, R.string.file_err_copy, Toast.LENGTH_LONG).show();
return null;
}
File dst = new File(preferences.getAttachmentsDirectory() + File.separator + src.getName());
try {
AndroidUtilities.copyFile(src, dst);
} catch (Exception e) {
Timber.e(e);
Toast.makeText(context, R.string.file_err_copy, Toast.LENGTH_LONG).show();
return null;
}
return dst.getAbsolutePath();
}
interface PlaybackExceptionHandler {
void playbackFailed();
}
} }

@ -5,15 +5,9 @@
*/ */
package com.todoroo.astrid.notes; package com.todoroo.astrid.notes;
import static androidx.core.content.ContextCompat.getColor;
import static org.tasks.files.FileHelper.getPathFromUri;
import static org.tasks.files.ImageHelper.sampleBitmap;
import android.app.Activity; import android.app.Activity;
import android.content.Intent;
import android.graphics.Color; import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import androidx.core.content.FileProvider;
import android.text.Html; import android.text.Html;
import android.text.util.Linkify; import android.text.util.Linkify;
import android.view.View; import android.view.View;
@ -21,18 +15,23 @@ import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.utility.Constants;
import java.io.File;
import java.util.ArrayList;
import javax.inject.Inject;
import org.tasks.R; import org.tasks.R;
import org.tasks.data.UserActivity; import org.tasks.data.UserActivity;
import org.tasks.data.UserActivityDao; import org.tasks.data.UserActivityDao;
import org.tasks.files.FileHelper; import org.tasks.files.FileHelper;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import java.util.ArrayList;
import javax.inject.Inject;
import static androidx.core.content.ContextCompat.getColor;
import static org.tasks.files.ImageHelper.sampleBitmap;
public class CommentsController { public class CommentsController {
private final UserActivityDao userActivityDao; private final UserActivityDao userActivityDao;
@ -53,27 +52,17 @@ public class CommentsController {
} }
private static void setupImagePopupForCommentView( private static void setupImagePopupForCommentView(
View view, ImageView commentPictureView, final Uri updateBitmap, final Activity activity) { View view, ImageView commentPictureView, final Uri uri, final Activity activity) {
if (updateBitmap != null) { if (uri != null) {
commentPictureView.setVisibility(View.VISIBLE); commentPictureView.setVisibility(View.VISIBLE);
String path = getPathFromUri(activity, updateBitmap);
commentPictureView.setImageBitmap( commentPictureView.setImageBitmap(
sampleBitmap( sampleBitmap(
path, activity,
uri,
commentPictureView.getLayoutParams().width, commentPictureView.getLayoutParams().width,
commentPictureView.getLayoutParams().height)); commentPictureView.getLayoutParams().height));
view.setOnClickListener( view.setOnClickListener(v -> FileHelper.startActionView(activity, uri));
v -> {
File file = new File(updateBitmap.getPath());
Uri uri =
FileProvider.getUriForFile(
activity, Constants.FILE_PROVIDER_AUTHORITY, file.getAbsoluteFile());
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, "image/*");
FileHelper.grantReadPermissions(activity, intent, uri);
activity.startActivity(intent);
});
} else { } else {
commentPictureView.setVisibility(View.GONE); commentPictureView.setVisibility(View.GONE);
} }

@ -5,9 +5,8 @@
*/ */
package com.todoroo.astrid.service; package com.todoroo.astrid.service;
import static com.google.common.base.Strings.isNullOrEmpty;
import android.content.Context; import android.content.Context;
import android.net.Uri;
import android.os.Environment; import android.os.Environment;
import com.google.common.base.Strings; import com.google.common.base.Strings;
@ -18,9 +17,7 @@ import com.google.common.collect.Multimaps;
import com.todoroo.astrid.api.GtasksFilter; import com.todoroo.astrid.api.GtasksFilter;
import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.dao.Database;
import com.todoroo.astrid.tags.TagService; import com.todoroo.astrid.tags.TagService;
import java.io.File;
import java.util.List;
import javax.inject.Inject;
import org.tasks.BuildConfig; import org.tasks.BuildConfig;
import org.tasks.LocalBroadcastManager; import org.tasks.LocalBroadcastManager;
import org.tasks.R; import org.tasks.R;
@ -35,12 +32,24 @@ import org.tasks.data.Tag;
import org.tasks.data.TagDao; import org.tasks.data.TagDao;
import org.tasks.data.TagData; import org.tasks.data.TagData;
import org.tasks.data.TagDataDao; import org.tasks.data.TagDataDao;
import org.tasks.data.TaskAttachment;
import org.tasks.data.TaskAttachmentDao;
import org.tasks.data.UserActivity;
import org.tasks.data.UserActivityDao;
import org.tasks.injection.ForApplication; import org.tasks.injection.ForApplication;
import org.tasks.preferences.DefaultFilterProvider; import org.tasks.preferences.DefaultFilterProvider;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.scheduling.BackgroundScheduler; import org.tasks.scheduling.BackgroundScheduler;
import java.io.File;
import java.util.List;
import javax.inject.Inject;
import timber.log.Timber; import timber.log.Timber;
import static com.google.common.base.Strings.isNullOrEmpty;
public class StartupService { public class StartupService {
private static final int V4_8_0 = 380; private static final int V4_8_0 = 380;
@ -61,6 +70,8 @@ public class StartupService {
private final FilterDao filterDao; private final FilterDao filterDao;
private final DefaultFilterProvider defaultFilterProvider; private final DefaultFilterProvider defaultFilterProvider;
private final GoogleTaskListDao googleTaskListDao; private final GoogleTaskListDao googleTaskListDao;
private final UserActivityDao userActivityDao;
private final TaskAttachmentDao taskAttachmentDao;
@Inject @Inject
public StartupService( public StartupService(
@ -74,7 +85,9 @@ public class StartupService {
TagDao tagDao, TagDao tagDao,
FilterDao filterDao, FilterDao filterDao,
DefaultFilterProvider defaultFilterProvider, DefaultFilterProvider defaultFilterProvider,
GoogleTaskListDao googleTaskListDao) { GoogleTaskListDao googleTaskListDao,
UserActivityDao userActivityDao,
TaskAttachmentDao taskAttachmentDao) {
this.database = database; this.database = database;
this.preferences = preferences; this.preferences = preferences;
this.tracker = tracker; this.tracker = tracker;
@ -86,6 +99,8 @@ public class StartupService {
this.filterDao = filterDao; this.filterDao = filterDao;
this.defaultFilterProvider = defaultFilterProvider; this.defaultFilterProvider = defaultFilterProvider;
this.googleTaskListDao = googleTaskListDao; this.googleTaskListDao = googleTaskListDao;
this.userActivityDao = userActivityDao;
this.taskAttachmentDao = taskAttachmentDao;
} }
/** Called when this application is started up */ /** Called when this application is started up */
@ -198,18 +213,32 @@ public class StartupService {
} }
private void migrateUris() { private void migrateUris() {
String backupDirectory = preferences.getStringValue(R.string.p_backup_dir); migrateUriPreference(R.string.p_backup_dir);
if (!Strings.isNullOrEmpty(backupDirectory)) { migrateUriPreference(R.string.p_attachment_dir);
File file = new File(backupDirectory); for (UserActivity userActivity : userActivityDao.getComments()) {
try { userActivity.convertPictureUri();
if (file.canWrite()) { userActivityDao.update(userActivity);
preferences.setUri(R.string.p_backup_dir, file.toURI()); }
} else { for (TaskAttachment attachment : taskAttachmentDao.getAttachments()) {
preferences.remove(R.string.p_backup_dir); attachment.convertPathUri();
} taskAttachmentDao.update(attachment);
} catch (SecurityException ignored) { }
preferences.remove(R.string.p_backup_dir); }
private void migrateUriPreference(int pref) {
String path = preferences.getStringValue(pref);
if (Strings.isNullOrEmpty(path)) {
return;
}
File file = new File(path);
try {
if (file.canWrite()) {
preferences.setUri(pref, file.toURI());
} else {
preferences.remove(pref);
} }
} catch (SecurityException ignored) {
preferences.remove(pref);
} }
} }

@ -1,36 +1,49 @@
package com.todoroo.astrid.voice; package com.todoroo.astrid.voice;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import android.content.Context;
import android.media.MediaRecorder; import android.media.MediaRecorder;
import android.net.Uri;
import android.os.SystemClock; import android.os.SystemClock;
import java.io.IOException; import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import org.tasks.files.FileHelper;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime;
import timber.log.Timber; import timber.log.Timber;
public class AACRecorder extends ViewModel { public class AACRecorder extends ViewModel {
private MediaRecorder mediaRecorder; private MediaRecorder mediaRecorder;
private final AtomicReference<String> nameRef = new AtomicReference<>();
private boolean recording; private boolean recording;
private AACRecorderCallbacks listener; private AACRecorderCallbacks listener;
private Preferences preferences; private Preferences preferences;
private long base; private long base;
private String tempFile; private Uri uri;
public synchronized void startRecording() { public synchronized void startRecording(Context context) throws IOException {
if (recording) { if (recording) {
return; return;
} }
tempFile = preferences.getNewAudioAttachmentPath(nameRef); uri =
FileHelper.newFile(
context,
preferences.getCacheDirectory(),
"audio/m4a",
new DateTime().toString("yyyyMMddHHmm"),
".m4a");
mediaRecorder = new MediaRecorder(); mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.setOutputFile(tempFile); mediaRecorder.setOutputFile(uri.getPath());
mediaRecorder.setOnErrorListener( mediaRecorder.setOnErrorListener(
(mr, what, extra) -> Timber.e("mediaRecorder.onError(mr, %s, %s)", what, extra)); (mr, what, extra) -> Timber.e("mediaRecorder.onError(mr, %s, %s)", what, extra));
mediaRecorder.setOnInfoListener( mediaRecorder.setOnInfoListener(
@ -63,7 +76,7 @@ public class AACRecorder extends ViewModel {
mediaRecorder.release(); mediaRecorder.release();
recording = false; recording = false;
if (listener != null) { if (listener != null) {
listener.encodingFinished(tempFile); listener.encodingFinished(uri);
} }
} }
@ -78,6 +91,6 @@ public class AACRecorder extends ViewModel {
public interface AACRecorderCallbacks { public interface AACRecorderCallbacks {
void encodingFinished(String path); void encodingFinished(Uri uri);
} }
} }

@ -1,36 +1,39 @@
package org.tasks.activities; package org.tasks.activities;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.provider.MediaStore; import android.provider.MediaStore;
import androidx.core.content.FileProvider;
import android.widget.Toast;
import com.todoroo.astrid.utility.Constants; import com.todoroo.astrid.utility.Constants;
import org.tasks.files.FileHelper;
import org.tasks.injection.ActivityComponent;
import org.tasks.injection.InjectingAppCompatActivity;
import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject; import javax.inject.Inject;
import org.tasks.R;
import org.tasks.injection.ActivityComponent; import androidx.core.content.FileProvider;
import org.tasks.injection.InjectingAppCompatActivity;
import org.tasks.preferences.Preferences; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop;
import timber.log.Timber;
public class CameraActivity extends InjectingAppCompatActivity { public class CameraActivity extends InjectingAppCompatActivity {
public static final String EXTRA_URI = "extra_uri";
private static final int REQUEST_CODE_CAMERA = 75; private static final int REQUEST_CODE_CAMERA = 75;
private static final String EXTRA_OUTPUT = "extra_output"; private static final String EXTRA_URI = "extra_output";
@Inject Preferences preferences; @Inject Preferences preferences;
private File output; private Uri uri;
@SuppressLint("NewApi") @SuppressLint("NewApi")
@Override @Override
@ -38,29 +41,41 @@ public class CameraActivity extends InjectingAppCompatActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (savedInstanceState != null) { if (savedInstanceState != null) {
output = (File) savedInstanceState.getSerializable(EXTRA_OUTPUT); uri = savedInstanceState.getParcelable(EXTRA_URI);
} else { } else {
output = getFilename(".jpeg"); try {
if (output == null) { uri =
Toast.makeText(this, R.string.external_storage_unavailable, Toast.LENGTH_LONG).show(); FileHelper.newFile(
this,
preferences.getCacheDirectory(),
"image/jpeg",
new DateTime().toString("yyyyMMddHHmm"),
".jpeg");
} catch (IOException e) {
throw new RuntimeException(e);
}
if (!uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
throw new RuntimeException("Invalid Uri");
}
final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(
MediaStore.EXTRA_OUTPUT,
FileProvider.getUriForFile(
this, Constants.FILE_PROVIDER_AUTHORITY, new File(uri.getPath())));
if (atLeastLollipop()) {
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else { } else {
final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); List<ResolveInfo> resolveInfoList =
Uri uri = FileProvider.getUriForFile(this, Constants.FILE_PROVIDER_AUTHORITY, output); getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); for (ResolveInfo resolveInfo : resolveInfoList) {
if (atLeastLollipop()) { grantUriPermission(
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); resolveInfo.activityInfo.packageName,
} else { uri,
List<ResolveInfo> resolveInfoList = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resolveInfoList) {
grantUriPermission(
resolveInfo.activityInfo.packageName,
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
} }
startActivityForResult(intent, REQUEST_CODE_CAMERA);
} }
startActivityForResult(intent, REQUEST_CODE_CAMERA);
} }
} }
@ -73,12 +88,9 @@ public class CameraActivity extends InjectingAppCompatActivity {
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_CAMERA) { if (requestCode == REQUEST_CODE_CAMERA) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
if (output != null) {
final Uri uri = Uri.fromFile(output);
Intent intent = new Intent(); Intent intent = new Intent();
intent.putExtra(EXTRA_URI, uri); intent.setData(uri);
setResult(RESULT_OK, intent); setResult(RESULT_OK, intent);
}
} }
finish(); finish();
} else { } else {
@ -90,25 +102,6 @@ public class CameraActivity extends InjectingAppCompatActivity {
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putSerializable(EXTRA_OUTPUT, output); outState.putParcelable(EXTRA_URI, uri);
}
private File getFilename(String extension) {
AtomicReference<String> nameRef = new AtomicReference<>();
if (!extension.startsWith(".")) {
extension = "." + extension;
}
try {
String path = preferences.getNewAttachmentPath(extension, nameRef);
File file = new File(path);
file.getParentFile().mkdirs();
if (!file.createNewFile()) {
throw new RuntimeException("Failed to create " + file.getPath());
}
return file;
} catch (IOException e) {
Timber.e(e);
}
return null;
} }
} }

@ -7,6 +7,7 @@ import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.widget.Toast; import android.widget.Toast;
import com.google.common.io.Files;
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;
@ -138,7 +139,8 @@ public class TasksJsonExporter {
List<Task> tasks = taskDao.getAll(); List<Task> tasks = taskDao.getAll();
if (tasks.size() > 0) { if (tasks.size() > 0) {
Uri uri = FileHelper.newFile(context, backupDirectory, "application/json", filename); String basename = Files.getNameWithoutExtension(filename);
Uri uri = FileHelper.newFile(context, backupDirectory, "application/json", basename, ".json");
OutputStream os = context.getContentResolver().openOutputStream(uri); OutputStream os = context.getContentResolver().openOutputStream(uri);
doTasksExport(os, tasks); doTasksExport(os, tasks);
os.close(); os.close();

@ -40,6 +40,7 @@ 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.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -48,8 +49,6 @@ 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;
@ -120,12 +119,18 @@ public class TasksJsonImporter {
private void performImport() { private void performImport() {
Gson gson = new Gson(); Gson gson = new Gson();
InputStream is = fromUri(activity, this.input); InputStream is;
try {
is = activity.getContentResolver().openInputStream(this.input);
} catch (FileNotFoundException e) {
throw new IllegalStateException(e);
}
InputStreamReader reader = new InputStreamReader(is); InputStreamReader reader = new InputStreamReader(is);
JsonObject input = gson.fromJson(reader, JsonObject.class); JsonObject input = gson.fromJson(reader, JsonObject.class);
try { try {
JsonElement data = input.get("data"); JsonElement data = input.get("data");
int version = input.get("version").getAsInt();
BackupContainer backupContainer = gson.fromJson(data, BackupContainer.class); BackupContainer backupContainer = gson.fromJson(data, BackupContainer.class);
for (TagData tagData : backupContainer.getTags()) { for (TagData tagData : backupContainer.getTags()) {
if (tagDataDao.getByUuid(tagData.getRemoteId()) == null) { if (tagDataDao.getByUuid(tagData.getRemoteId()) == null) {
@ -174,6 +179,9 @@ public class TasksJsonImporter {
} }
for (UserActivity comment : backup.comments) { for (UserActivity comment : backup.comments) {
comment.setTargetId(taskUuid); comment.setTargetId(taskUuid);
if (version < 546) {
comment.convertPictureUri();
}
userActivityDao.createNew(comment); userActivityDao.createNew(comment);
} }
for (GoogleTask googleTask : backup.google) { for (GoogleTask googleTask : backup.google) {
@ -191,6 +199,9 @@ public class TasksJsonImporter {
} }
for (TaskAttachment attachment : backup.getAttachments()) { for (TaskAttachment attachment : backup.getAttachments()) {
attachment.setTaskId(taskUuid); attachment.setTaskId(taskUuid);
if (version < 546) {
attachment.convertPathUri();
}
taskAttachmentDao.insert(attachment); taskAttachmentDao.insert(attachment);
} }
for (CaldavTask caldavTask : backup.getCaldavTasks()) { for (CaldavTask caldavTask : backup.getCaldavTasks()) {

@ -1,12 +1,18 @@
package org.tasks.data; package org.tasks.data;
import android.net.Uri;
import androidx.room.ColumnInfo; import androidx.room.ColumnInfo;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import com.google.common.base.Strings;
import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Table; import com.todoroo.andlib.data.Table;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import java.io.File;
@Entity(tableName = "task_attachments") @Entity(tableName = "task_attachments")
public final class TaskAttachment { public final class TaskAttachment {
@ -16,11 +22,6 @@ public final class TaskAttachment {
public static final Property.LongProperty ID = new Property.LongProperty(TABLE, "_id"); public static final Property.LongProperty ID = new Property.LongProperty(TABLE, "_id");
/** default directory for files on external storage */ /** default directory for files on external storage */
public static final String FILES_DIRECTORY_DEFAULT = "attachments"; // $NON-NLS-1$ public static final String FILES_DIRECTORY_DEFAULT = "attachments"; // $NON-NLS-1$
/** Constants for file types */
public static final String FILE_TYPE_AUDIO = "audio/"; // $NON-NLS-1$
public static final String FILE_TYPE_IMAGE = "image/"; // $NON-NLS-1$
public static final String FILE_TYPE_OTHER = "application/octet-stream"; // $NON-NLS-1$
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "_id") @ColumnInfo(name = "_id")
@ -37,18 +38,16 @@ public final class TaskAttachment {
private String name = ""; private String name = "";
@ColumnInfo(name = "path") @ColumnInfo(name = "path")
private String path = ""; private String uri = "";
@ColumnInfo(name = "content_type") @ColumnInfo(name = "content_type")
private String contentType = ""; private String contentType = "";
public static TaskAttachment createNewAttachment( public static TaskAttachment createNewAttachment(String taskUuid, Uri uri, String fileName) {
String taskUuid, String filePath, String fileName, String fileType) {
TaskAttachment attachment = new TaskAttachment(); TaskAttachment attachment = new TaskAttachment();
attachment.setTaskId(taskUuid); attachment.setTaskId(taskUuid);
attachment.setName(fileName); attachment.setName(fileName);
attachment.setPath(filePath); attachment.setUri(uri);
attachment.setContentType(fileType);
return attachment; return attachment;
} }
@ -84,12 +83,12 @@ public final class TaskAttachment {
this.name = name; this.name = name;
} }
public String getPath() { public String getUri() {
return path; return uri;
} }
public void setPath(String path) { public void setUri(String uri) {
this.path = path; this.uri = uri;
} }
public String getContentType() { public String getContentType() {
@ -99,4 +98,16 @@ public final class TaskAttachment {
public void setContentType(String contentType) { public void setContentType(String contentType) {
this.contentType = contentType; this.contentType = contentType;
} }
public void convertPathUri() {
setUri(Uri.fromFile(new File(uri)).toString());
}
public void setUri(Uri uri) {
setUri(uri == null ? null : uri.toString());
}
public Uri parseUri() {
return Strings.isNullOrEmpty(uri) ? null : Uri.parse(uri);
}
} }

@ -16,6 +16,9 @@ public abstract class TaskAttachmentDao {
@Query("SELECT * FROM task_attachments WHERE task_id = :taskUuid") @Query("SELECT * FROM task_attachments WHERE task_id = :taskUuid")
public abstract List<TaskAttachment> getAttachments(String taskUuid); public abstract List<TaskAttachment> getAttachments(String taskUuid);
@Query("SELECT * FROM task_attachments")
public abstract List<TaskAttachment> getAttachments();
@Delete @Delete
public abstract void delete(TaskAttachment taskAttachment); public abstract void delete(TaskAttachment taskAttachment);

@ -1,17 +1,22 @@
package org.tasks.data; package org.tasks.data;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import android.net.Uri; import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import com.google.common.base.Strings;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import java.io.File;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.tasks.backup.XmlReader; import org.tasks.backup.XmlReader;
import java.io.File;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import timber.log.Timber; import timber.log.Timber;
@Entity(tableName = "userActivity") @Entity(tableName = "userActivity")
@ -55,7 +60,10 @@ public class UserActivity implements Parcelable {
public UserActivity(XmlReader reader) { public UserActivity(XmlReader reader) {
reader.readString("remoteId", this::setRemoteId); reader.readString("remoteId", this::setRemoteId);
reader.readString("message", this::setMessage); reader.readString("message", this::setMessage);
reader.readString("picture", this::setPicture); reader.readString("picture", p -> {
setPicture(p);
convertPictureUri();
});
reader.readString("target_id", this::setTargetId); reader.readString("target_id", this::setTargetId);
reader.readLong("created_at", this::setCreated); reader.readLong("created_at", this::setCreated);
} }
@ -70,28 +78,6 @@ public class UserActivity implements Parcelable {
created = parcel.readLong(); created = parcel.readLong();
} }
private static Uri getPictureUri(String value) {
try {
if (value == null) {
return null;
}
if (value.contains("uri") || value.contains("path")) {
JSONObject json = new JSONObject(value);
if (json.has("uri")) {
return Uri.parse(json.getString("uri"));
}
if (json.has("path")) {
String path = json.getString("path");
return Uri.fromFile(new File(path));
}
}
return null;
} catch (JSONException e) {
Timber.e(e);
return null;
}
}
public Long getId() { public Long getId() {
return id; return id;
} }
@ -120,6 +106,10 @@ public class UserActivity implements Parcelable {
return picture; return picture;
} }
public void setPicture(Uri uri) {
picture = uri == null ? null : uri.toString();
}
public void setPicture(String picture) { public void setPicture(String picture) {
this.picture = picture; this.picture = picture;
} }
@ -141,7 +131,33 @@ public class UserActivity implements Parcelable {
} }
public Uri getPictureUri() { public Uri getPictureUri() {
return getPictureUri(picture); return Strings.isNullOrEmpty(picture) ? null : Uri.parse(picture);
}
public void convertPictureUri() {
setPicture(getLegacyPictureUri(picture));
}
private static Uri getLegacyPictureUri(String value) {
try {
if (Strings.isNullOrEmpty(value)) {
return null;
}
if (value.contains("uri") || value.contains("path")) {
JSONObject json = new JSONObject(value);
if (json.has("uri")) {
return Uri.parse(json.getString("uri"));
}
if (json.has("path")) {
String path = json.getString("path");
return Uri.fromFile(new File(path));
}
}
return null;
} catch (JSONException e) {
Timber.e(e);
return null;
}
} }
@Override @Override

@ -3,6 +3,8 @@ package org.tasks.data;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Insert; import androidx.room.Insert;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Update;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.helper.UUIDHelper; import com.todoroo.astrid.helper.UUIDHelper;
@ -14,9 +16,15 @@ public abstract class UserActivityDao {
@Insert @Insert
public abstract void insert(UserActivity userActivity); public abstract void insert(UserActivity userActivity);
@Update
public abstract void update(UserActivity userActivity);
@Query("SELECT * FROM userActivity WHERE target_id = :taskUuid ORDER BY created_at DESC ") @Query("SELECT * FROM userActivity WHERE target_id = :taskUuid ORDER BY created_at DESC ")
public abstract List<UserActivity> getCommentsForTask(String taskUuid); public abstract List<UserActivity> getCommentsForTask(String taskUuid);
@Query("SELECT * FROM userActivity")
public abstract List<UserActivity> getComments();
public void createNew(UserActivity item) { public void createNew(UserActivity item) {
if (item.getCreated() == null || item.getCreated() == 0L) { if (item.getCreated() == null || item.getCreated() == 0L) {
item.setCreated(DateUtilities.now()); item.setCreated(DateUtilities.now());

@ -1,26 +1,31 @@
package org.tasks.dialogs; package org.tasks.dialogs;
import static com.google.common.collect.Lists.newArrayList;
import static org.tasks.dialogs.RecordAudioDialog.newRecordAudioDialog;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.provider.MediaStore.Images.Media; import android.provider.MediaStore.Images.Media;
import androidx.annotation.NonNull;
import com.todoroo.astrid.files.FilesControlSet; import com.todoroo.astrid.files.FilesControlSet;
import java.util.List;
import javax.inject.Inject;
import org.tasks.R; import org.tasks.R;
import org.tasks.activities.CameraActivity; import org.tasks.activities.CameraActivity;
import org.tasks.files.FileExplore;
import org.tasks.injection.DialogFragmentComponent; import org.tasks.injection.DialogFragmentComponent;
import org.tasks.injection.ForActivity; import org.tasks.injection.ForActivity;
import org.tasks.injection.InjectingDialogFragment; import org.tasks.injection.InjectingDialogFragment;
import org.tasks.preferences.Device; import org.tasks.preferences.Device;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import java.util.List;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import static com.google.common.collect.Lists.newArrayList;
import static org.tasks.dialogs.RecordAudioDialog.newRecordAudioDialog;
import static org.tasks.files.FileHelper.newFilePickerIntent;
public class AddAttachmentDialog extends InjectingDialogFragment { public class AddAttachmentDialog extends InjectingDialogFragment {
private static final String FRAG_TAG_RECORD_AUDIO = "frag_tag_record_audio"; private static final String FRAG_TAG_RECORD_AUDIO = "frag_tag_record_audio";
@ -29,9 +34,6 @@ public class AddAttachmentDialog extends InjectingDialogFragment {
public static final int REQUEST_STORAGE = 12122; public static final int REQUEST_STORAGE = 12122;
public static final int REQUEST_AUDIO = 12123; public static final int REQUEST_AUDIO = 12123;
public static final String EXTRA_PATH = "extra_path";
public static final String EXTRA_TYPE = "extra_type";
@Inject @ForActivity Context context; @Inject @ForActivity Context context;
@Inject DialogBuilder dialogBuilder; @Inject DialogBuilder dialogBuilder;
@Inject Device device; @Inject Device device;
@ -88,7 +90,7 @@ public class AddAttachmentDialog extends InjectingDialogFragment {
} }
} }
public void pickFromStorage() { private void pickFromStorage() {
getTargetFragment().startActivityForResult(new Intent(context, FileExplore.class), REQUEST_STORAGE); getTargetFragment().startActivityForResult(newFilePickerIntent(getActivity(), null), REQUEST_STORAGE);
} }
} }

@ -2,13 +2,12 @@ package org.tasks.dialogs;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static org.tasks.PermissionUtil.verifyPermissions; import static org.tasks.PermissionUtil.verifyPermissions;
import static org.tasks.dialogs.AddAttachmentDialog.EXTRA_PATH;
import static org.tasks.dialogs.AddAttachmentDialog.EXTRA_TYPE;
import android.app.Dialog; import android.app.Dialog;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@ -22,7 +21,6 @@ import com.todoroo.astrid.files.FilesControlSet;
import com.todoroo.astrid.voice.AACRecorder; import com.todoroo.astrid.voice.AACRecorder;
import javax.inject.Inject; import javax.inject.Inject;
import org.tasks.R; import org.tasks.R;
import org.tasks.data.TaskAttachment;
import org.tasks.injection.DialogFragmentComponent; import org.tasks.injection.DialogFragmentComponent;
import org.tasks.injection.InjectingDialogFragment; import org.tasks.injection.InjectingDialogFragment;
import org.tasks.preferences.FragmentPermissionRequestor; import org.tasks.preferences.FragmentPermissionRequestor;
@ -31,6 +29,8 @@ import org.tasks.preferences.PermissionRequestor;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.themes.Theme; import org.tasks.themes.Theme;
import java.io.IOException;
public class RecordAudioDialog extends InjectingDialogFragment public class RecordAudioDialog extends InjectingDialogFragment
implements AACRecorder.AACRecorderCallbacks { implements AACRecorder.AACRecorderCallbacks {
@ -45,7 +45,7 @@ public class RecordAudioDialog extends InjectingDialogFragment
private AACRecorder recorder; private AACRecorder recorder;
public static RecordAudioDialog newRecordAudioDialog(FilesControlSet target, int requestCode) { static RecordAudioDialog newRecordAudioDialog(FilesControlSet target, int requestCode) {
RecordAudioDialog dialog = new RecordAudioDialog(); RecordAudioDialog dialog = new RecordAudioDialog();
dialog.setTargetFragment(target, requestCode); dialog.setTargetFragment(target, requestCode);
return dialog; return dialog;
@ -75,9 +75,13 @@ public class RecordAudioDialog extends InjectingDialogFragment
} }
private void startRecording() { private void startRecording() {
recorder.startRecording(); try {
timer.setBase(recorder.getBase()); recorder.startRecording(getContext());
timer.start(); timer.setBase(recorder.getBase());
timer.start();
} catch (IOException e) {
stopRecording();
}
} }
@Override @Override
@ -99,11 +103,9 @@ public class RecordAudioDialog extends InjectingDialogFragment
} }
@Override @Override
public void encodingFinished(String path) { public void encodingFinished(Uri uri) {
final String extension = path.substring(path.lastIndexOf('.') + 1);
Intent intent = new Intent(); Intent intent = new Intent();
intent.putExtra(EXTRA_PATH, path); intent.setData(uri);
intent.putExtra(EXTRA_TYPE, TaskAttachment.FILE_TYPE_AUDIO + extension);
Fragment target = getTargetFragment(); Fragment target = getTargetFragment();
if (target != null) { if (target != null) {
target.onActivityResult(getTargetRequestCode(), RESULT_OK, intent); target.onActivityResult(getTargetRequestCode(), RESULT_OK, intent);

@ -1,30 +1,23 @@
package org.tasks.files; package org.tasks.files;
import static org.tasks.PermissionUtil.verifyPermissions;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import androidx.annotation.NonNull;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.nononsenseapps.filepicker.FilePickerActivity; import com.nononsenseapps.filepicker.FilePickerActivity;
import java.io.File;
import javax.inject.Inject;
import org.tasks.injection.ActivityComponent; import org.tasks.injection.ActivityComponent;
import org.tasks.injection.InjectingAppCompatActivity; import org.tasks.injection.InjectingAppCompatActivity;
import org.tasks.preferences.ActivityPermissionRequestor;
import org.tasks.preferences.PermissionRequestor; import java.io.File;
public class FileExplore extends InjectingAppCompatActivity { public class FileExplore extends InjectingAppCompatActivity {
public static final String EXTRA_FILE = "extra_file"; // $NON-NLS-1$
public static final String EXTRA_DIRECTORY = "extra_directory"; // $NON-NLS-1$
public static final String EXTRA_START_PATH = "extra_start_path"; public static final String EXTRA_START_PATH = "extra_start_path";
public static final String EXTRA_DIRECTORY_MODE = "extra_directory_mode"; // $NON-NLS-1$ public static final String EXTRA_DIRECTORY_MODE = "extra_directory_mode"; // $NON-NLS-1$
private static final int REQUEST_PICKER = 1000; private static final int REQUEST_PICKER = 1000;
@Inject ActivityPermissionRequestor permissionRequestor;
private boolean directoryMode; private boolean directoryMode;
private String startPath; private String startPath;
@ -38,9 +31,7 @@ public class FileExplore extends InjectingAppCompatActivity {
directoryMode = intent.getBooleanExtra(EXTRA_DIRECTORY_MODE, false); directoryMode = intent.getBooleanExtra(EXTRA_DIRECTORY_MODE, false);
startPath = intent.getStringExtra(EXTRA_START_PATH); startPath = intent.getStringExtra(EXTRA_START_PATH);
if (permissionRequestor.requestFileWritePermission()) { launchPicker();
launchPicker();
}
} }
} }
@ -71,29 +62,12 @@ public class FileExplore extends InjectingAppCompatActivity {
startActivityForResult(i, REQUEST_PICKER); startActivityForResult(i, REQUEST_PICKER);
} }
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PermissionRequestor.REQUEST_FILE_WRITE) {
if (verifyPermissions(grantResults)) {
launchPicker();
} else {
finish();
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_PICKER) { if (requestCode == REQUEST_PICKER) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
Uri uri = data.getData();
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.setData(data.getData());
intent.setData(uri);
setResult(Activity.RESULT_OK, intent); setResult(Activity.RESULT_OK, intent);
} }
finish(); finish();

@ -1,38 +1,43 @@
package org.tasks.files; package org.tasks.files;
import android.app.Activity; import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; 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.OpenableColumns;
import android.webkit.MimeTypeMap;
import android.widget.Toast;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.todoroo.astrid.utility.Constants; import com.todoroo.astrid.utility.Constants;
import org.tasks.preferences.BasicPreferences; import org.tasks.R;
import org.tasks.preferences.Preferences;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List; import java.util.List;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import androidx.core.content.FileProvider;
import androidx.documentfile.provider.DocumentFile; import androidx.documentfile.provider.DocumentFile;
import timber.log.Timber;
import static androidx.core.content.FileProvider.getUriForFile;
import static com.google.common.collect.Iterables.any;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastKitKat; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastKitKat;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; 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) { public static Intent newFilePickerIntent(Activity activity, Uri initial, String... mimeTypes) {
if (atLeastKitKat()) { if (atLeastKitKat()) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE); intent.addCategory(Intent.CATEGORY_OPENABLE);
@ -47,7 +52,7 @@ public class FileHelper {
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
} }
} }
activity.startActivityForResult(intent, rc); return intent;
} else { } else {
Intent intent = new Intent(activity, FileExplore.class); Intent intent = new Intent(activity, FileExplore.class);
if (initial != null) { if (initial != null) {
@ -55,7 +60,7 @@ public class FileHelper {
FileExplore.EXTRA_START_PATH, FileExplore.EXTRA_START_PATH,
new File(initial.getPath())); new File(initial.getPath()));
} }
activity.startActivityForResult(intent, rc); return intent;
} }
} }
@ -65,7 +70,8 @@ public class FileHelper {
intent.addFlags( intent.addFlags(
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_READ_URI_PERMISSION); | Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
intent.putExtra("android.content.extra.SHOW_ADVANCED",true); intent.putExtra("android.content.extra.SHOW_ADVANCED",true);
activity.startActivityForResult(intent, rc); activity.startActivityForResult(intent, rc);
} else { } else {
@ -80,65 +86,78 @@ public class FileHelper {
} }
} }
public static InputStream fromUri(Context context, Uri uri) { public static void delete(Context context, Uri uri) {
try { if (uri == null) {
switch (uri.getScheme()) { return;
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) { switch (uri.getScheme()) {
String[] projection = {MediaStore.Images.Media.DATA}; case "content":
Cursor cursor = activity.managedQuery(uri, projection, null, null, null); DocumentFile documentFile = DocumentFile.fromSingleUri(context, uri);
documentFile.delete();
break;
case "file":
new File(uri.getPath()).delete();
break;
}
}
if (cursor != null) { public static String getFilename(Context context, Uri uri) {
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); switch (uri.getScheme()) {
cursor.moveToFirst(); case ContentResolver.SCHEME_FILE:
return cursor.getString(column_index); return uri.getLastPathSegment();
} else { case ContentResolver.SCHEME_CONTENT:
return uri.getPath(); Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
try {
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
} finally {
cursor.close();
}
}
break;
} }
return null;
} }
public static Intent getReadableActionView(Context context, String path, String type) { public static void startActionView(Activity context, Uri uri) {
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
String filename = getFilename(context, uri);
String extension = Files.getFileExtension(filename);
String mimeType = mimeTypeMap.getMimeTypeFromExtension(extension);
Intent intent = new Intent(Intent.ACTION_VIEW); Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
FileProvider.getUriForFile(context, Constants.FILE_PROVIDER_AUTHORITY, new File(path)); uri = copyToUri(context, Uri.fromFile(context.getCacheDir()), uri);
intent.setDataAndType(uri, type); }
grantReadPermissions(context, intent, uri); Uri share = getUriForFile(context, Constants.FILE_PROVIDER_AUTHORITY, new File(uri.getPath()));
return intent; intent.setDataAndType(share, mimeType);
grantReadPermissions(context, intent, share);
PackageManager packageManager = context.getPackageManager();
if (intent.resolveActivity(packageManager) != null) {
context.startActivity(intent);
} else {
Toast.makeText(context, R.string.no_application_found, Toast.LENGTH_SHORT).show();
}
} }
public static void grantReadPermissions(Context context, Intent intent, Uri uri) { private static void grantReadPermissions(Context context, Intent intent, Uri uri) {
if (atLeastLollipop()) { if (atLeastLollipop()) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else { } else {
if (atLeastLollipop()) { List<ResolveInfo> resolveInfoList =
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); context
} else { .getPackageManager()
List<ResolveInfo> resolveInfoList = .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
context for (ResolveInfo resolveInfo : resolveInfoList) {
.getPackageManager() context.grantUriPermission(
.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); resolveInfo.activityInfo.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
for (ResolveInfo resolveInfo : resolveInfoList) {
context.grantUriPermission(
resolveInfo.activityInfo.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
} }
} }
} }
public static Uri newFile(Context context, Uri destination, String mimeType, String filename) public static Uri newFile(Context context, Uri destination, String mimeType, String baseName, String extension)
throws IOException { throws IOException {
String filename = getNonCollidingFileName(context, destination, baseName, extension);
switch (destination.getScheme()) { switch (destination.getScheme()) {
case "content": case "content":
DocumentFile tree = DocumentFile.fromTreeUri(context, destination); DocumentFile tree = DocumentFile.fromTreeUri(context, destination);
@ -161,4 +180,56 @@ public class FileHelper {
throw new IllegalArgumentException("Unknown URI scheme: " + destination.getScheme()); throw new IllegalArgumentException("Unknown URI scheme: " + destination.getScheme());
} }
} }
public static Uri copyToUri(Context context, Uri destination, Uri input) {
ContentResolver contentResolver = context.getContentResolver();
MimeTypeMap mime = MimeTypeMap.getSingleton();
String filename = getFilename(context, input);
String baseName = Files.getNameWithoutExtension(filename);
String extension = Files.getFileExtension(filename);
String mimeType = mime.getMimeTypeFromExtension(extension);
try {
Uri output = newFile(context, destination, mimeType, baseName, extension);
InputStream inputStream = contentResolver.openInputStream(input);
OutputStream outputStream = contentResolver.openOutputStream(output);
ByteStreams.copy(inputStream, outputStream);
inputStream.close();
outputStream.close();
return output;
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private static String getNonCollidingFileName(Context context, Uri uri, String baseName, String extension) {
int tries = 1;
if (!extension.startsWith(".")) {
extension = "." + extension;
}
String tempName = baseName;
switch (uri.getScheme()) {
case ContentResolver.SCHEME_CONTENT:
DocumentFile dir = DocumentFile.fromTreeUri(context, uri);
List<DocumentFile> documentFiles = Arrays.asList(dir.listFiles());
while (true) {
String result = tempName + extension;
if (any(documentFiles, f -> f.getName().equals(result))) {
tempName = baseName + "-" + tries;
tries++;
} else {
break;
}
}
break;
case ContentResolver.SCHEME_FILE:
File f = new File(uri.getPath(), baseName + extension);
while (f.exists()) {
tempName = baseName + "-" + tries; // $NON-NLS-1$
f = new File(uri.getPath(), tempName + extension);
tries++;
}
break;
}
return tempName + extension;
}
} }

@ -1,7 +1,16 @@
package org.tasks.files; package org.tasks.files;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.net.Uri;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import timber.log.Timber;
public class ImageHelper { public class ImageHelper {
@ -27,17 +36,45 @@ public class ImageHelper {
return inSampleSize; return inSampleSize;
} }
public static Bitmap sampleBitmap(String path, int reqWidth, int reqHeight) { public static Bitmap sampleBitmap(Context context, Uri uri, int reqWidth, int reqHeight) {
ContentResolver contentResolver = context.getContentResolver();
InputStream inputStream;
try {
inputStream = contentResolver.openInputStream(uri);
} catch (FileNotFoundException e) {
Timber.e(e);
return null;
}
// First decode with inJustDecodeBounds=true to check dimensions // First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options(); final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options); BitmapFactory.decodeStream(inputStream, null, options);
// Calculate inSampleSize // Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set // Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false; options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(path, options);
try {
inputStream.close();
} catch (IOException e) {
Timber.e(e);
}
try {
inputStream = contentResolver.openInputStream(uri);
return BitmapFactory.decodeStream(inputStream, null, options);
} catch (IOException e) {
Timber.e(e);
return null;
} finally {
try {
inputStream.close();
} catch (IOException e) {
Timber.e(e);
}
}
} }
} }

@ -1,8 +1,5 @@
package org.tasks.fragments; package org.tasks.fragments;
import static org.tasks.files.FileHelper.getPathFromUri;
import static org.tasks.files.ImageHelper.sampleBitmap;
import android.app.Activity; import android.app.Activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
@ -10,9 +7,6 @@ import android.graphics.Bitmap;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.KeyEvent; import android.view.KeyEvent;
@ -23,19 +17,11 @@ import android.view.inputmethod.EditorInfo;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.OnEditorAction;
import butterknife.OnTextChanged;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.json.JSONException;
import org.json.JSONObject;
import org.tasks.R; import org.tasks.R;
import org.tasks.activities.CameraActivity; import org.tasks.activities.CameraActivity;
import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.DialogBuilder;
@ -43,7 +29,22 @@ import org.tasks.injection.FragmentComponent;
import org.tasks.preferences.Device; import org.tasks.preferences.Device;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.ui.TaskEditControlFragment; import org.tasks.ui.TaskEditControlFragment;
import timber.log.Timber;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.OnEditorAction;
import butterknife.OnTextChanged;
import static org.tasks.files.ImageHelper.sampleBitmap;
public class CommentBarFragment extends TaskEditControlFragment { public class CommentBarFragment extends TaskEditControlFragment {
@ -71,17 +72,6 @@ public class CommentBarFragment extends TaskEditControlFragment {
private CommentBarFragmentCallback callback; private CommentBarFragmentCallback callback;
private Uri pendingCommentPicture = null; private Uri pendingCommentPicture = null;
private static JSONObject savePictureJson(final Uri uri) {
try {
JSONObject json = new JSONObject();
json.put("uri", uri.toString());
return json;
} catch (JSONException e) {
Timber.e(e);
}
return null;
}
@Override @Override
public void onAttach(Activity activity) { public void onAttach(Activity activity) {
super.onAttach(activity); super.onAttach(activity);
@ -191,7 +181,7 @@ public class CommentBarFragment extends TaskEditControlFragment {
public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_CAMERA) { if (requestCode == REQUEST_CODE_CAMERA) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
pendingCommentPicture = data.getParcelableExtra(CameraActivity.EXTRA_URI); pendingCommentPicture = data.getData();
setPictureButtonToPendingPicture(); setPictureButtonToPendingPicture();
commentField.requestFocus(); commentField.requestFocus();
} }
@ -206,10 +196,12 @@ public class CommentBarFragment extends TaskEditControlFragment {
} }
private void setPictureButtonToPendingPicture() { private void setPictureButtonToPendingPicture() {
String path = getPathFromUri(activity, pendingCommentPicture);
Bitmap bitmap = Bitmap bitmap =
sampleBitmap( sampleBitmap(
path, pictureButton.getLayoutParams().width, pictureButton.getLayoutParams().height); activity,
pendingCommentPicture,
pictureButton.getLayoutParams().width,
pictureButton.getLayoutParams().height);
pictureButton.setImageBitmap(bitmap); pictureButton.setImageBitmap(bitmap);
commentButton.setVisibility(View.VISIBLE); commentButton.setVisibility(View.VISIBLE);
} }
@ -219,13 +211,7 @@ public class CommentBarFragment extends TaskEditControlFragment {
if (TextUtils.isEmpty(message)) { if (TextUtils.isEmpty(message)) {
message = " "; message = " ";
} }
String picture = null; Uri picture = pendingCommentPicture;
if (pendingCommentPicture != null) {
JSONObject pictureJson = savePictureJson(pendingCommentPicture);
if (pictureJson != null) {
picture = pictureJson.toString();
}
}
if (commentField != null) { if (commentField != null) {
commentField.setText(""); // $NON-NLS-1$ commentField.setText(""); // $NON-NLS-1$
@ -281,7 +267,7 @@ public class CommentBarFragment extends TaskEditControlFragment {
public interface CommentBarFragmentCallback { public interface CommentBarFragmentCallback {
void addComment(String message, String picture); void addComment(String message, Uri picture);
} }
interface ClearImageCallback { interface ClearImageCallback {

@ -42,6 +42,7 @@ import static com.todoroo.andlib.utility.AndroidUtilities.atLeastKitKat;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop;
import static org.tasks.dialogs.ExportTasksDialog.newExportTasksDialog; import static org.tasks.dialogs.ExportTasksDialog.newExportTasksDialog;
import static org.tasks.dialogs.ImportTasksDialog.newImportTasksDialog; import static org.tasks.dialogs.ImportTasksDialog.newImportTasksDialog;
import static org.tasks.files.FileHelper.newFilePickerIntent;
import static org.tasks.locale.LocalePickerDialog.newLocalePickerDialog; import static org.tasks.locale.LocalePickerDialog.newLocalePickerDialog;
import static org.tasks.themes.ThemeColor.LAUNCHERS; import static org.tasks.themes.ThemeColor.LAUNCHERS;
@ -154,7 +155,7 @@ public class BasicPreferences extends InjectingPreferenceActivity
findPreference(R.string.backup_BAc_import) findPreference(R.string.backup_BAc_import)
.setOnPreferenceClickListener( .setOnPreferenceClickListener(
preference -> { preference -> {
FileHelper.newFilePicker(BasicPreferences.this, REQUEST_PICKER, preferences.getBackupDirectory()); startActivityForResult(newFilePickerIntent(BasicPreferences.this, preferences.getBackupDirectory()), REQUEST_PICKER);
return false; return false;
}); });
@ -235,14 +236,14 @@ 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) {
Uri dir = data.getData(); Uri uri = data.getData();
if (atLeastLollipop()) { if (atLeastLollipop()) {
getContentResolver() getContentResolver()
.takePersistableUriPermission( .takePersistableUriPermission(
dir, uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} }
preferences.setString(R.string.p_backup_dir, dir.toString()); preferences.setUri(R.string.p_backup_dir, uri);
updateBackupDirectory(); updateBackupDirectory();
} }
} else if (requestCode == REQUEST_PICKER) { } else if (requestCode == REQUEST_PICKER) {

@ -1,8 +1,10 @@
package org.tasks.preferences; package org.tasks.preferences;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop;
import static org.tasks.PermissionUtil.verifyPermissions; import static org.tasks.PermissionUtil.verifyPermissions;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.CheckBoxPreference; import android.preference.CheckBoxPreference;
import android.speech.tts.TextToSpeech; import android.speech.tts.TextToSpeech;
@ -12,6 +14,7 @@ import java.io.File;
import javax.inject.Inject; import javax.inject.Inject;
import org.tasks.R; import org.tasks.R;
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.scheduling.CalendarNotificationIntentService; import org.tasks.scheduling.CalendarNotificationIntentService;
@ -45,31 +48,37 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity {
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_FILES_DIR && resultCode == RESULT_OK) { if (requestCode == REQUEST_CODE_FILES_DIR) {
if (data != null) { if (resultCode == RESULT_OK) {
String dir = data.getStringExtra(FileExplore.EXTRA_DIRECTORY); Uri uri = data.getData();
preferences.setString(R.string.p_attachment_dir, dir); if (atLeastLollipop()) {
getContentResolver()
.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
preferences.setUri(R.string.p_attachment_dir, uri);
updateAttachmentDirectory(); updateAttachmentDirectory();
} }
return; } else {
} try {
try { if (requestCode == REQUEST_CODE_TTS_CHECK) {
if (requestCode == REQUEST_CODE_TTS_CHECK) { if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) {
if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) { // success, create the TTS instance
// success, create the TTS instance voiceOutputAssistant.initTTS();
voiceOutputAssistant.initTTS(); } else {
} else { // missing data, install it
// missing data, install it Intent installIntent = new Intent();
Intent installIntent = new Intent(); installIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
installIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA); startActivity(installIntent);
startActivity(installIntent); }
} }
} catch (VerifyError e) {
// unavailable
Timber.e(e);
} }
} catch (VerifyError e) { super.onActivityResult(requestCode, resultCode, data);
// unavailable
Timber.e(e);
} }
super.onActivityResult(requestCode, resultCode, data);
} }
@Override @Override
@ -83,10 +92,8 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity {
findPreference(getString(R.string.p_attachment_dir)) findPreference(getString(R.string.p_attachment_dir))
.setOnPreferenceClickListener( .setOnPreferenceClickListener(
p -> { p -> {
Intent filesDir = new Intent(MiscellaneousPreferences.this, FileExplore.class); FileHelper.newDirectoryPicker(this, REQUEST_CODE_FILES_DIR, preferences.getAttachmentsDirectory());
filesDir.putExtra(FileExplore.EXTRA_DIRECTORY_MODE, true); return false;
startActivityForResult(filesDir, REQUEST_CODE_FILES_DIR);
return true;
}); });
updateAttachmentDirectory(); updateAttachmentDirectory();
} }
@ -96,8 +103,10 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity {
} }
private String getAttachmentDirectory() { private String getAttachmentDirectory() {
File dir = preferences.getAttachmentsDirectory(); Uri uri = preferences.getAttachmentsDirectory();
return dir == null ? "" : dir.getAbsolutePath(); return uri.getScheme().equals("file")
? new File(uri.getPath()).getAbsolutePath()
: uri.toString();
} }
private void initializeCalendarReminderPreference() { private void initializeCalendarReminderPreference() {

@ -27,10 +27,6 @@ public class PermissionChecker {
asList(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)); asList(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR));
} }
public boolean canWriteToExternalStorage() {
return checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
public boolean canAccessAccounts() { public boolean canAccessAccounts() {
return atLeastOreo() || checkPermission(Manifest.permission.GET_ACCOUNTS); return atLeastOreo() || checkPermission(Manifest.permission.GET_ACCOUNTS);
} }

@ -4,7 +4,6 @@ import android.Manifest;
public abstract class PermissionRequestor { public abstract class PermissionRequestor {
public static final int REQUEST_FILE_WRITE = 50;
public static final int REQUEST_CALENDAR = 51; public static final int REQUEST_CALENDAR = 51;
public static final int REQUEST_MIC = 52; public static final int REQUEST_MIC = 52;
public static final int REQUEST_GOOGLE_ACCOUNTS = 53; public static final int REQUEST_GOOGLE_ACCOUNTS = 53;
@ -24,14 +23,6 @@ public abstract class PermissionRequestor {
return false; return false;
} }
public boolean requestFileWritePermission() {
if (permissionChecker.canWriteToExternalStorage()) {
return true;
}
requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, REQUEST_FILE_WRITE);
return false;
}
public boolean requestCalendarPermissions() { public boolean requestCalendarPermissions() {
return requestCalendarPermissions(REQUEST_CALENDAR); return requestCalendarPermissions(REQUEST_CALENDAR);
} }

@ -28,7 +28,6 @@ import org.tasks.time.DateTime;
import java.io.File; import java.io.File;
import java.util.Collection; import java.util.Collection;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject; import javax.inject.Inject;
@ -49,31 +48,17 @@ public class Preferences {
private static final String PREF_SORT_SORT = "sort_sort"; // $NON-NLS-1$ private static final String PREF_SORT_SORT = "sort_sort"; // $NON-NLS-1$
private final Context context; private final Context context;
private final PermissionChecker permissionChecker;
private final SharedPreferences prefs; private final SharedPreferences prefs;
private final SharedPreferences publicPrefs; private final SharedPreferences publicPrefs;
@Inject @Inject
public Preferences(@ForApplication Context context, PermissionChecker permissionChecker) { public Preferences(@ForApplication Context context) {
this.context = context; this.context = context;
this.permissionChecker = permissionChecker;
prefs = PreferenceManager.getDefaultSharedPreferences(context); prefs = PreferenceManager.getDefaultSharedPreferences(context);
publicPrefs = publicPrefs =
context.getSharedPreferences(AstridApiConstants.PUBLIC_PREFS, Context.MODE_PRIVATE); context.getSharedPreferences(AstridApiConstants.PUBLIC_PREFS, Context.MODE_PRIVATE);
} }
private static String getNonCollidingFileName(String dir, String baseName, String extension) {
int tries = 1;
File f = new File(dir + File.separator + baseName + extension);
String tempName = baseName;
while (f.exists()) {
tempName = baseName + "-" + tries; // $NON-NLS-1$
f = new File(dir + File.separator + tempName + extension);
tries++;
}
return tempName + extension;
}
public boolean backButtonSavesTask() { public boolean backButtonSavesTask() {
return getBoolean(R.string.p_back_button_saves_task, false); return getBoolean(R.string.p_back_button_saves_task, false);
} }
@ -389,18 +374,34 @@ public class Preferences {
} }
} }
public File getAttachmentsDirectory() { public Uri getAttachmentsDirectory() {
File directory = null; Uri uri = getUri(R.string.p_attachment_dir);
String customDir = getStringValue(R.string.p_attachment_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(TaskAttachment.FILES_DIRECTORY_DEFAULT); return DocumentFile.fromFile(context.getExternalFilesDir(null))
.createDirectory(TaskAttachment.FILES_DIRECTORY_DEFAULT)
.getUri();
} else {
return Uri.fromFile(getDefaultFileLocation(TaskAttachment.FILES_DIRECTORY_DEFAULT));
} }
return directory;
} }
private File getDefaultFileLocation(String type) { private File getDefaultFileLocation(String type) {
@ -413,20 +414,12 @@ public class Preferences {
return file.isDirectory() || file.mkdirs() ? file : null; return file.isDirectory() || file.mkdirs() ? file : null;
} }
public String getNewAudioAttachmentPath(AtomicReference<String> nameReference) { public Uri getCacheDirectory() {
return getNewAttachmentPath(".m4a", nameReference); // $NON-NLS-1$ if (atLeastKitKat()) {
} return DocumentFile.fromFile(context.getCacheDir()).getUri();
} else {
public String getNewAttachmentPath(String extension, AtomicReference<String> nameReference) { return Uri.fromFile(context.getCacheDir());
String dir = getAttachmentsDirectory().getAbsolutePath();
String name = getNonCollidingFileName(dir, new DateTime().toString("yyyyMMddHHmm"), extension);
if (nameReference != null) {
nameReference.set(name);
} }
return dir + File.separator + name;
} }
public Uri getBackupDirectory() { public Uri getBackupDirectory() {

@ -168,8 +168,6 @@
<string name="premium_remove_file_confirm">Сигурни ли сте? Не може да бъде отменено</string> <string name="premium_remove_file_confirm">Сигурни ли сте? Не може да бъде отменено</string>
<string name="audio_recording_title">Записване на Аудио</string> <string name="audio_recording_title">Записване на Аудио</string>
<string name="audio_stop_recording">Спри Записването</string> <string name="audio_stop_recording">Спри Записването</string>
<string name="file_type_unhandled">Съжаляваме! Не е намерено приложение, което да работи с този тип файл.</string>
<string name="file_err_copy">Грешка при копиране на файла за прикачване</string>
<string name="ring_once">Звънене веднъж</string> <string name="ring_once">Звънене веднъж</string>
<string name="ring_five_times">Звънене пет пъти</string> <string name="ring_five_times">Звънене пет пъти</string>
<string name="ring_nonstop">Звънене без спиране</string> <string name="ring_nonstop">Звънене без спиране</string>
@ -289,7 +287,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks ще изтоваря имената на задачите по време на напомняния за задача</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks ще изтоваря имената на задачите по време на напомняния за задача</string>
<string name="delete_task">Изтрий задача</string> <string name="delete_task">Изтрий задача</string>
<string name="voice_command_added_task">Добавена задача</string> <string name="voice_command_added_task">Добавена задача</string>
<string name="external_storage_unavailable">Не може да достъпвате до външната памет</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 задача</item> <item quantity="one">1 задача</item>
<item quantity="other">%d задачи</item> <item quantity="other">%d задачи</item>

@ -144,8 +144,6 @@
<string name="premium_remove_file_confirm">Skutečně? Nebude cesty zpět</string> <string name="premium_remove_file_confirm">Skutečně? Nebude cesty zpět</string>
<string name="audio_recording_title">Nahrávám zvuk</string> <string name="audio_recording_title">Nahrávám zvuk</string>
<string name="audio_stop_recording">Ukončit záznam</string> <string name="audio_stop_recording">Ukončit záznam</string>
<string name="file_type_unhandled">Pro tento typ souborů nebyla nalezena žádná aplikace.</string>
<string name="file_err_copy">Chyba při kopírování souboru jako přílohy</string>
<string name="random_reminder_hour">hodina</string> <string name="random_reminder_hour">hodina</string>
<string name="random_reminder_day">den</string> <string name="random_reminder_day">den</string>
<string name="random_reminder_week">týden</string> <string name="random_reminder_week">týden</string>
@ -182,7 +180,6 @@
<string name="EPr_voiceRemindersEnabled_title">Hlasové upomínky</string> <string name="EPr_voiceRemindersEnabled_title">Hlasové upomínky</string>
<string name="delete_task">Smazat úkol</string> <string name="delete_task">Smazat úkol</string>
<string name="voice_command_added_task">Přidán úkol</string> <string name="voice_command_added_task">Přidán úkol</string>
<string name="external_storage_unavailable">Není přístup k externí databázi</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 úkol</item> <item quantity="one">1 úkol</item>
<item quantity="other">%d úkolů</item> <item quantity="other">%d úkolů</item>

@ -163,8 +163,6 @@
<string name="premium_remove_file_confirm">Sind Sie sicher? Das kann nicht rückgängig gemacht werden</string> <string name="premium_remove_file_confirm">Sind Sie sicher? Das kann nicht rückgängig gemacht werden</string>
<string name="audio_recording_title">Audio aufnehmen</string> <string name="audio_recording_title">Audio aufnehmen</string>
<string name="audio_stop_recording">Aufnahme stoppen</string> <string name="audio_stop_recording">Aufnahme stoppen</string>
<string name="file_type_unhandled">Tut mir leid! Dieser Dateityp kann nicht geöffnet werden.</string>
<string name="file_err_copy">Fehler beim Kopieren der anzuhängenden Datei</string>
<string name="ring_once">Einmal klingeln</string> <string name="ring_once">Einmal klingeln</string>
<string name="ring_five_times">Fünfmal klingeln</string> <string name="ring_five_times">Fünfmal klingeln</string>
<string name="ring_nonstop">Ununterbrochen klingeln</string> <string name="ring_nonstop">Ununterbrochen klingeln</string>
@ -283,7 +281,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks wird Aufgabennamen bei der Erinnerung aussprechen</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks wird Aufgabennamen bei der Erinnerung aussprechen</string>
<string name="delete_task">Aufgabe löschen</string> <string name="delete_task">Aufgabe löschen</string>
<string name="voice_command_added_task">Hinzugefügte Aufgabe</string> <string name="voice_command_added_task">Hinzugefügte Aufgabe</string>
<string name="external_storage_unavailable">Kein Zugriff auf externen Speicher</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 Aufgabe</item> <item quantity="one">1 Aufgabe</item>
<item quantity="other">%d Aufgaben</item> <item quantity="other">%d Aufgaben</item>

@ -128,8 +128,6 @@
<string name="premium_remove_file_confirm">Είστε σίγουρος; Δεν μπορεί να ακυρωθεί</string> <string name="premium_remove_file_confirm">Είστε σίγουρος; Δεν μπορεί να ακυρωθεί</string>
<string name="audio_recording_title">Εγγραφή Ήχου</string> <string name="audio_recording_title">Εγγραφή Ήχου</string>
<string name="audio_stop_recording">Σταμάτημα εγγραφής</string> <string name="audio_stop_recording">Σταμάτημα εγγραφής</string>
<string name="file_type_unhandled">Συγγνώμη! Δεν βρέθηκε εφαρμογή που να χειρίζεται τέτοιο τύπο αρχείου</string>
<string name="file_err_copy">Σφάλμα αντιγραφής αρχείου προς επισύναψη</string>
<string name="random_reminder_hour">μια ώρα</string> <string name="random_reminder_hour">μια ώρα</string>
<string name="random_reminder_day">μια μέρα</string> <string name="random_reminder_day">μια μέρα</string>
<string name="random_reminder_week">μια εβδομάδα</string> <string name="random_reminder_week">μια εβδομάδα</string>
@ -165,7 +163,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Η εφαρμογή θα λέει να ονόματα των εργασιών κατά την διάρκεια των υπενθυμίσεων</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Η εφαρμογή θα λέει να ονόματα των εργασιών κατά την διάρκεια των υπενθυμίσεων</string>
<string name="delete_task">Διαγραφή καθήκοντος</string> <string name="delete_task">Διαγραφή καθήκοντος</string>
<string name="voice_command_added_task">Η εργασία προστέθηκε</string> <string name="voice_command_added_task">Η εργασία προστέθηκε</string>
<string name="external_storage_unavailable">Δεν είναι δυνατή η πρόσβαση σε εξωτερικά μέσα αποθήκευσης</string>
<string name="today">Σήμερα</string> <string name="today">Σήμερα</string>
<string name="tomorrow">Αύριο</string> <string name="tomorrow">Αύριο</string>
<string name="yesterday">Χθές</string> <string name="yesterday">Χθές</string>

@ -164,8 +164,6 @@
<string name="premium_remove_file_confirm">Está seguro? No se puede deshacer</string> <string name="premium_remove_file_confirm">Está seguro? No se puede deshacer</string>
<string name="audio_recording_title">Grabando Audio</string> <string name="audio_recording_title">Grabando Audio</string>
<string name="audio_stop_recording">Detener grabación</string> <string name="audio_stop_recording">Detener grabación</string>
<string name="file_type_unhandled">Lo sentimos! No se encontró ninguna aplicación para abrir este tipo de archivo.</string>
<string name="file_err_copy">Error al copiar el archivo a adjuntar</string>
<string name="ring_once">Sonar una vez</string> <string name="ring_once">Sonar una vez</string>
<string name="ring_five_times">Sonar cinco veces</string> <string name="ring_five_times">Sonar cinco veces</string>
<string name="ring_nonstop">Sonar sin parar</string> <string name="ring_nonstop">Sonar sin parar</string>
@ -285,7 +283,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks dirá los nombres de las tareas durante los avisos</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks dirá los nombres de las tareas durante los avisos</string>
<string name="delete_task">Eliminar tarea</string> <string name="delete_task">Eliminar tarea</string>
<string name="voice_command_added_task">Tarea agregada</string> <string name="voice_command_added_task">Tarea agregada</string>
<string name="external_storage_unavailable">Almacenamiento externo inaccesible</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 tarea</item> <item quantity="one">1 tarea</item>
<item quantity="other">%d tareas</item> <item quantity="other">%d tareas</item>

@ -113,8 +113,6 @@
<string name="premium_remove_file_confirm">آیا مطمن هستید؟ قادر به برگرداندن نخواهید بود</string> <string name="premium_remove_file_confirm">آیا مطمن هستید؟ قادر به برگرداندن نخواهید بود</string>
<string name="audio_recording_title">ضبط صدا</string> <string name="audio_recording_title">ضبط صدا</string>
<string name="audio_stop_recording">توقف ضبط</string> <string name="audio_stop_recording">توقف ضبط</string>
<string name="file_type_unhandled">پوزش! هیچ برنامه ای برای بازکردن این فایل پیدا نشد.</string>
<string name="file_err_copy">خطای کپی فایل برای ضمیمه</string>
<string name="ring_once">یک بار زنگ بزن</string> <string name="ring_once">یک بار زنگ بزن</string>
<string name="ring_five_times">پنج بار زنگ بزن</string> <string name="ring_five_times">پنج بار زنگ بزن</string>
<string name="ring_nonstop">بدون توقف زنگ بزن</string> <string name="ring_nonstop">بدون توقف زنگ بزن</string>
@ -177,7 +175,6 @@
<string name="EPr_voiceRemindersEnabled_title">یادآور صوتی</string> <string name="EPr_voiceRemindersEnabled_title">یادآور صوتی</string>
<string name="delete_task">حذف وظیفه</string> <string name="delete_task">حذف وظیفه</string>
<string name="voice_command_added_task">وظیفه اضافه شده</string> <string name="voice_command_added_task">وظیفه اضافه شده</string>
<string name="external_storage_unavailable">عدم امکان دسترسی به حافظه خارجی</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">۱ وظیفه</item> <item quantity="one">۱ وظیفه</item>
<item quantity="other">%d tasks</item> <item quantity="other">%d tasks</item>

@ -156,8 +156,6 @@
<string name="premium_remove_file_confirm">Oletko varma? Ei voi peruuttaa</string> <string name="premium_remove_file_confirm">Oletko varma? Ei voi peruuttaa</string>
<string name="audio_recording_title">Tallentaa ääntä</string> <string name="audio_recording_title">Tallentaa ääntä</string>
<string name="audio_stop_recording">Lopeta tallennus</string> <string name="audio_stop_recording">Lopeta tallennus</string>
<string name="file_type_unhandled">Pahoittelen! Ei sopivaa sovelllusta tämän tiedostotyypin käsittelyyn.</string>
<string name="file_err_copy">Virhe tiedoston kopioimisessa liitteeseen</string>
<string name="ring_once">Soi kerran</string> <string name="ring_once">Soi kerran</string>
<string name="ring_five_times">Soi viisi kertaa</string> <string name="ring_five_times">Soi viisi kertaa</string>
<string name="ring_nonstop">Soi jatkuvasti</string> <string name="ring_nonstop">Soi jatkuvasti</string>
@ -276,7 +274,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks kertoo tehtävien nimet muistutusten aikana</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks kertoo tehtävien nimet muistutusten aikana</string>
<string name="delete_task">Poista tehtävä</string> <string name="delete_task">Poista tehtävä</string>
<string name="voice_command_added_task">Lisätty tehtävä</string> <string name="voice_command_added_task">Lisätty tehtävä</string>
<string name="external_storage_unavailable">Eii pääsyä ulkoiseen muistiin</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 tehtävä</item> <item quantity="one">1 tehtävä</item>
<item quantity="other">%d tehtävät</item> <item quantity="other">%d tehtävät</item>

@ -162,8 +162,6 @@
<string name="premium_remove_file_confirm">Êtes-vous certain(e)? Cette opération est irréversible</string> <string name="premium_remove_file_confirm">Êtes-vous certain(e)? Cette opération est irréversible</string>
<string name="audio_recording_title">Enregistrement Audio</string> <string name="audio_recording_title">Enregistrement Audio</string>
<string name="audio_stop_recording">Arrêter l\'enregistrement</string> <string name="audio_stop_recording">Arrêter l\'enregistrement</string>
<string name="file_type_unhandled">Désolé ! Aucune application n\'a été trouvé pour gérer ce type de fichier.</string>
<string name="file_err_copy">Erreur lors de la copie du fichier à joindre</string>
<string name="ring_once">Sonner une fois</string> <string name="ring_once">Sonner une fois</string>
<string name="ring_five_times">Sonner cinq fois</string> <string name="ring_five_times">Sonner cinq fois</string>
<string name="ring_nonstop">Sonner en continu</string> <string name="ring_nonstop">Sonner en continu</string>
@ -275,7 +273,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks donnera le nom de la tâche</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks donnera le nom de la tâche</string>
<string name="delete_task">Supprimer la tâche ? </string> <string name="delete_task">Supprimer la tâche ? </string>
<string name="voice_command_added_task">Tâche ajoutée</string> <string name="voice_command_added_task">Tâche ajoutée</string>
<string name="external_storage_unavailable">Impossible d\'accéder au stockage externe</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 tâche</item> <item quantity="one">1 tâche</item>
<item quantity="other">%d tâches</item> <item quantity="other">%d tâches</item>

@ -152,8 +152,6 @@
<string name="premium_remove_file_confirm">Está seguro? No se puede deshacer</string> <string name="premium_remove_file_confirm">Está seguro? No se puede deshacer</string>
<string name="audio_recording_title">Grabando Audio</string> <string name="audio_recording_title">Grabando Audio</string>
<string name="audio_stop_recording">Detener grabación</string> <string name="audio_stop_recording">Detener grabación</string>
<string name="file_type_unhandled">Lo sentimos! No se encontró ninguna aplicación para abrir este tipo de archivo.</string>
<string name="file_err_copy">Error al copiar el archivo a adjuntar</string>
<string name="ring_once">Sonar una vez</string> <string name="ring_once">Sonar una vez</string>
<string name="ring_five_times">Sonar cinco veces</string> <string name="ring_five_times">Sonar cinco veces</string>
<string name="ring_nonstop">Sonar sin parar</string> <string name="ring_nonstop">Sonar sin parar</string>
@ -198,7 +196,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks dirá los nombres de las tareas durante los avisos</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks dirá los nombres de las tareas durante los avisos</string>
<string name="delete_task">Eliminar tarefa</string> <string name="delete_task">Eliminar tarefa</string>
<string name="voice_command_added_task">Tarea agregada</string> <string name="voice_command_added_task">Tarea agregada</string>
<string name="external_storage_unavailable">Almacenamiento externo inaccesible</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 tarea</item> <item quantity="one">1 tarea</item>
<item quantity="other">%d tareas</item> <item quantity="other">%d tareas</item>

@ -168,8 +168,6 @@
<string name="premium_remove_file_confirm">Biztos benne? A művelet nem visszavonható</string> <string name="premium_remove_file_confirm">Biztos benne? A művelet nem visszavonható</string>
<string name="audio_recording_title">Hang rögzítése</string> <string name="audio_recording_title">Hang rögzítése</string>
<string name="audio_stop_recording">Hangrögzítés leállítása</string> <string name="audio_stop_recording">Hangrögzítés leállítása</string>
<string name="file_type_unhandled">Sajnáljuk, az adott file megnyitására képes alkalmazás nem található.</string>
<string name="file_err_copy">Hiba lépett fel a file csatolásakor</string>
<string name="ring_once">Egy csengés</string> <string name="ring_once">Egy csengés</string>
<string name="ring_five_times">Öt csengés</string> <string name="ring_five_times">Öt csengés</string>
<string name="ring_nonstop">Folyamatos csengés</string> <string name="ring_nonstop">Folyamatos csengés</string>
@ -289,7 +287,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">A Tasks kimondja a feladatcímeket emlékeztetőkor</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">A Tasks kimondja a feladatcímeket emlékeztetőkor</string>
<string name="delete_task">Feladat törlése</string> <string name="delete_task">Feladat törlése</string>
<string name="voice_command_added_task">Hozzáadott feladat</string> <string name="voice_command_added_task">Hozzáadott feladat</string>
<string name="external_storage_unavailable">Külső tárhely nem elérhető</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 feladat</item> <item quantity="one">1 feladat</item>
<item quantity="other">%d feladat</item> <item quantity="other">%d feladat</item>

@ -166,8 +166,6 @@
<string name="premium_remove_file_confirm">Se sicuro? Non può essere eseguito</string> <string name="premium_remove_file_confirm">Se sicuro? Non può essere eseguito</string>
<string name="audio_recording_title">Registrazione Audio</string> <string name="audio_recording_title">Registrazione Audio</string>
<string name="audio_stop_recording">Fine Registrazione</string> <string name="audio_stop_recording">Fine Registrazione</string>
<string name="file_type_unhandled">Spiacente! Non è stata trovata nessuna applicazione per gestire questo tipo di file</string>
<string name="file_err_copy">Errore di copia dei file da allegare</string>
<string name="ring_once">Suona una volta</string> <string name="ring_once">Suona una volta</string>
<string name="ring_five_times">Suona cinque volte</string> <string name="ring_five_times">Suona cinque volte</string>
<string name="ring_nonstop">Suono continuo</string> <string name="ring_nonstop">Suono continuo</string>
@ -286,7 +284,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks pronuncerà il nome dell\'attività durante i promemoria</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks pronuncerà il nome dell\'attività durante i promemoria</string>
<string name="delete_task">Elimina attività</string> <string name="delete_task">Elimina attività</string>
<string name="voice_command_added_task">Attività aggiunta</string> <string name="voice_command_added_task">Attività aggiunta</string>
<string name="external_storage_unavailable">Non posso accedere alla memoria esterna</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 attività</item> <item quantity="one">1 attività</item>
<item quantity="other">%d attività</item> <item quantity="other">%d attività</item>

@ -160,8 +160,6 @@
<string name="premium_remove_file_confirm">בטוח? לא ניתן לבטל את הפעולה</string> <string name="premium_remove_file_confirm">בטוח? לא ניתן לבטל את הפעולה</string>
<string name="audio_recording_title">מקליטה שֵׁמַע</string> <string name="audio_recording_title">מקליטה שֵׁמַע</string>
<string name="audio_stop_recording">הפסק הקלטה</string> <string name="audio_stop_recording">הפסק הקלטה</string>
<string name="file_type_unhandled">\"מצטערת! לא מצאתי ישום שיכול לטפל בקבצים מסוג זה\"</string>
<string name="file_err_copy">שגיאה בהעתקת הקובץ המצורף</string>
<string name="ring_once">צלצל פעם אחת</string> <string name="ring_once">צלצל פעם אחת</string>
<string name="ring_five_times">צלצל חמש פעמים</string> <string name="ring_five_times">צלצל חמש פעמים</string>
<string name="ring_nonstop">צלצל ללא הפסקה</string> <string name="ring_nonstop">צלצל ללא הפסקה</string>
@ -281,7 +279,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">אסטריד תאמר את שם המשימה כחלק מהתזכורת</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">אסטריד תאמר את שם המשימה כחלק מהתזכורת</string>
<string name="delete_task">מחק משימה</string> <string name="delete_task">מחק משימה</string>
<string name="voice_command_added_task">משימות שנוצרו</string> <string name="voice_command_added_task">משימות שנוצרו</string>
<string name="external_storage_unavailable">לא ניתן לגשת לאחסון חיצוני</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">משימה אחת</item> <item quantity="one">משימה אחת</item>
<item quantity="other">%d משימות</item> <item quantity="other">%d משימות</item>

@ -160,8 +160,6 @@
<string name="premium_remove_file_confirm">よろしいですか? 取り消しできません</string> <string name="premium_remove_file_confirm">よろしいですか? 取り消しできません</string>
<string name="audio_recording_title">音声を録音中</string> <string name="audio_recording_title">音声を録音中</string>
<string name="audio_stop_recording">録音を停止</string> <string name="audio_stop_recording">録音を停止</string>
<string name="file_type_unhandled">申し訳ありません! このファイル形式を扱うアプリケーションがありません.</string>
<string name="file_err_copy">ファイル添付のコピー中にエラー</string>
<string name="ring_once">1回通知音を鳴らす</string> <string name="ring_once">1回通知音を鳴らす</string>
<string name="ring_five_times">5回通知音を鳴らす</string> <string name="ring_five_times">5回通知音を鳴らす</string>
<string name="ring_nonstop">通知音を鳴らし続ける</string> <string name="ring_nonstop">通知音を鳴らし続ける</string>
@ -281,7 +279,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks はタスクリマインダーでタスク名を話します</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks はタスクリマインダーでタスク名を話します</string>
<string name="delete_task">タスクを削除</string> <string name="delete_task">タスクを削除</string>
<string name="voice_command_added_task">追加されたタスク</string> <string name="voice_command_added_task">追加されたタスク</string>
<string name="external_storage_unavailable">外部メモリーにアクセスできません</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">タスク 1 件</item> <item quantity="one">タスク 1 件</item>
<item quantity="other">タスク %d 件</item> <item quantity="other">タスク %d 件</item>

@ -162,8 +162,6 @@
<string name="premium_remove_file_confirm">정말입니까? 되돌릴 수 없습니다</string> <string name="premium_remove_file_confirm">정말입니까? 되돌릴 수 없습니다</string>
<string name="audio_recording_title">오디오 녹음 중</string> <string name="audio_recording_title">오디오 녹음 중</string>
<string name="audio_stop_recording">녹음 중단</string> <string name="audio_stop_recording">녹음 중단</string>
<string name="file_type_unhandled">죄송하지만 이 파일 형식을 다룰 수 있는 프로그램이 없습니다.</string>
<string name="file_err_copy">첨부용 파일 복사 에러</string>
<string name="ring_once">한번 울림</string> <string name="ring_once">한번 울림</string>
<string name="ring_five_times">다섯번 울림</string> <string name="ring_five_times">다섯번 울림</string>
<string name="ring_nonstop">계속 울림</string> <string name="ring_nonstop">계속 울림</string>
@ -283,7 +281,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">할일 알림 시 할일 제목을 소리내어 읽어줍니다</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">할일 알림 시 할일 제목을 소리내어 읽어줍니다</string>
<string name="delete_task">할일 지우기</string> <string name="delete_task">할일 지우기</string>
<string name="voice_command_added_task">추가된 할일</string> <string name="voice_command_added_task">추가된 할일</string>
<string name="external_storage_unavailable">외부 저장소에 접근할 수 없음</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 할일</item> <item quantity="one">1 할일</item>
<item quantity="other">%d 할일</item> <item quantity="other">%d 할일</item>

@ -164,8 +164,6 @@
<string name="premium_remove_file_confirm">Ar tikrai? Nebegalės būti atstatyta</string> <string name="premium_remove_file_confirm">Ar tikrai? Nebegalės būti atstatyta</string>
<string name="audio_recording_title">Įrašyti garsą</string> <string name="audio_recording_title">Įrašyti garsą</string>
<string name="audio_stop_recording">Stabdyti įrašą</string> <string name="audio_stop_recording">Stabdyti įrašą</string>
<string name="file_type_unhandled">Atsiprašome! Nerasta programa, galinti atidaryti šio tipo failus.</string>
<string name="file_err_copy">Klaida bandant pridėti failą</string>
<string name="ring_once">Suskambėti vieną kartą</string> <string name="ring_once">Suskambėti vieną kartą</string>
<string name="ring_five_times">Suskambėti penkis kartus</string> <string name="ring_five_times">Suskambėti penkis kartus</string>
<string name="ring_nonstop">Skambėti nepaliaujant</string> <string name="ring_nonstop">Skambėti nepaliaujant</string>
@ -285,7 +283,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks garsu praneš užduoties pavadinimą per priminimus</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks garsu praneš užduoties pavadinimą per priminimus</string>
<string name="delete_task">Ištrinti užduotį</string> <string name="delete_task">Ištrinti užduotį</string>
<string name="voice_command_added_task">Pridėtos užduotys</string> <string name="voice_command_added_task">Pridėtos užduotys</string>
<string name="external_storage_unavailable">Nėra prieigos prie išorinės laikmenos</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 užduotis</item> <item quantity="one">1 užduotis</item>
<item quantity="other">%d užduotys(-čių)</item> <item quantity="other">%d užduotys(-čių)</item>

@ -164,8 +164,6 @@
<string name="premium_remove_file_confirm">Weet je het zeker? Dit kan niet ongedaan gemaakt worden</string> <string name="premium_remove_file_confirm">Weet je het zeker? Dit kan niet ongedaan gemaakt worden</string>
<string name="audio_recording_title">Bezig met opname</string> <string name="audio_recording_title">Bezig met opname</string>
<string name="audio_stop_recording">Opname stoppen</string> <string name="audio_stop_recording">Opname stoppen</string>
<string name="file_type_unhandled">Sorry! Er is geen applicatie gevonden die dit bestandtype ondersteunt.</string>
<string name="file_err_copy">Fout bij kopiëren toe te voegen bestand</string>
<string name="ring_once">Ring eenmalig</string> <string name="ring_once">Ring eenmalig</string>
<string name="ring_five_times">Ring vijf keer</string> <string name="ring_five_times">Ring vijf keer</string>
<string name="ring_nonstop">Ring continue</string> <string name="ring_nonstop">Ring continue</string>
@ -284,7 +282,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Bij herinneringen zullen de taaknamen uitgesproken worden</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Bij herinneringen zullen de taaknamen uitgesproken worden</string>
<string name="delete_task">Verwijder taak</string> <string name="delete_task">Verwijder taak</string>
<string name="voice_command_added_task">Toegevoegde taak</string> <string name="voice_command_added_task">Toegevoegde taak</string>
<string name="external_storage_unavailable">Geen toegang tot externe opslag</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 taak</item> <item quantity="one">1 taak</item>
<item quantity="other">%d taken</item> <item quantity="other">%d taken</item>

@ -159,8 +159,6 @@
<string name="premium_remove_file_confirm">Jesteś pewny? Tych zmian nie można odwrócić</string> <string name="premium_remove_file_confirm">Jesteś pewny? Tych zmian nie można odwrócić</string>
<string name="audio_recording_title">Nagrywanie dźwięku</string> <string name="audio_recording_title">Nagrywanie dźwięku</string>
<string name="audio_stop_recording">Zakończ nagrywanie</string> <string name="audio_stop_recording">Zakończ nagrywanie</string>
<string name="file_type_unhandled">Przepraszamy! Nie znaleziono aplikacji do obsługi tego typu pliku.</string>
<string name="file_err_copy">Błąd kopiowania pliku do załącznika</string>
<string name="ring_once">Dzwoń raz</string> <string name="ring_once">Dzwoń raz</string>
<string name="ring_five_times">Dzwoń pięć razy</string> <string name="ring_five_times">Dzwoń pięć razy</string>
<string name="ring_nonstop">Dzwoń nonstop</string> <string name="ring_nonstop">Dzwoń nonstop</string>
@ -278,7 +276,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks będzie mówił nazwę zadania podczas przypomnienia</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks będzie mówił nazwę zadania podczas przypomnienia</string>
<string name="delete_task">Usuń zadanie</string> <string name="delete_task">Usuń zadanie</string>
<string name="voice_command_added_task">Dodane zadanie</string> <string name="voice_command_added_task">Dodane zadanie</string>
<string name="external_storage_unavailable">Brak dostępu do pamięci zewnętrznej</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 zadanie</item> <item quantity="one">1 zadanie</item>
<item quantity="other">%d zadań</item> <item quantity="other">%d zadań</item>

@ -166,8 +166,6 @@
<string name="premium_remove_file_confirm">Você tem certeza? Não pode ser desfeito.</string> <string name="premium_remove_file_confirm">Você tem certeza? Não pode ser desfeito.</string>
<string name="audio_recording_title">Gravando Áudio</string> <string name="audio_recording_title">Gravando Áudio</string>
<string name="audio_stop_recording">Parar Gravação</string> <string name="audio_stop_recording">Parar Gravação</string>
<string name="file_type_unhandled">Desculpa! Nenhuma aplicação para manipular este tipo de arquivo foi encontrada.</string>
<string name="file_err_copy">Erro ao copiar o arquivo para o anexo</string>
<string name="ring_once">Tocar uma vez</string> <string name="ring_once">Tocar uma vez</string>
<string name="ring_five_times">Tocar cinco vezes</string> <string name="ring_five_times">Tocar cinco vezes</string>
<string name="ring_nonstop">Tocar continuamente</string> <string name="ring_nonstop">Tocar continuamente</string>
@ -287,7 +285,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks irá falar o nome das tarefas durante os lembretes</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks irá falar o nome das tarefas durante os lembretes</string>
<string name="delete_task">Excluir tarefa</string> <string name="delete_task">Excluir tarefa</string>
<string name="voice_command_added_task">Tarefa adicionada</string> <string name="voice_command_added_task">Tarefa adicionada</string>
<string name="external_storage_unavailable">Não é possível acessar armazenamento externo</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 tarefa</item> <item quantity="one">1 tarefa</item>
<item quantity="other">%d tarefas</item> <item quantity="other">%d tarefas</item>

@ -154,8 +154,6 @@
<string name="premium_remove_file_confirm">Tem a certeza? A ação não pode ser anulada!</string> <string name="premium_remove_file_confirm">Tem a certeza? A ação não pode ser anulada!</string>
<string name="audio_recording_title">Gravação áudio</string> <string name="audio_recording_title">Gravação áudio</string>
<string name="audio_stop_recording">Parar gravação</string> <string name="audio_stop_recording">Parar gravação</string>
<string name="file_type_unhandled">Não foi encontrada qualquer aplicação para gerir ficheiros deste tipo.</string>
<string name="file_err_copy">Erro ao copiar o ficheiro como anexo</string>
<string name="ring_once">Tocar uma vez</string> <string name="ring_once">Tocar uma vez</string>
<string name="ring_five_times">Tocar 5 vezes</string> <string name="ring_five_times">Tocar 5 vezes</string>
<string name="ring_nonstop">Tocar incessantemente</string> <string name="ring_nonstop">Tocar incessantemente</string>
@ -273,7 +271,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">O Tasks irá reproduzir o nome da tarefa durante os lembretes</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">O Tasks irá reproduzir o nome da tarefa durante os lembretes</string>
<string name="delete_task">Eliminar tarefa</string> <string name="delete_task">Eliminar tarefa</string>
<string name="voice_command_added_task">Tarefa adicionada</string> <string name="voice_command_added_task">Tarefa adicionada</string>
<string name="external_storage_unavailable">Não foi possível aceder ao disco externo</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 tarefa</item> <item quantity="one">1 tarefa</item>
<item quantity="other">%d tarefas</item> <item quantity="other">%d tarefas</item>

@ -158,8 +158,6 @@
<string name="premium_remove_file_confirm">Вы уверены? Действие нельзя отменить</string> <string name="premium_remove_file_confirm">Вы уверены? Действие нельзя отменить</string>
<string name="audio_recording_title">Запись голоса</string> <string name="audio_recording_title">Запись голоса</string>
<string name="audio_stop_recording">Остановить запись</string> <string name="audio_stop_recording">Остановить запись</string>
<string name="file_type_unhandled">Извините! Не найдена программа для просмотра файлов этого типа.</string>
<string name="file_err_copy">Ошибка копирования прикрепляемого файла.</string>
<string name="ring_once">1 раз</string> <string name="ring_once">1 раз</string>
<string name="ring_five_times">5 раз</string> <string name="ring_five_times">5 раз</string>
<string name="ring_nonstop">Пока не выкл.</string> <string name="ring_nonstop">Пока не выкл.</string>
@ -279,7 +277,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks должен произносить название задач во время напоминаний</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks должен произносить название задач во время напоминаний</string>
<string name="delete_task">Удалить задачу</string> <string name="delete_task">Удалить задачу</string>
<string name="voice_command_added_task">Добавленная задача</string> <string name="voice_command_added_task">Добавленная задача</string>
<string name="external_storage_unavailable">Нет доступа к внешнему хранилищу</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 задача</item> <item quantity="one">1 задача</item>
<item quantity="other">%d задач(а/и)</item> <item quantity="other">%d задач(а/и)</item>

@ -153,8 +153,6 @@
<string name="premium_remove_file_confirm">Naozaj? Nedá sa vrátiť</string> <string name="premium_remove_file_confirm">Naozaj? Nedá sa vrátiť</string>
<string name="audio_recording_title">Nahrávanie zvuku</string> <string name="audio_recording_title">Nahrávanie zvuku</string>
<string name="audio_stop_recording">Zastaviť nahrávanie</string> <string name="audio_stop_recording">Zastaviť nahrávanie</string>
<string name="file_type_unhandled">Pre tento typ súboru nebola nájdená žiadna aplikácia</string>
<string name="file_err_copy">Chyba pri kopírovaní súboru do prílohy</string>
<string name="ring_once">Zvoniť raz</string> <string name="ring_once">Zvoniť raz</string>
<string name="ring_five_times">Zvoniť päť krát</string> <string name="ring_five_times">Zvoniť päť krát</string>
<string name="ring_nonstop">Zvoniť neustále</string> <string name="ring_nonstop">Zvoniť neustále</string>
@ -273,7 +271,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Úlohy vyslovia názov úlohy počas pripomineky</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Úlohy vyslovia názov úlohy počas pripomineky</string>
<string name="delete_task">Zmazať úlohu</string> <string name="delete_task">Zmazať úlohu</string>
<string name="voice_command_added_task">Pridaná úloha</string> <string name="voice_command_added_task">Pridaná úloha</string>
<string name="external_storage_unavailable">Nemožno získať prístup k externej pamäti</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 úloha</item> <item quantity="one">1 úloha</item>
<item quantity="other">%d úlohy</item> <item quantity="other">%d úlohy</item>

@ -131,8 +131,6 @@
<string name="premium_remove_file_confirm">Ste prepričani? Tega ni mogoče razveljaviti</string> <string name="premium_remove_file_confirm">Ste prepričani? Tega ni mogoče razveljaviti</string>
<string name="audio_recording_title">Snemam zvok</string> <string name="audio_recording_title">Snemam zvok</string>
<string name="audio_stop_recording">Prekini snemanje</string> <string name="audio_stop_recording">Prekini snemanje</string>
<string name="file_type_unhandled">Žal nobena aplikacija ne ustreza takim datotekam.</string>
<string name="file_err_copy">Napaka pri kopiranju datoteke za priponko</string>
<string name="random_reminder_hour">na uro</string> <string name="random_reminder_hour">na uro</string>
<string name="random_reminder_day">dan</string> <string name="random_reminder_day">dan</string>
<string name="random_reminder_week">na teden</string> <string name="random_reminder_week">na teden</string>

@ -153,8 +153,6 @@
<string name="premium_remove_file_confirm">Är du säker? Detta kan inte ångras</string> <string name="premium_remove_file_confirm">Är du säker? Detta kan inte ångras</string>
<string name="audio_recording_title">Spelar in ljud</string> <string name="audio_recording_title">Spelar in ljud</string>
<string name="audio_stop_recording">Avsluta inspelning</string> <string name="audio_stop_recording">Avsluta inspelning</string>
<string name="file_type_unhandled">Tyvärr hittades ingen applikation för att hantera den här filtypen</string>
<string name="file_err_copy">Ett fel uppstod vid kopiering av filen till bilaga</string>
<string name="ring_once">Ring en gång</string> <string name="ring_once">Ring en gång</string>
<string name="ring_five_times">Ring fem gånger</string> <string name="ring_five_times">Ring fem gånger</string>
<string name="ring_nonstop">Ring konstant</string> <string name="ring_nonstop">Ring konstant</string>
@ -198,7 +196,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks läser upp uppgifterna vid påminnelse</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks läser upp uppgifterna vid påminnelse</string>
<string name="delete_task">Radera uppgift</string> <string name="delete_task">Radera uppgift</string>
<string name="voice_command_added_task">Uppgift skapad</string> <string name="voice_command_added_task">Uppgift skapad</string>
<string name="external_storage_unavailable">Kan inte komma åt externt lagringsutrymme</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 uppgift</item> <item quantity="one">1 uppgift</item>
<item quantity="other">%d uppgifter</item> <item quantity="other">%d uppgifter</item>

@ -170,8 +170,6 @@
<string name="premium_remove_file_confirm">Emin misiniz? Geri döndürülemez</string> <string name="premium_remove_file_confirm">Emin misiniz? Geri döndürülemez</string>
<string name="audio_recording_title">Ses Kaydediliyor</string> <string name="audio_recording_title">Ses Kaydediliyor</string>
<string name="audio_stop_recording">Kaydı Durdur</string> <string name="audio_stop_recording">Kaydı Durdur</string>
<string name="file_type_unhandled">Üzgünüm! Bu dosya türünü destekleyen bir uygulama bulunamadı.</string>
<string name="file_err_copy">Dosyanın ek olarak kopyalanmasında hata</string>
<string name="ring_once">Bir kez çal</string> <string name="ring_once">Bir kez çal</string>
<string name="ring_five_times">5 kez çal</string> <string name="ring_five_times">5 kez çal</string>
<string name="ring_nonstop">Durmadan çal</string> <string name="ring_nonstop">Durmadan çal</string>
@ -291,7 +289,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks, görev adlarını görev hatırlatmaları sırasında söyleyecek</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks, görev adlarını görev hatırlatmaları sırasında söyleyecek</string>
<string name="delete_task">Görevi sil</string> <string name="delete_task">Görevi sil</string>
<string name="voice_command_added_task">Görev eklendi</string> <string name="voice_command_added_task">Görev eklendi</string>
<string name="external_storage_unavailable">Dış depolamaya ulaşılamadı.</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 görev</item> <item quantity="one">1 görev</item>
<item quantity="other">%d görev</item> <item quantity="other">%d görev</item>

@ -155,8 +155,6 @@
<string name="premium_remove_file_confirm">Ви впевнені? Це не може бути скасовано.</string> <string name="premium_remove_file_confirm">Ви впевнені? Це не може бути скасовано.</string>
<string name="audio_recording_title">Запис аудіо</string> <string name="audio_recording_title">Запис аудіо</string>
<string name="audio_stop_recording">Зупинити запис</string> <string name="audio_stop_recording">Зупинити запис</string>
<string name="file_type_unhandled">Вибачте! Не знайдено програму для перегляду файлів цього типу.</string>
<string name="file_err_copy">Помилка копіювання файлу для вкладення</string>
<string name="ring_once">Дзвеніти 1 раз</string> <string name="ring_once">Дзвеніти 1 раз</string>
<string name="ring_five_times">Дзвеніти 5 раз</string> <string name="ring_five_times">Дзвеніти 5 раз</string>
<string name="ring_nonstop">Дзвеніти безперервно</string> <string name="ring_nonstop">Дзвеніти безперервно</string>
@ -201,7 +199,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks повинен вимовляти назву завдань під час нагадувань</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks повинен вимовляти назву завдань під час нагадувань</string>
<string name="delete_task">Видалити завдання</string> <string name="delete_task">Видалити завдання</string>
<string name="voice_command_added_task">Додане завдання</string> <string name="voice_command_added_task">Додане завдання</string>
<string name="external_storage_unavailable">Немає доступу до зовнішнього сховища</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 завдання</item> <item quantity="one">1 завдання</item>
<item quantity="other">%d завдань</item> <item quantity="other">%d завдань</item>

@ -159,8 +159,6 @@
<string name="premium_remove_file_confirm">您确定吗?无法恢复的喔</string> <string name="premium_remove_file_confirm">您确定吗?无法恢复的喔</string>
<string name="audio_recording_title">正在录制音频</string> <string name="audio_recording_title">正在录制音频</string>
<string name="audio_stop_recording">停止录制</string> <string name="audio_stop_recording">停止录制</string>
<string name="file_type_unhandled">对不起!找不到应用程序处理这种文件类型。</string>
<string name="file_err_copy">复制文件添加附件时出错</string>
<string name="ring_once">响铃一次</string> <string name="ring_once">响铃一次</string>
<string name="ring_five_times">响铃五次</string> <string name="ring_five_times">响铃五次</string>
<string name="ring_nonstop">响个不停</string> <string name="ring_nonstop">响个不停</string>
@ -280,7 +278,6 @@
<string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks会在任务提醒时读出任务名</string> <string name="EPr_voiceRemindersEnabled_desc_enabled">Tasks会在任务提醒时读出任务名</string>
<string name="delete_task">删除任务</string> <string name="delete_task">删除任务</string>
<string name="voice_command_added_task">已添加的任务</string> <string name="voice_command_added_task">已添加的任务</string>
<string name="external_storage_unavailable">无法访问外部存储</string>
<plurals name="Ntasks"> <plurals name="Ntasks">
<item quantity="one">1 个任务</item> <item quantity="one">1 个任务</item>
<item quantity="other">%d 个任务</item> <item quantity="other">%d 个任务</item>

@ -124,8 +124,6 @@
<string name="premium_remove_file_confirm">您確定嗎?無法恢復的喔</string> <string name="premium_remove_file_confirm">您確定嗎?無法恢復的喔</string>
<string name="audio_recording_title">正在錄製音頻</string> <string name="audio_recording_title">正在錄製音頻</string>
<string name="audio_stop_recording">停止錄製</string> <string name="audio_stop_recording">停止錄製</string>
<string name="file_type_unhandled">對不起!找不到應用程序處理這種文件類型。</string>
<string name="file_err_copy">複製文件添加附件時出錯</string>
<string name="ring_once">響鈴一次</string> <string name="ring_once">響鈴一次</string>
<string name="ring_five_times">響鈴五次</string> <string name="ring_five_times">響鈴五次</string>
<string name="ring_nonstop">不斷響鈴</string> <string name="ring_nonstop">不斷響鈴</string>

@ -419,10 +419,6 @@ File %1$s contained %2$s.\n\n
<string name="audio_recording_title">Recording Audio</string> <string name="audio_recording_title">Recording Audio</string>
<string name="audio_stop_recording">Stop Recording</string> <string name="audio_stop_recording">Stop Recording</string>
<string name="file_type_unhandled">Sorry! No application was found to handle this file type.</string>
<string name="file_err_copy">Error copying file for attachment</string>
<!-- slide 45a: Task Edit: Reminder mode: ring once --> <!-- slide 45a: Task Edit: Reminder mode: ring once -->
<string name="ring_once">Ring once</string> <string name="ring_once">Ring once</string>
@ -612,7 +608,6 @@ File %1$s contained %2$s.\n\n
<string name="delete_task">Delete task</string> <string name="delete_task">Delete task</string>
<string name="voice_command_added_task">Added task</string> <string name="voice_command_added_task">Added task</string>
<string name="external_storage_unavailable">Cannot access external storage</string>
<!-- ==================================================== Generic Units == --> <!-- ==================================================== Generic Units == -->

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<paths> <paths>
<root-path <root-path
name="root" name="root"
path="."/> path="." />
<cache-path
name="cache"
path="." />
</paths> </paths>
Loading…
Cancel
Save