diff --git a/app/src/androidTest/java/org/tasks/preferences/PermissivePermissionChecker.java b/app/src/androidTest/java/org/tasks/preferences/PermissivePermissionChecker.java index 9d142fb63..f312d68ce 100644 --- a/app/src/androidTest/java/org/tasks/preferences/PermissivePermissionChecker.java +++ b/app/src/androidTest/java/org/tasks/preferences/PermissivePermissionChecker.java @@ -14,11 +14,6 @@ public class PermissivePermissionChecker extends PermissionChecker { return true; } - @Override - public boolean canWriteToExternalStorage() { - return true; - } - @Override public boolean canAccessAccounts() { return true; diff --git a/app/src/main/java/com/todoroo/andlib/utility/AndroidUtilities.java b/app/src/main/java/com/todoroo/andlib/utility/AndroidUtilities.java index 9e27df17f..9ec9ee7ec 100644 --- a/app/src/main/java/com/todoroo/andlib/utility/AndroidUtilities.java +++ b/app/src/main/java/com/todoroo/andlib/utility/AndroidUtilities.java @@ -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) { // developer.android.com/guide/practices/screens_support.html#dips-pels return (int) (dp * displayMetrics.density + 0.5f); diff --git a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.java b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.java index 8d62e12da..1a1070a46 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.java +++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.java @@ -14,6 +14,7 @@ import android.annotation.SuppressLint; import androidx.lifecycle.ViewModelProviders; import android.content.Intent; import android.content.res.Configuration; +import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.fragment.app.FragmentManager; @@ -464,7 +465,7 @@ public class MainActivity extends InjectingAppCompatActivity } @Override - public void addComment(String message, String picture) { + public void addComment(String message, Uri picture) { TaskEditFragment taskEditFragment = getTaskEditFragment(); if (taskEditFragment != null) { taskEditFragment.addComment(message, picture); diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.java b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.java index 5ff812f95..7bde43d53 100755 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.java +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.java @@ -5,23 +5,17 @@ */ package com.todoroo.astrid.activity; -import static org.tasks.date.DateTimeUtils.newDateTime; - import android.app.Activity; import android.content.Context; +import android.net.Uri; 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.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; -import butterknife.BindView; -import butterknife.ButterKnife; + import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.DateUtilities; 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.ui.EditTitleControlSet; import com.todoroo.astrid.utility.Flags; -import java.util.List; -import javax.inject.Inject; + import org.tasks.LocalBroadcastManager; import org.tasks.R; import org.tasks.analytics.Tracker; @@ -50,6 +43,22 @@ import org.tasks.preferences.Preferences; import org.tasks.ui.MenuColorizer; 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 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(); + if (picture != null) { + Uri output = copyToUri(context, preferences.getAttachmentsDirectory(), picture); + userActivity.setPicture(output); + } userActivity.setMessage(message); userActivity.setTargetId(model.getUuid()); userActivity.setCreated(DateUtilities.now()); - if (picture != null) { - userActivity.setPicture(picture); - } userActivityDao.createNew(userActivity); commentsController.reloadView(); } diff --git a/app/src/main/java/com/todoroo/astrid/backup/TasksXmlImporter.java b/app/src/main/java/com/todoroo/astrid/backup/TasksXmlImporter.java index 6bc356f72..cf63eaab2 100755 --- a/app/src/main/java/com/todoroo/astrid/backup/TasksXmlImporter.java +++ b/app/src/main/java/com/todoroo/astrid/backup/TasksXmlImporter.java @@ -39,14 +39,11 @@ import org.xmlpull.v1.XmlPullParserFactory; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.StringReader; import javax.inject.Inject; import timber.log.Timber; -import static org.tasks.files.FileHelper.fromUri; - public class TasksXmlImporter { private static final String FORMAT2 = "2"; // $NON-NLS-1$ @@ -120,7 +117,7 @@ public class TasksXmlImporter { private void performImport() throws IOException, XmlPullParserException { XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParser xpp = factory.newPullParser(); - InputStream inputStream = fromUri(activity, input); + InputStream inputStream = activity.getContentResolver().openInputStream(input); InputStreamReader reader = new InputStreamReader(inputStream); xpp.setInput(reader); diff --git a/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.java b/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.java index bcc4d5364..b13c7223a 100644 --- a/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.java +++ b/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.java @@ -5,55 +5,43 @@ */ 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.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.media.MediaPlayer; import android.net.Uri; import android.os.Bundle; -import androidx.annotation.Nullable; -import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.webkit.MimeTypeMap; import android.widget.LinearLayout; 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 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.activities.CameraActivity; import org.tasks.data.TaskAttachment; import org.tasks.data.TaskAttachmentDao; -import org.tasks.dialogs.AddAttachmentDialog; import org.tasks.dialogs.DialogBuilder; -import org.tasks.files.FileExplore; import org.tasks.files.FileHelper; import org.tasks.injection.ForActivity; import org.tasks.injection.FragmentComponent; import org.tasks.preferences.Preferences; 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 { @@ -75,19 +63,6 @@ public class FilesControlSet extends TaskEditControlFragment { 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 @Override public View onCreateView( @@ -96,12 +71,9 @@ public class FilesControlSet extends TaskEditControlFragment { taskUuid = task.getUuid(); - final List files = new ArrayList<>(); for (TaskAttachment attachment : taskAttachmentDao.getAttachments(taskUuid)) { - files.add(attachment); addAttachment(attachment); } - validateFiles(files); return view; } @@ -131,46 +103,12 @@ public class FilesControlSet extends TaskEditControlFragment { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == AddAttachmentDialog.REQUEST_CAMERA) { - if (resultCode == RESULT_OK) { - Uri uri = data.getParcelableExtra(CameraActivity.EXTRA_URI); - final File file = new File(uri.getPath()); - 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 (requestCode == REQUEST_CAMERA + || requestCode == REQUEST_STORAGE + || requestCode == REQUEST_GALLERY + || requestCode == REQUEST_AUDIO) { if (resultCode == RESULT_OK) { - String path = data.getStringExtra(FileExplore.EXTRA_FILE); - 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); - } + copyToAttachmentDirectory(data.getData()); } } else { super.onActivityResult(requestCode, resultCode, data); @@ -199,32 +137,13 @@ public class FilesControlSet extends TaskEditControlFragment { android.R.string.ok, (dialog, which) -> { taskAttachmentDao.delete(taskAttachment); - if (!Strings.isNullOrEmpty(taskAttachment.getPath())) { - File f = new File(taskAttachment.getPath()); - f.delete(); - } + FileHelper.delete(context, taskAttachment.parseUri()); attachmentContainer.removeView(fileRow); }) .setNegativeButton(android.R.string.cancel, null) .show()); } - private void validateFiles(List 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 protected void inject(FragmentComponent component) { component.inject(this); @@ -232,113 +151,17 @@ public class FilesControlSet extends TaskEditControlFragment { @SuppressLint("NewApi") private void showFile(final TaskAttachment m) { - final String fileType = - !Strings.isNullOrEmpty(m.getContentType()) - ? m.getContentType() - : 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(); + final Uri uri = m.parseUri(); + if (uri != null) { + FileHelper.startActionView(getActivity(), uri); } } - private void createNewFileAttachment(File file, String fileType) { + private void copyToAttachmentDirectory(Uri input) { + Uri output = copyToUri(context, preferences.getAttachmentsDirectory(), input); TaskAttachment attachment = - TaskAttachment.createNewAttachment( - taskUuid, file.getAbsolutePath(), file.getName(), fileType); + createNewAttachment(taskUuid, output, FileHelper.getFilename(context, output)); taskAttachmentDao.createNew(attachment); addAttachment(attachment); } - - private File getFilename(String extension) { - AtomicReference 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(); - } } diff --git a/app/src/main/java/com/todoroo/astrid/notes/CommentsController.java b/app/src/main/java/com/todoroo/astrid/notes/CommentsController.java index a12faf44b..cfa213718 100644 --- a/app/src/main/java/com/todoroo/astrid/notes/CommentsController.java +++ b/app/src/main/java/com/todoroo/astrid/notes/CommentsController.java @@ -5,15 +5,9 @@ */ 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.content.Intent; import android.graphics.Color; import android.net.Uri; -import androidx.core.content.FileProvider; import android.text.Html; import android.text.util.Linkify; import android.view.View; @@ -21,18 +15,23 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; + import com.todoroo.andlib.utility.DateUtilities; 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.data.UserActivity; import org.tasks.data.UserActivityDao; import org.tasks.files.FileHelper; 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 { private final UserActivityDao userActivityDao; @@ -53,27 +52,17 @@ public class CommentsController { } private static void setupImagePopupForCommentView( - View view, ImageView commentPictureView, final Uri updateBitmap, final Activity activity) { - if (updateBitmap != null) { + View view, ImageView commentPictureView, final Uri uri, final Activity activity) { + if (uri != null) { commentPictureView.setVisibility(View.VISIBLE); - String path = getPathFromUri(activity, updateBitmap); commentPictureView.setImageBitmap( sampleBitmap( - path, + activity, + uri, commentPictureView.getLayoutParams().width, commentPictureView.getLayoutParams().height)); - view.setOnClickListener( - 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); - }); + view.setOnClickListener(v -> FileHelper.startActionView(activity, uri)); } else { commentPictureView.setVisibility(View.GONE); } diff --git a/app/src/main/java/com/todoroo/astrid/service/StartupService.java b/app/src/main/java/com/todoroo/astrid/service/StartupService.java index 61b635fd1..2e7d78bb1 100644 --- a/app/src/main/java/com/todoroo/astrid/service/StartupService.java +++ b/app/src/main/java/com/todoroo/astrid/service/StartupService.java @@ -5,9 +5,8 @@ */ package com.todoroo.astrid.service; -import static com.google.common.base.Strings.isNullOrEmpty; - import android.content.Context; +import android.net.Uri; import android.os.Environment; 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.dao.Database; 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.LocalBroadcastManager; import org.tasks.R; @@ -35,12 +32,24 @@ import org.tasks.data.Tag; import org.tasks.data.TagDao; import org.tasks.data.TagData; 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.preferences.DefaultFilterProvider; import org.tasks.preferences.Preferences; import org.tasks.scheduling.BackgroundScheduler; + +import java.io.File; +import java.util.List; + +import javax.inject.Inject; + import timber.log.Timber; +import static com.google.common.base.Strings.isNullOrEmpty; + public class StartupService { private static final int V4_8_0 = 380; @@ -61,6 +70,8 @@ public class StartupService { private final FilterDao filterDao; private final DefaultFilterProvider defaultFilterProvider; private final GoogleTaskListDao googleTaskListDao; + private final UserActivityDao userActivityDao; + private final TaskAttachmentDao taskAttachmentDao; @Inject public StartupService( @@ -74,7 +85,9 @@ public class StartupService { TagDao tagDao, FilterDao filterDao, DefaultFilterProvider defaultFilterProvider, - GoogleTaskListDao googleTaskListDao) { + GoogleTaskListDao googleTaskListDao, + UserActivityDao userActivityDao, + TaskAttachmentDao taskAttachmentDao) { this.database = database; this.preferences = preferences; this.tracker = tracker; @@ -86,6 +99,8 @@ public class StartupService { this.filterDao = filterDao; this.defaultFilterProvider = defaultFilterProvider; this.googleTaskListDao = googleTaskListDao; + this.userActivityDao = userActivityDao; + this.taskAttachmentDao = taskAttachmentDao; } /** Called when this application is started up */ @@ -198,18 +213,32 @@ public class StartupService { } private void migrateUris() { - String backupDirectory = preferences.getStringValue(R.string.p_backup_dir); - if (!Strings.isNullOrEmpty(backupDirectory)) { - File file = new File(backupDirectory); - try { - if (file.canWrite()) { - preferences.setUri(R.string.p_backup_dir, file.toURI()); - } else { - preferences.remove(R.string.p_backup_dir); - } - } catch (SecurityException ignored) { - preferences.remove(R.string.p_backup_dir); + migrateUriPreference(R.string.p_backup_dir); + migrateUriPreference(R.string.p_attachment_dir); + for (UserActivity userActivity : userActivityDao.getComments()) { + userActivity.convertPictureUri(); + userActivityDao.update(userActivity); + } + for (TaskAttachment attachment : taskAttachmentDao.getAttachments()) { + attachment.convertPathUri(); + taskAttachmentDao.update(attachment); + } + } + + 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); } } diff --git a/app/src/main/java/com/todoroo/astrid/voice/AACRecorder.java b/app/src/main/java/com/todoroo/astrid/voice/AACRecorder.java index 8e0c432c5..4d47da7d5 100644 --- a/app/src/main/java/com/todoroo/astrid/voice/AACRecorder.java +++ b/app/src/main/java/com/todoroo/astrid/voice/AACRecorder.java @@ -1,36 +1,49 @@ package com.todoroo.astrid.voice; import androidx.lifecycle.ViewModel; + +import android.content.Context; import android.media.MediaRecorder; +import android.net.Uri; import android.os.SystemClock; import java.io.IOException; +import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; + +import org.tasks.files.FileHelper; import org.tasks.preferences.Preferences; +import org.tasks.time.DateTime; + import timber.log.Timber; public class AACRecorder extends ViewModel { private MediaRecorder mediaRecorder; - private final AtomicReference nameRef = new AtomicReference<>(); private boolean recording; private AACRecorderCallbacks listener; private Preferences preferences; private long base; - private String tempFile; + private Uri uri; - public synchronized void startRecording() { + public synchronized void startRecording(Context context) throws IOException { if (recording) { return; } - tempFile = preferences.getNewAudioAttachmentPath(nameRef); + uri = + FileHelper.newFile( + context, + preferences.getCacheDirectory(), + "audio/m4a", + new DateTime().toString("yyyyMMddHHmm"), + ".m4a"); mediaRecorder = new MediaRecorder(); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); - mediaRecorder.setOutputFile(tempFile); + mediaRecorder.setOutputFile(uri.getPath()); mediaRecorder.setOnErrorListener( (mr, what, extra) -> Timber.e("mediaRecorder.onError(mr, %s, %s)", what, extra)); mediaRecorder.setOnInfoListener( @@ -63,7 +76,7 @@ public class AACRecorder extends ViewModel { mediaRecorder.release(); recording = false; if (listener != null) { - listener.encodingFinished(tempFile); + listener.encodingFinished(uri); } } @@ -78,6 +91,6 @@ public class AACRecorder extends ViewModel { public interface AACRecorderCallbacks { - void encodingFinished(String path); + void encodingFinished(Uri uri); } } diff --git a/app/src/main/java/org/tasks/activities/CameraActivity.java b/app/src/main/java/org/tasks/activities/CameraActivity.java index 210209318..d122e7674 100644 --- a/app/src/main/java/org/tasks/activities/CameraActivity.java +++ b/app/src/main/java/org/tasks/activities/CameraActivity.java @@ -1,36 +1,39 @@ package org.tasks.activities; -import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; - import android.annotation.SuppressLint; +import android.content.ContentResolver; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Bundle; import android.provider.MediaStore; -import androidx.core.content.FileProvider; -import android.widget.Toast; + 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.IOException; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; + import javax.inject.Inject; -import org.tasks.R; -import org.tasks.injection.ActivityComponent; -import org.tasks.injection.InjectingAppCompatActivity; -import org.tasks.preferences.Preferences; -import timber.log.Timber; + +import androidx.core.content.FileProvider; + +import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; public class CameraActivity extends InjectingAppCompatActivity { - public static final String EXTRA_URI = "extra_uri"; 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; - private File output; + private Uri uri; @SuppressLint("NewApi") @Override @@ -38,29 +41,41 @@ public class CameraActivity extends InjectingAppCompatActivity { super.onCreate(savedInstanceState); if (savedInstanceState != null) { - output = (File) savedInstanceState.getSerializable(EXTRA_OUTPUT); + uri = savedInstanceState.getParcelable(EXTRA_URI); } else { - output = getFilename(".jpeg"); - if (output == null) { - Toast.makeText(this, R.string.external_storage_unavailable, Toast.LENGTH_LONG).show(); + try { + uri = + 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 { - final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - Uri uri = FileProvider.getUriForFile(this, Constants.FILE_PROVIDER_AUTHORITY, output); - intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); - if (atLeastLollipop()) { - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - } else { - List resolveInfoList = - 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); - } + List resolveInfoList = + 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) { if (requestCode == REQUEST_CODE_CAMERA) { if (resultCode == RESULT_OK) { - if (output != null) { - final Uri uri = Uri.fromFile(output); Intent intent = new Intent(); - intent.putExtra(EXTRA_URI, uri); + intent.setData(uri); setResult(RESULT_OK, intent); - } } finish(); } else { @@ -90,25 +102,6 @@ public class CameraActivity extends InjectingAppCompatActivity { protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putSerializable(EXTRA_OUTPUT, output); - } - - private File getFilename(String extension) { - AtomicReference 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; + outState.putParcelable(EXTRA_URI, uri); } } diff --git a/app/src/main/java/org/tasks/backup/TasksJsonExporter.java b/app/src/main/java/org/tasks/backup/TasksJsonExporter.java index 8a652b76b..99421fdf6 100755 --- a/app/src/main/java/org/tasks/backup/TasksJsonExporter.java +++ b/app/src/main/java/org/tasks/backup/TasksJsonExporter.java @@ -7,6 +7,7 @@ import android.net.Uri; import android.os.Handler; import android.widget.Toast; +import com.google.common.io.Files; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.todoroo.andlib.utility.DialogUtilities; @@ -138,7 +139,8 @@ public class TasksJsonExporter { List tasks = taskDao.getAll(); 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); doTasksExport(os, tasks); os.close(); diff --git a/app/src/main/java/org/tasks/backup/TasksJsonImporter.java b/app/src/main/java/org/tasks/backup/TasksJsonImporter.java index 604f42d52..4ebb3c496 100644 --- a/app/src/main/java/org/tasks/backup/TasksJsonImporter.java +++ b/app/src/main/java/org/tasks/backup/TasksJsonImporter.java @@ -40,6 +40,7 @@ import org.tasks.data.UserActivity; import org.tasks.data.UserActivityDao; import org.tasks.dialogs.DialogBuilder; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -48,8 +49,6 @@ import javax.inject.Inject; import timber.log.Timber; -import static org.tasks.files.FileHelper.fromUri; - public class TasksJsonImporter { private final TagDataDao tagDataDao; @@ -120,12 +119,18 @@ public class TasksJsonImporter { private void performImport() { 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); JsonObject input = gson.fromJson(reader, JsonObject.class); try { JsonElement data = input.get("data"); + int version = input.get("version").getAsInt(); BackupContainer backupContainer = gson.fromJson(data, BackupContainer.class); for (TagData tagData : backupContainer.getTags()) { if (tagDataDao.getByUuid(tagData.getRemoteId()) == null) { @@ -174,6 +179,9 @@ public class TasksJsonImporter { } for (UserActivity comment : backup.comments) { comment.setTargetId(taskUuid); + if (version < 546) { + comment.convertPictureUri(); + } userActivityDao.createNew(comment); } for (GoogleTask googleTask : backup.google) { @@ -191,6 +199,9 @@ public class TasksJsonImporter { } for (TaskAttachment attachment : backup.getAttachments()) { attachment.setTaskId(taskUuid); + if (version < 546) { + attachment.convertPathUri(); + } taskAttachmentDao.insert(attachment); } for (CaldavTask caldavTask : backup.getCaldavTasks()) { diff --git a/app/src/main/java/org/tasks/data/TaskAttachment.java b/app/src/main/java/org/tasks/data/TaskAttachment.java index c686139e5..f53d0894a 100644 --- a/app/src/main/java/org/tasks/data/TaskAttachment.java +++ b/app/src/main/java/org/tasks/data/TaskAttachment.java @@ -1,12 +1,18 @@ package org.tasks.data; +import android.net.Uri; + import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; + +import com.google.common.base.Strings; import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Table; import com.todoroo.astrid.data.Task; +import java.io.File; + @Entity(tableName = "task_attachments") public final class TaskAttachment { @@ -16,11 +22,6 @@ public final class TaskAttachment { public static final Property.LongProperty ID = new Property.LongProperty(TABLE, "_id"); /** default directory for files on external storage */ 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) @ColumnInfo(name = "_id") @@ -37,18 +38,16 @@ public final class TaskAttachment { private String name = ""; @ColumnInfo(name = "path") - private String path = ""; + private String uri = ""; @ColumnInfo(name = "content_type") private String contentType = ""; - public static TaskAttachment createNewAttachment( - String taskUuid, String filePath, String fileName, String fileType) { + public static TaskAttachment createNewAttachment(String taskUuid, Uri uri, String fileName) { TaskAttachment attachment = new TaskAttachment(); attachment.setTaskId(taskUuid); attachment.setName(fileName); - attachment.setPath(filePath); - attachment.setContentType(fileType); + attachment.setUri(uri); return attachment; } @@ -84,12 +83,12 @@ public final class TaskAttachment { this.name = name; } - public String getPath() { - return path; + public String getUri() { + return uri; } - public void setPath(String path) { - this.path = path; + public void setUri(String uri) { + this.uri = uri; } public String getContentType() { @@ -99,4 +98,16 @@ public final class TaskAttachment { public void setContentType(String 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); + } } diff --git a/app/src/main/java/org/tasks/data/TaskAttachmentDao.java b/app/src/main/java/org/tasks/data/TaskAttachmentDao.java index ba501d49f..a570568f4 100644 --- a/app/src/main/java/org/tasks/data/TaskAttachmentDao.java +++ b/app/src/main/java/org/tasks/data/TaskAttachmentDao.java @@ -16,6 +16,9 @@ public abstract class TaskAttachmentDao { @Query("SELECT * FROM task_attachments WHERE task_id = :taskUuid") public abstract List getAttachments(String taskUuid); + @Query("SELECT * FROM task_attachments") + public abstract List getAttachments(); + @Delete public abstract void delete(TaskAttachment taskAttachment); diff --git a/app/src/main/java/org/tasks/data/UserActivity.java b/app/src/main/java/org/tasks/data/UserActivity.java index 355e5e6a4..6992e3772 100644 --- a/app/src/main/java/org/tasks/data/UserActivity.java +++ b/app/src/main/java/org/tasks/data/UserActivity.java @@ -1,17 +1,22 @@ 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.os.Parcel; import android.os.Parcelable; + +import com.google.common.base.Strings; import com.todoroo.astrid.data.Task; -import java.io.File; + import org.json.JSONException; import org.json.JSONObject; 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; @Entity(tableName = "userActivity") @@ -55,7 +60,10 @@ public class UserActivity implements Parcelable { public UserActivity(XmlReader reader) { reader.readString("remoteId", this::setRemoteId); reader.readString("message", this::setMessage); - reader.readString("picture", this::setPicture); + reader.readString("picture", p -> { + setPicture(p); + convertPictureUri(); + }); reader.readString("target_id", this::setTargetId); reader.readLong("created_at", this::setCreated); } @@ -70,28 +78,6 @@ public class UserActivity implements Parcelable { 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() { return id; } @@ -120,6 +106,10 @@ public class UserActivity implements Parcelable { return picture; } + public void setPicture(Uri uri) { + picture = uri == null ? null : uri.toString(); + } + public void setPicture(String picture) { this.picture = picture; } @@ -141,7 +131,33 @@ public class UserActivity implements Parcelable { } 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 diff --git a/app/src/main/java/org/tasks/data/UserActivityDao.java b/app/src/main/java/org/tasks/data/UserActivityDao.java index 9e7803585..47cb3944d 100644 --- a/app/src/main/java/org/tasks/data/UserActivityDao.java +++ b/app/src/main/java/org/tasks/data/UserActivityDao.java @@ -3,6 +3,8 @@ package org.tasks.data; import androidx.room.Dao; import androidx.room.Insert; import androidx.room.Query; +import androidx.room.Update; + import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.helper.UUIDHelper; @@ -14,9 +16,15 @@ public abstract class UserActivityDao { @Insert 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 ") public abstract List getCommentsForTask(String taskUuid); + @Query("SELECT * FROM userActivity") + public abstract List getComments(); + public void createNew(UserActivity item) { if (item.getCreated() == null || item.getCreated() == 0L) { item.setCreated(DateUtilities.now()); diff --git a/app/src/main/java/org/tasks/dialogs/AddAttachmentDialog.java b/app/src/main/java/org/tasks/dialogs/AddAttachmentDialog.java index fab9035eb..34f80c0bd 100644 --- a/app/src/main/java/org/tasks/dialogs/AddAttachmentDialog.java +++ b/app/src/main/java/org/tasks/dialogs/AddAttachmentDialog.java @@ -1,26 +1,31 @@ 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.content.Context; import android.content.Intent; import android.os.Bundle; import android.provider.MediaStore.Images.Media; -import androidx.annotation.NonNull; + import com.todoroo.astrid.files.FilesControlSet; -import java.util.List; -import javax.inject.Inject; + import org.tasks.R; import org.tasks.activities.CameraActivity; -import org.tasks.files.FileExplore; import org.tasks.injection.DialogFragmentComponent; import org.tasks.injection.ForActivity; import org.tasks.injection.InjectingDialogFragment; import org.tasks.preferences.Device; 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 { 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_AUDIO = 12123; - public static final String EXTRA_PATH = "extra_path"; - public static final String EXTRA_TYPE = "extra_type"; - @Inject @ForActivity Context context; @Inject DialogBuilder dialogBuilder; @Inject Device device; @@ -88,7 +90,7 @@ public class AddAttachmentDialog extends InjectingDialogFragment { } } - public void pickFromStorage() { - getTargetFragment().startActivityForResult(new Intent(context, FileExplore.class), REQUEST_STORAGE); + private void pickFromStorage() { + getTargetFragment().startActivityForResult(newFilePickerIntent(getActivity(), null), REQUEST_STORAGE); } } diff --git a/app/src/main/java/org/tasks/dialogs/RecordAudioDialog.java b/app/src/main/java/org/tasks/dialogs/RecordAudioDialog.java index de1eede0f..b302712ae 100644 --- a/app/src/main/java/org/tasks/dialogs/RecordAudioDialog.java +++ b/app/src/main/java/org/tasks/dialogs/RecordAudioDialog.java @@ -2,13 +2,12 @@ package org.tasks.dialogs; import static android.app.Activity.RESULT_OK; 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 androidx.lifecycle.ViewModelProviders; import android.content.DialogInterface; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; @@ -22,7 +21,6 @@ import com.todoroo.astrid.files.FilesControlSet; import com.todoroo.astrid.voice.AACRecorder; import javax.inject.Inject; import org.tasks.R; -import org.tasks.data.TaskAttachment; import org.tasks.injection.DialogFragmentComponent; import org.tasks.injection.InjectingDialogFragment; import org.tasks.preferences.FragmentPermissionRequestor; @@ -31,6 +29,8 @@ import org.tasks.preferences.PermissionRequestor; import org.tasks.preferences.Preferences; import org.tasks.themes.Theme; +import java.io.IOException; + public class RecordAudioDialog extends InjectingDialogFragment implements AACRecorder.AACRecorderCallbacks { @@ -45,7 +45,7 @@ public class RecordAudioDialog extends InjectingDialogFragment private AACRecorder recorder; - public static RecordAudioDialog newRecordAudioDialog(FilesControlSet target, int requestCode) { + static RecordAudioDialog newRecordAudioDialog(FilesControlSet target, int requestCode) { RecordAudioDialog dialog = new RecordAudioDialog(); dialog.setTargetFragment(target, requestCode); return dialog; @@ -75,9 +75,13 @@ public class RecordAudioDialog extends InjectingDialogFragment } private void startRecording() { - recorder.startRecording(); - timer.setBase(recorder.getBase()); - timer.start(); + try { + recorder.startRecording(getContext()); + timer.setBase(recorder.getBase()); + timer.start(); + } catch (IOException e) { + stopRecording(); + } } @Override @@ -99,11 +103,9 @@ public class RecordAudioDialog extends InjectingDialogFragment } @Override - public void encodingFinished(String path) { - final String extension = path.substring(path.lastIndexOf('.') + 1); + public void encodingFinished(Uri uri) { Intent intent = new Intent(); - intent.putExtra(EXTRA_PATH, path); - intent.putExtra(EXTRA_TYPE, TaskAttachment.FILE_TYPE_AUDIO + extension); + intent.setData(uri); Fragment target = getTargetFragment(); if (target != null) { target.onActivityResult(getTargetRequestCode(), RESULT_OK, intent); diff --git a/app/src/main/java/org/tasks/files/FileExplore.java b/app/src/main/java/org/tasks/files/FileExplore.java index b83775677..4589d1d72 100644 --- a/app/src/main/java/org/tasks/files/FileExplore.java +++ b/app/src/main/java/org/tasks/files/FileExplore.java @@ -1,30 +1,23 @@ package org.tasks.files; -import static org.tasks.PermissionUtil.verifyPermissions; - import android.app.Activity; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.os.Environment; -import androidx.annotation.NonNull; + import com.google.common.base.Strings; import com.nononsenseapps.filepicker.FilePickerActivity; -import java.io.File; -import javax.inject.Inject; + import org.tasks.injection.ActivityComponent; 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 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_DIRECTORY_MODE = "extra_directory_mode"; // $NON-NLS-1$ private static final int REQUEST_PICKER = 1000; - @Inject ActivityPermissionRequestor permissionRequestor; private boolean directoryMode; private String startPath; @@ -38,9 +31,7 @@ public class FileExplore extends InjectingAppCompatActivity { directoryMode = intent.getBooleanExtra(EXTRA_DIRECTORY_MODE, false); startPath = intent.getStringExtra(EXTRA_START_PATH); - if (permissionRequestor.requestFileWritePermission()) { - launchPicker(); - } + launchPicker(); } } @@ -71,29 +62,12 @@ public class FileExplore extends InjectingAppCompatActivity { 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 protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_PICKER) { if (resultCode == Activity.RESULT_OK) { - Uri uri = data.getData(); - File file = com.nononsenseapps.filepicker.Utils.getFileForUri(uri); Intent intent = new Intent(); - intent.putExtra(directoryMode ? EXTRA_DIRECTORY : EXTRA_FILE, file.getAbsolutePath()); - intent.setData(uri); + intent.setData(data.getData()); setResult(Activity.RESULT_OK, intent); } finish(); diff --git a/app/src/main/java/org/tasks/files/FileHelper.java b/app/src/main/java/org/tasks/files/FileHelper.java index e0f81a020..271712a50 100644 --- a/app/src/main/java/org/tasks/files/FileHelper.java +++ b/app/src/main/java/org/tasks/files/FileHelper.java @@ -1,38 +1,43 @@ package org.tasks.files; import android.app.Activity; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; 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 org.tasks.preferences.BasicPreferences; -import org.tasks.preferences.Preferences; +import org.tasks.R; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; import java.util.List; import javax.annotation.Nullable; -import androidx.core.content.FileProvider; 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.atLeastLollipop; 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()) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); @@ -47,7 +52,7 @@ public class FileHelper { intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); } } - activity.startActivityForResult(intent, rc); + return intent; } else { Intent intent = new Intent(activity, FileExplore.class); if (initial != null) { @@ -55,7 +60,7 @@ public class FileHelper { FileExplore.EXTRA_START_PATH, new File(initial.getPath())); } - activity.startActivityForResult(intent, rc); + return intent; } } @@ -65,7 +70,8 @@ public class FileHelper { intent.addFlags( Intent.FLAG_GRANT_PERSISTABLE_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); activity.startActivityForResult(intent, rc); } else { @@ -80,65 +86,78 @@ public class FileHelper { } } - public static InputStream fromUri(Context context, Uri uri) { - try { - switch (uri.getScheme()) { - case "content": - return context.getContentResolver().openInputStream(uri); - case "file": - return new FileInputStream(new File(uri.getPath())); - default: - throw new IllegalArgumentException("Unhandled scheme: " + uri.getScheme()); - } - } catch (FileNotFoundException e) { - Timber.e(e); - return null; + public static void delete(Context context, Uri uri) { + if (uri == null) { + return; } - } - public static String getPathFromUri(Activity activity, Uri uri) { - String[] projection = {MediaStore.Images.Media.DATA}; - Cursor cursor = activity.managedQuery(uri, projection, null, null, null); + switch (uri.getScheme()) { + case "content": + DocumentFile documentFile = DocumentFile.fromSingleUri(context, uri); + documentFile.delete(); + break; + case "file": + new File(uri.getPath()).delete(); + break; + } + } - if (cursor != null) { - int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); - cursor.moveToFirst(); - return cursor.getString(column_index); - } else { - return uri.getPath(); + public static String getFilename(Context context, Uri uri) { + switch (uri.getScheme()) { + case ContentResolver.SCHEME_FILE: + return uri.getLastPathSegment(); + case ContentResolver.SCHEME_CONTENT: + 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); - Uri uri = - FileProvider.getUriForFile(context, Constants.FILE_PROVIDER_AUTHORITY, new File(path)); - intent.setDataAndType(uri, type); - grantReadPermissions(context, intent, uri); - return intent; + if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { + uri = copyToUri(context, Uri.fromFile(context.getCacheDir()), uri); + } + Uri share = getUriForFile(context, Constants.FILE_PROVIDER_AUTHORITY, new File(uri.getPath())); + 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()) { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } else { - if (atLeastLollipop()) { - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - } else { - List resolveInfoList = - context - .getPackageManager() - .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resolveInfoList) { - context.grantUriPermission( - resolveInfo.activityInfo.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - } + List resolveInfoList = + context + .getPackageManager() + .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + 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 { + String filename = getNonCollidingFileName(context, destination, baseName, extension); switch (destination.getScheme()) { case "content": DocumentFile tree = DocumentFile.fromTreeUri(context, destination); @@ -161,4 +180,56 @@ public class FileHelper { 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 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; + } } diff --git a/app/src/main/java/org/tasks/files/ImageHelper.java b/app/src/main/java/org/tasks/files/ImageHelper.java index 462badc2b..ff063a71e 100644 --- a/app/src/main/java/org/tasks/files/ImageHelper.java +++ b/app/src/main/java/org/tasks/files/ImageHelper.java @@ -1,7 +1,16 @@ package org.tasks.files; +import android.content.ContentResolver; +import android.content.Context; import android.graphics.Bitmap; 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 { @@ -27,17 +36,45 @@ public class ImageHelper { 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 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(path, options); + BitmapFactory.decodeStream(inputStream, null, options); // Calculate inSampleSize options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + // Decode bitmap with inSampleSize set 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); + } + } } } diff --git a/app/src/main/java/org/tasks/fragments/CommentBarFragment.java b/app/src/main/java/org/tasks/fragments/CommentBarFragment.java index 093897d6e..cbbd054b2 100644 --- a/app/src/main/java/org/tasks/fragments/CommentBarFragment.java +++ b/app/src/main/java/org/tasks/fragments/CommentBarFragment.java @@ -1,8 +1,5 @@ package org.tasks.fragments; -import static org.tasks.files.FileHelper.getPathFromUri; -import static org.tasks.files.ImageHelper.sampleBitmap; - import android.app.Activity; import android.content.DialogInterface; import android.content.Intent; @@ -10,9 +7,6 @@ import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; 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.util.TypedValue; import android.view.KeyEvent; @@ -23,19 +17,11 @@ import android.view.inputmethod.EditorInfo; import android.widget.EditText; import android.widget.ImageView; 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.todoroo.andlib.utility.AndroidUtilities; 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.activities.CameraActivity; import org.tasks.dialogs.DialogBuilder; @@ -43,7 +29,22 @@ import org.tasks.injection.FragmentComponent; import org.tasks.preferences.Device; import org.tasks.preferences.Preferences; 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 { @@ -71,17 +72,6 @@ public class CommentBarFragment extends TaskEditControlFragment { private CommentBarFragmentCallback callback; 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 public void onAttach(Activity activity) { super.onAttach(activity); @@ -191,7 +181,7 @@ public class CommentBarFragment extends TaskEditControlFragment { public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE_CAMERA) { if (resultCode == Activity.RESULT_OK) { - pendingCommentPicture = data.getParcelableExtra(CameraActivity.EXTRA_URI); + pendingCommentPicture = data.getData(); setPictureButtonToPendingPicture(); commentField.requestFocus(); } @@ -206,10 +196,12 @@ public class CommentBarFragment extends TaskEditControlFragment { } private void setPictureButtonToPendingPicture() { - String path = getPathFromUri(activity, pendingCommentPicture); Bitmap bitmap = sampleBitmap( - path, pictureButton.getLayoutParams().width, pictureButton.getLayoutParams().height); + activity, + pendingCommentPicture, + pictureButton.getLayoutParams().width, + pictureButton.getLayoutParams().height); pictureButton.setImageBitmap(bitmap); commentButton.setVisibility(View.VISIBLE); } @@ -219,13 +211,7 @@ public class CommentBarFragment extends TaskEditControlFragment { if (TextUtils.isEmpty(message)) { message = " "; } - String picture = null; - if (pendingCommentPicture != null) { - JSONObject pictureJson = savePictureJson(pendingCommentPicture); - if (pictureJson != null) { - picture = pictureJson.toString(); - } - } + Uri picture = pendingCommentPicture; if (commentField != null) { commentField.setText(""); // $NON-NLS-1$ @@ -281,7 +267,7 @@ public class CommentBarFragment extends TaskEditControlFragment { public interface CommentBarFragmentCallback { - void addComment(String message, String picture); + void addComment(String message, Uri picture); } interface ClearImageCallback { diff --git a/app/src/main/java/org/tasks/preferences/BasicPreferences.java b/app/src/main/java/org/tasks/preferences/BasicPreferences.java index 5821279bb..11d4f5813 100644 --- a/app/src/main/java/org/tasks/preferences/BasicPreferences.java +++ b/app/src/main/java/org/tasks/preferences/BasicPreferences.java @@ -42,6 +42,7 @@ import static com.todoroo.andlib.utility.AndroidUtilities.atLeastKitKat; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; import static org.tasks.dialogs.ExportTasksDialog.newExportTasksDialog; import static org.tasks.dialogs.ImportTasksDialog.newImportTasksDialog; +import static org.tasks.files.FileHelper.newFilePickerIntent; import static org.tasks.locale.LocalePickerDialog.newLocalePickerDialog; import static org.tasks.themes.ThemeColor.LAUNCHERS; @@ -154,7 +155,7 @@ public class BasicPreferences extends InjectingPreferenceActivity findPreference(R.string.backup_BAc_import) .setOnPreferenceClickListener( preference -> { - FileHelper.newFilePicker(BasicPreferences.this, REQUEST_PICKER, preferences.getBackupDirectory()); + startActivityForResult(newFilePickerIntent(BasicPreferences.this, preferences.getBackupDirectory()), REQUEST_PICKER); return false; }); @@ -235,14 +236,14 @@ public class BasicPreferences extends InjectingPreferenceActivity } } else if (requestCode == REQUEST_CODE_BACKUP_DIR) { if (resultCode == RESULT_OK && data != null) { - Uri dir = data.getData(); + Uri uri = data.getData(); if (atLeastLollipop()) { getContentResolver() .takePersistableUriPermission( - dir, + uri, 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(); } } else if (requestCode == REQUEST_PICKER) { diff --git a/app/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java b/app/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java index a2fed5c55..d602c3e6f 100644 --- a/app/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java +++ b/app/src/main/java/org/tasks/preferences/MiscellaneousPreferences.java @@ -1,8 +1,10 @@ package org.tasks.preferences; +import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; import static org.tasks.PermissionUtil.verifyPermissions; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.speech.tts.TextToSpeech; @@ -12,6 +14,7 @@ import java.io.File; import javax.inject.Inject; import org.tasks.R; import org.tasks.files.FileExplore; +import org.tasks.files.FileHelper; import org.tasks.injection.ActivityComponent; import org.tasks.injection.InjectingPreferenceActivity; import org.tasks.scheduling.CalendarNotificationIntentService; @@ -45,31 +48,37 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity { @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_CODE_FILES_DIR && resultCode == RESULT_OK) { - if (data != null) { - String dir = data.getStringExtra(FileExplore.EXTRA_DIRECTORY); - preferences.setString(R.string.p_attachment_dir, dir); + if (requestCode == REQUEST_CODE_FILES_DIR) { + if (resultCode == RESULT_OK) { + Uri uri = data.getData(); + 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(); } - return; - } - try { - if (requestCode == REQUEST_CODE_TTS_CHECK) { - if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) { - // success, create the TTS instance - voiceOutputAssistant.initTTS(); - } else { - // missing data, install it - Intent installIntent = new Intent(); - installIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA); - startActivity(installIntent); + } else { + try { + if (requestCode == REQUEST_CODE_TTS_CHECK) { + if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) { + // success, create the TTS instance + voiceOutputAssistant.initTTS(); + } else { + // missing data, install it + Intent installIntent = new Intent(); + installIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA); + startActivity(installIntent); + } } + } catch (VerifyError e) { + // unavailable + Timber.e(e); } - } catch (VerifyError e) { - // unavailable - Timber.e(e); + super.onActivityResult(requestCode, resultCode, data); } - super.onActivityResult(requestCode, resultCode, data); } @Override @@ -83,10 +92,8 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity { findPreference(getString(R.string.p_attachment_dir)) .setOnPreferenceClickListener( p -> { - Intent filesDir = new Intent(MiscellaneousPreferences.this, FileExplore.class); - filesDir.putExtra(FileExplore.EXTRA_DIRECTORY_MODE, true); - startActivityForResult(filesDir, REQUEST_CODE_FILES_DIR); - return true; + FileHelper.newDirectoryPicker(this, REQUEST_CODE_FILES_DIR, preferences.getAttachmentsDirectory()); + return false; }); updateAttachmentDirectory(); } @@ -96,8 +103,10 @@ public class MiscellaneousPreferences extends InjectingPreferenceActivity { } private String getAttachmentDirectory() { - File dir = preferences.getAttachmentsDirectory(); - return dir == null ? "" : dir.getAbsolutePath(); + Uri uri = preferences.getAttachmentsDirectory(); + return uri.getScheme().equals("file") + ? new File(uri.getPath()).getAbsolutePath() + : uri.toString(); } private void initializeCalendarReminderPreference() { diff --git a/app/src/main/java/org/tasks/preferences/PermissionChecker.java b/app/src/main/java/org/tasks/preferences/PermissionChecker.java index 423156951..5d213171d 100644 --- a/app/src/main/java/org/tasks/preferences/PermissionChecker.java +++ b/app/src/main/java/org/tasks/preferences/PermissionChecker.java @@ -27,10 +27,6 @@ public class PermissionChecker { asList(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR)); } - public boolean canWriteToExternalStorage() { - return checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); - } - public boolean canAccessAccounts() { return atLeastOreo() || checkPermission(Manifest.permission.GET_ACCOUNTS); } diff --git a/app/src/main/java/org/tasks/preferences/PermissionRequestor.java b/app/src/main/java/org/tasks/preferences/PermissionRequestor.java index 8ad8e58dd..3029e1db2 100644 --- a/app/src/main/java/org/tasks/preferences/PermissionRequestor.java +++ b/app/src/main/java/org/tasks/preferences/PermissionRequestor.java @@ -4,7 +4,6 @@ import android.Manifest; public abstract class PermissionRequestor { - public static final int REQUEST_FILE_WRITE = 50; public static final int REQUEST_CALENDAR = 51; public static final int REQUEST_MIC = 52; public static final int REQUEST_GOOGLE_ACCOUNTS = 53; @@ -24,14 +23,6 @@ public abstract class PermissionRequestor { return false; } - public boolean requestFileWritePermission() { - if (permissionChecker.canWriteToExternalStorage()) { - return true; - } - requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, REQUEST_FILE_WRITE); - return false; - } - public boolean requestCalendarPermissions() { return requestCalendarPermissions(REQUEST_CALENDAR); } diff --git a/app/src/main/java/org/tasks/preferences/Preferences.java b/app/src/main/java/org/tasks/preferences/Preferences.java index 9b96d3998..3b027588f 100644 --- a/app/src/main/java/org/tasks/preferences/Preferences.java +++ b/app/src/main/java/org/tasks/preferences/Preferences.java @@ -28,7 +28,6 @@ import org.tasks.time.DateTime; import java.io.File; import java.util.Collection; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; @@ -49,31 +48,17 @@ public class Preferences { private static final String PREF_SORT_SORT = "sort_sort"; // $NON-NLS-1$ private final Context context; - private final PermissionChecker permissionChecker; private final SharedPreferences prefs; private final SharedPreferences publicPrefs; @Inject - public Preferences(@ForApplication Context context, PermissionChecker permissionChecker) { + public Preferences(@ForApplication Context context) { this.context = context; - this.permissionChecker = permissionChecker; prefs = PreferenceManager.getDefaultSharedPreferences(context); publicPrefs = 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() { return getBoolean(R.string.p_back_button_saves_task, false); } @@ -389,18 +374,34 @@ public class Preferences { } } - public File getAttachmentsDirectory() { - File directory = null; - String customDir = getStringValue(R.string.p_attachment_dir); - if (permissionChecker.canWriteToExternalStorage() && !TextUtils.isEmpty(customDir)) { - directory = new File(customDir); + public Uri getAttachmentsDirectory() { + Uri uri = getUri(R.string.p_attachment_dir); + if (uri != null) { + switch (uri.getScheme()) { + 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()) { - directory = getDefaultFileLocation(TaskAttachment.FILES_DIRECTORY_DEFAULT); + if (atLeastKitKat()) { + 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) { @@ -413,20 +414,12 @@ public class Preferences { return file.isDirectory() || file.mkdirs() ? file : null; } - public String getNewAudioAttachmentPath(AtomicReference nameReference) { - return getNewAttachmentPath(".m4a", nameReference); // $NON-NLS-1$ - } - - public String getNewAttachmentPath(String extension, AtomicReference nameReference) { - String dir = getAttachmentsDirectory().getAbsolutePath(); - - String name = getNonCollidingFileName(dir, new DateTime().toString("yyyyMMddHHmm"), extension); - - if (nameReference != null) { - nameReference.set(name); + public Uri getCacheDirectory() { + if (atLeastKitKat()) { + return DocumentFile.fromFile(context.getCacheDir()).getUri(); + } else { + return Uri.fromFile(context.getCacheDir()); } - - return dir + File.separator + name; } public Uri getBackupDirectory() { diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml index 344de3486..efb7bf97c 100644 --- a/app/src/main/res/values-bg-rBG/strings.xml +++ b/app/src/main/res/values-bg-rBG/strings.xml @@ -168,8 +168,6 @@ Сигурни ли сте? Не може да бъде отменено Записване на Аудио Спри Записването - Съжаляваме! Не е намерено приложение, което да работи с този тип файл. - Грешка при копиране на файла за прикачване Звънене веднъж Звънене пет пъти Звънене без спиране @@ -289,7 +287,6 @@ Tasks ще изтоваря имената на задачите по време на напомняния за задача Изтрий задача Добавена задача - Не може да достъпвате до външната памет 1 задача %d задачи diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 0c1c977e5..a21b85b6d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -144,8 +144,6 @@ Skutečně? Nebude cesty zpět Nahrávám zvuk Ukončit záznam - Pro tento typ souborů nebyla nalezena žádná aplikace. - Chyba při kopírování souboru jako přílohy hodina den týden @@ -182,7 +180,6 @@ Hlasové upomínky Smazat úkol Přidán úkol - Není přístup k externí databázi 1 úkol %d úkolů diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 38f34c0ff..e01f09344 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -163,8 +163,6 @@ Sind Sie sicher? Das kann nicht rückgängig gemacht werden Audio aufnehmen Aufnahme stoppen - Tut mir leid! Dieser Dateityp kann nicht geöffnet werden. - Fehler beim Kopieren der anzuhängenden Datei Einmal klingeln Fünfmal klingeln Ununterbrochen klingeln @@ -283,7 +281,6 @@ Tasks wird Aufgabennamen bei der Erinnerung aussprechen Aufgabe löschen Hinzugefügte Aufgabe - Kein Zugriff auf externen Speicher 1 Aufgabe %d Aufgaben diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index be9f120e8..aa41a2e46 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -128,8 +128,6 @@ Είστε σίγουρος; Δεν μπορεί να ακυρωθεί Εγγραφή Ήχου Σταμάτημα εγγραφής - Συγγνώμη! Δεν βρέθηκε εφαρμογή που να χειρίζεται τέτοιο τύπο αρχείου - Σφάλμα αντιγραφής αρχείου προς επισύναψη μια ώρα μια μέρα μια εβδομάδα @@ -165,7 +163,6 @@ Η εφαρμογή θα λέει να ονόματα των εργασιών κατά την διάρκεια των υπενθυμίσεων Διαγραφή καθήκοντος Η εργασία προστέθηκε - Δεν είναι δυνατή η πρόσβαση σε εξωτερικά μέσα αποθήκευσης Σήμερα Αύριο Χθές diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 20afbf37f..65e94195b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -164,8 +164,6 @@ Está seguro? No se puede deshacer Grabando Audio Detener grabación - Lo sentimos! No se encontró ninguna aplicación para abrir este tipo de archivo. - Error al copiar el archivo a adjuntar Sonar una vez Sonar cinco veces Sonar sin parar @@ -285,7 +283,6 @@ Tasks dirá los nombres de las tareas durante los avisos Eliminar tarea Tarea agregada - Almacenamiento externo inaccesible 1 tarea %d tareas diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 349d62b1b..5652a2b92 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -113,8 +113,6 @@ آیا مطمن هستید؟ قادر به برگرداندن نخواهید بود ضبط صدا توقف ضبط - پوزش! هیچ برنامه ای برای بازکردن این فایل پیدا نشد. - خطای کپی فایل برای ضمیمه یک بار زنگ بزن پنج بار زنگ بزن بدون توقف زنگ بزن @@ -177,7 +175,6 @@ یادآور صوتی حذف وظیفه وظیفه اضافه شده - عدم امکان دسترسی به حافظه خارجی ۱ وظیفه %d tasks diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index a6700c1db..b73abad3b 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -156,8 +156,6 @@ Oletko varma? Ei voi peruuttaa Tallentaa ääntä Lopeta tallennus - Pahoittelen! Ei sopivaa sovelllusta tämän tiedostotyypin käsittelyyn. - Virhe tiedoston kopioimisessa liitteeseen Soi kerran Soi viisi kertaa Soi jatkuvasti @@ -276,7 +274,6 @@ Tasks kertoo tehtävien nimet muistutusten aikana Poista tehtävä Lisätty tehtävä - Eii pääsyä ulkoiseen muistiin 1 tehtävä %d tehtävät diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index afcb8ddc9..598da50b6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -162,8 +162,6 @@ Êtes-vous certain(e)? Cette opération est irréversible Enregistrement Audio Arrêter l\'enregistrement - Désolé ! Aucune application n\'a été trouvé pour gérer ce type de fichier. - Erreur lors de la copie du fichier à joindre Sonner une fois Sonner cinq fois Sonner en continu @@ -275,7 +273,6 @@ Tasks donnera le nom de la tâche Supprimer la tâche ? Tâche ajoutée - Impossible d\'accéder au stockage externe 1 tâche %d tâches diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 8a064489a..09a42fddb 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -152,8 +152,6 @@ Está seguro? No se puede deshacer Grabando Audio Detener grabación - Lo sentimos! No se encontró ninguna aplicación para abrir este tipo de archivo. - Error al copiar el archivo a adjuntar Sonar una vez Sonar cinco veces Sonar sin parar @@ -198,7 +196,6 @@ Tasks dirá los nombres de las tareas durante los avisos Eliminar tarefa Tarea agregada - Almacenamiento externo inaccesible 1 tarea %d tareas diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 9fe9adc8b..606f732aa 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -168,8 +168,6 @@ Biztos benne? A művelet nem visszavonható Hang rögzítése Hangrögzítés leállítása - Sajnáljuk, az adott file megnyitására képes alkalmazás nem található. - Hiba lépett fel a file csatolásakor Egy csengés Öt csengés Folyamatos csengés @@ -289,7 +287,6 @@ A Tasks kimondja a feladatcímeket emlékeztetőkor Feladat törlése Hozzáadott feladat - Külső tárhely nem elérhető 1 feladat %d feladat diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b9c873ce1..55aad73a3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -166,8 +166,6 @@ Se sicuro? Non può essere eseguito Registrazione Audio Fine Registrazione - Spiacente! Non è stata trovata nessuna applicazione per gestire questo tipo di file - Errore di copia dei file da allegare Suona una volta Suona cinque volte Suono continuo @@ -286,7 +284,6 @@ Tasks pronuncerà il nome dell\'attività durante i promemoria Elimina attività Attività aggiunta - Non posso accedere alla memoria esterna 1 attività %d attività diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index e524eca7a..f4f81a8e9 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -160,8 +160,6 @@ בטוח? לא ניתן לבטל את הפעולה מקליטה שֵׁמַע הפסק הקלטה - \"מצטערת! לא מצאתי ישום שיכול לטפל בקבצים מסוג זה\" - שגיאה בהעתקת הקובץ המצורף צלצל פעם אחת צלצל חמש פעמים צלצל ללא הפסקה @@ -281,7 +279,6 @@ אסטריד תאמר את שם המשימה כחלק מהתזכורת מחק משימה משימות שנוצרו - לא ניתן לגשת לאחסון חיצוני משימה אחת %d משימות diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c15b3f369..369979e5c 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -160,8 +160,6 @@ よろしいですか? 取り消しできません 音声を録音中 録音を停止 - 申し訳ありません! このファイル形式を扱うアプリケーションがありません. - ファイル添付のコピー中にエラー 1回通知音を鳴らす 5回通知音を鳴らす 通知音を鳴らし続ける @@ -281,7 +279,6 @@ Tasks はタスクリマインダーでタスク名を話します タスクを削除 追加されたタスク - 外部メモリーにアクセスできません タスク 1 件 タスク %d 件 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 3211f25d1..a86f9bc95 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -162,8 +162,6 @@ 정말입니까? 되돌릴 수 없습니다 오디오 녹음 중 녹음 중단 - 죄송하지만 이 파일 형식을 다룰 수 있는 프로그램이 없습니다. - 첨부용 파일 복사 에러 한번 울림 다섯번 울림 계속 울림 @@ -283,7 +281,6 @@ 할일 알림 시 할일 제목을 소리내어 읽어줍니다 할일 지우기 추가된 할일 - 외부 저장소에 접근할 수 없음 1 할일 %d 할일 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index ba98aac8d..93a92dd48 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -164,8 +164,6 @@ Ar tikrai? Nebegalės būti atstatyta Įrašyti garsą Stabdyti įrašą - Atsiprašome! Nerasta programa, galinti atidaryti šio tipo failus. - Klaida bandant pridėti failą Suskambėti vieną kartą Suskambėti penkis kartus Skambėti nepaliaujant @@ -285,7 +283,6 @@ Tasks garsu praneš užduoties pavadinimą per priminimus Ištrinti užduotį Pridėtos užduotys - Nėra prieigos prie išorinės laikmenos 1 užduotis %d užduotys(-čių) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3d75a70e4..abd5e6124 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -164,8 +164,6 @@ Weet je het zeker? Dit kan niet ongedaan gemaakt worden Bezig met opname Opname stoppen - Sorry! Er is geen applicatie gevonden die dit bestandtype ondersteunt. - Fout bij kopiëren toe te voegen bestand Ring eenmalig Ring vijf keer Ring continue @@ -284,7 +282,6 @@ Bij herinneringen zullen de taaknamen uitgesproken worden Verwijder taak Toegevoegde taak - Geen toegang tot externe opslag 1 taak %d taken diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 96df4a3ed..63fdf7801 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -159,8 +159,6 @@ Jesteś pewny? Tych zmian nie można odwrócić Nagrywanie dźwięku Zakończ nagrywanie - Przepraszamy! Nie znaleziono aplikacji do obsługi tego typu pliku. - Błąd kopiowania pliku do załącznika Dzwoń raz Dzwoń pięć razy Dzwoń nonstop @@ -278,7 +276,6 @@ Tasks będzie mówił nazwę zadania podczas przypomnienia Usuń zadanie Dodane zadanie - Brak dostępu do pamięci zewnętrznej 1 zadanie %d zadań diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 8c0343449..ae8ec23ac 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -166,8 +166,6 @@ Você tem certeza? Não pode ser desfeito. Gravando Áudio Parar Gravação - Desculpa! Nenhuma aplicação para manipular este tipo de arquivo foi encontrada. - Erro ao copiar o arquivo para o anexo Tocar uma vez Tocar cinco vezes Tocar continuamente @@ -287,7 +285,6 @@ Tasks irá falar o nome das tarefas durante os lembretes Excluir tarefa Tarefa adicionada - Não é possível acessar armazenamento externo 1 tarefa %d tarefas diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 2865100be..8feb40dd8 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -154,8 +154,6 @@ Tem a certeza? A ação não pode ser anulada! Gravação áudio Parar gravação - Não foi encontrada qualquer aplicação para gerir ficheiros deste tipo. - Erro ao copiar o ficheiro como anexo Tocar uma vez Tocar 5 vezes Tocar incessantemente @@ -273,7 +271,6 @@ O Tasks irá reproduzir o nome da tarefa durante os lembretes Eliminar tarefa Tarefa adicionada - Não foi possível aceder ao disco externo 1 tarefa %d tarefas diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ac241d4d4..6fd72756e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -158,8 +158,6 @@ Вы уверены? Действие нельзя отменить Запись голоса Остановить запись - Извините! Не найдена программа для просмотра файлов этого типа. - Ошибка копирования прикрепляемого файла. 1 раз 5 раз Пока не выкл. @@ -279,7 +277,6 @@ Tasks должен произносить название задач во время напоминаний Удалить задачу Добавленная задача - Нет доступа к внешнему хранилищу 1 задача %d задач(а/и) diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 14b74ef76..e47229f1b 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -153,8 +153,6 @@ Naozaj? Nedá sa vrátiť Nahrávanie zvuku Zastaviť nahrávanie - Pre tento typ súboru nebola nájdená žiadna aplikácia - Chyba pri kopírovaní súboru do prílohy Zvoniť raz Zvoniť päť krát Zvoniť neustále @@ -273,7 +271,6 @@ Úlohy vyslovia názov úlohy počas pripomineky Zmazať úlohu Pridaná úloha - Nemožno získať prístup k externej pamäti 1 úloha %d úlohy diff --git a/app/src/main/res/values-sl-rSI/strings.xml b/app/src/main/res/values-sl-rSI/strings.xml index 8824867ae..07121507b 100644 --- a/app/src/main/res/values-sl-rSI/strings.xml +++ b/app/src/main/res/values-sl-rSI/strings.xml @@ -131,8 +131,6 @@ Ste prepričani? Tega ni mogoče razveljaviti Snemam zvok Prekini snemanje - Žal nobena aplikacija ne ustreza takim datotekam. - Napaka pri kopiranju datoteke za priponko na uro dan na teden diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 0cc08e989..c1f077b58 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -153,8 +153,6 @@ Är du säker? Detta kan inte ångras Spelar in ljud Avsluta inspelning - Tyvärr hittades ingen applikation för att hantera den här filtypen - Ett fel uppstod vid kopiering av filen till bilaga Ring en gång Ring fem gånger Ring konstant @@ -198,7 +196,6 @@ Tasks läser upp uppgifterna vid påminnelse Radera uppgift Uppgift skapad - Kan inte komma åt externt lagringsutrymme 1 uppgift %d uppgifter diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a27c3e668..d6afa18e2 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -170,8 +170,6 @@ Emin misiniz? Geri döndürülemez Ses Kaydediliyor Kaydı Durdur - Üzgünüm! Bu dosya türünü destekleyen bir uygulama bulunamadı. - Dosyanın ek olarak kopyalanmasında hata Bir kez çal 5 kez çal Durmadan çal @@ -291,7 +289,6 @@ Tasks, görev adlarını görev hatırlatmaları sırasında söyleyecek Görevi sil Görev eklendi - Dış depolamaya ulaşılamadı. 1 görev %d görev diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index ff3aa515b..95873539e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -155,8 +155,6 @@ Ви впевнені? Це не може бути скасовано. Запис аудіо Зупинити запис - Вибачте! Не знайдено програму для перегляду файлів цього типу. - Помилка копіювання файлу для вкладення Дзвеніти 1 раз Дзвеніти 5 раз Дзвеніти безперервно @@ -201,7 +199,6 @@ Tasks повинен вимовляти назву завдань під час нагадувань Видалити завдання Додане завдання - Немає доступу до зовнішнього сховища 1 завдання %d завдань diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 9c70f6cc2..097fbd906 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -159,8 +159,6 @@ 您确定吗?无法恢复的喔 正在录制音频 停止录制 - 对不起!找不到应用程序处理这种文件类型。 - 复制文件添加附件时出错 响铃一次 响铃五次 响个不停 @@ -280,7 +278,6 @@ Tasks会在任务提醒时读出任务名 删除任务 已添加的任务 - 无法访问外部存储 1 个任务 %d 个任务 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c28ab78f7..4dae38f6a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -124,8 +124,6 @@ 您確定嗎?無法恢復的喔 正在錄製音頻 停止錄製 - 對不起!找不到應用程序處理這種文件類型。 - 複製文件添加附件時出錯 響鈴一次 響鈴五次 不斷響鈴 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e25854ab4..dd4116f00 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -419,10 +419,6 @@ File %1$s contained %2$s.\n\n Recording Audio Stop Recording - Sorry! No application was found to handle this file type. - - Error copying file for attachment - Ring once @@ -612,7 +608,6 @@ File %1$s contained %2$s.\n\n Delete task Added task - Cannot access external storage diff --git a/app/src/main/res/xml/file_provider_paths.xml b/app/src/main/res/xml/file_provider_paths.xml index 8eb49a4bc..e35796903 100644 --- a/app/src/main/res/xml/file_provider_paths.xml +++ b/app/src/main/res/xml/file_provider_paths.xml @@ -1,6 +1,9 @@ - + + \ No newline at end of file