diff --git a/app/src/main/java/com/todoroo/astrid/activity/ShareLinkActivity.kt b/app/src/main/java/com/todoroo/astrid/activity/ShareLinkActivity.kt index 673465d20..c22c1bf42 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/ShareLinkActivity.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/ShareLinkActivity.kt @@ -93,7 +93,7 @@ class ShareLinkActivity : InjectingAppCompatActivity() { ?: uri.lastPathSegment } val basename = Files.getNameWithoutExtension(filename!!) - return Lists.newArrayList(FileHelper.copyToUri(context, preferences.attachmentsDirectory, uri, basename)) + return Lists.newArrayList(FileHelper.copyToUri(context, preferences.attachmentsDirectory!!, uri, basename)) } private fun copyMultipleAttachments(intent: Intent): ArrayList { @@ -101,7 +101,7 @@ class ShareLinkActivity : InjectingAppCompatActivity() { val uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) if (uris != null) { for (uri in uris) { - result.add(FileHelper.copyToUri(context, preferences.attachmentsDirectory, uri)) + result.add(FileHelper.copyToUri(context, preferences.attachmentsDirectory!!, uri)) } } return result diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt index e2fc623f7..542d15e2e 100755 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt @@ -337,7 +337,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener { val model = editViewModel.task!! val userActivity = UserActivity() if (picture != null) { - val output = FileHelper.copyToUri(context, preferences.attachmentsDirectory, picture) + val output = FileHelper.copyToUri(context, preferences.attachmentsDirectory!!, picture) userActivity.setPicture(output) } userActivity.message = message diff --git a/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.kt b/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.kt index ab353ab00..51fa868e2 100644 --- a/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.kt @@ -132,14 +132,14 @@ class FilesControlSet : TaskEditControlFragment() { } private fun copyToAttachmentDirectory(input: Uri?) { - newAttachment(FileHelper.copyToUri(context, preferences.attachmentsDirectory, input)) + newAttachment(FileHelper.copyToUri(requireContext(), preferences.attachmentsDirectory!!, input!!)) } private fun newAttachment(output: Uri) { val attachment = TaskAttachment( viewModel.task!!.uuid, output, - FileHelper.getFilename(context, output)!!) + FileHelper.getFilename(requireContext(), output)!!) lifecycleScope.launch { taskAttachmentDao.createNew(attachment) addAttachment(attachment) diff --git a/app/src/main/java/org/tasks/backup/TasksJsonExporter.kt b/app/src/main/java/org/tasks/backup/TasksJsonExporter.kt index 16c62b68a..18e742a0e 100755 --- a/app/src/main/java/org/tasks/backup/TasksJsonExporter.kt +++ b/app/src/main/java/org/tasks/backup/TasksJsonExporter.kt @@ -83,12 +83,12 @@ class TasksJsonExporter @Inject constructor( doTasksExport(os, tasks) os!!.close() val externalStorageBackup = FileHelper.newFile( - context, - preferences.backupDirectory, + context!!, + preferences.backupDirectory!!, MIME, Files.getNameWithoutExtension(filename), EXTENSION) - FileHelper.copyStream(context, internalStorageBackup, externalStorageBackup) + FileHelper.copyStream(context!!, internalStorageBackup, externalStorageBackup) workManager.scheduleDriveUpload(externalStorageBackup, exportType == ExportType.EXPORT_TYPE_SERVICE) BackupManager(context).dataChanged() } diff --git a/app/src/main/java/org/tasks/drive/DriveInvoker.kt b/app/src/main/java/org/tasks/drive/DriveInvoker.kt index 41a3c91cb..e3b1420b0 100644 --- a/app/src/main/java/org/tasks/drive/DriveInvoker.kt +++ b/app/src/main/java/org/tasks/drive/DriveInvoker.kt @@ -74,7 +74,7 @@ class DriveInvoker @Inject constructor( @Throws(IOException::class) suspend fun createFile(folderId: String, uri: Uri?) { - val mime = FileHelper.getMimeType(context, uri) + val mime = FileHelper.getMimeType(context, uri!!) val metadata = File() .setParents(listOf(folderId)) .setMimeType(mime) diff --git a/app/src/main/java/org/tasks/files/FileHelper.java b/app/src/main/java/org/tasks/files/FileHelper.java deleted file mode 100644 index 0021ea502..000000000 --- a/app/src/main/java/org/tasks/files/FileHelper.java +++ /dev/null @@ -1,288 +0,0 @@ -package org.tasks.files; - -import static android.content.ContentResolver.SCHEME_CONTENT; -import static android.provider.DocumentsContract.EXTRA_INITIAL_URI; -import static androidx.core.content.FileProvider.getUriForFile; -import static com.google.common.collect.Iterables.any; -import static com.todoroo.astrid.utility.Constants.FILE_PROVIDER_AUTHORITY; -import static org.tasks.Strings.isNullOrEmpty; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.provider.OpenableColumns; -import android.webkit.MimeTypeMap; -import android.widget.Toast; -import androidx.annotation.Nullable; -import androidx.documentfile.provider.DocumentFile; -import androidx.fragment.app.Fragment; -import com.google.common.io.ByteStreams; -import com.google.common.io.Files; -import java.io.File; -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 org.tasks.R; -import timber.log.Timber; - -public class FileHelper { - - public static final int MAX_FILENAME_LENGTH = 40; - - public static Intent newFilePickerIntent(Activity activity, Uri initial, String... mimeTypes) { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.putExtra("android.content.extra.SHOW_ADVANCED", true); - intent.putExtra("android.content.extra.FANCY", true); - intent.putExtra("android.content.extra.SHOW_FILESIZE", true); - intent.addCategory(Intent.CATEGORY_OPENABLE); - setInitialUri(activity, intent, initial); - if (mimeTypes.length == 1) { - intent.setType(mimeTypes[0]); - } else { - intent.setType("*/*"); - if (mimeTypes.length > 1) { - intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); - } - } - return intent; - } - - public static void newDirectoryPicker(Fragment fragment, int rc, @Nullable Uri initial) { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - intent.addFlags( - Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); - intent.putExtra("android.content.extra.SHOW_ADVANCED", true); - intent.putExtra("android.content.extra.FANCY", true); - intent.putExtra("android.content.extra.SHOW_FILESIZE", true); - setInitialUri(fragment.getContext(), intent, initial); - fragment.startActivityForResult(intent, rc); - } - - @TargetApi(Build.VERSION_CODES.O) - private static void setInitialUri(Context context, Intent intent, Uri uri) { - if (uri == null || !uri.getScheme().equals(SCHEME_CONTENT)) { - return; - } - - try { - intent.putExtra(EXTRA_INITIAL_URI, DocumentFile.fromTreeUri(context, uri).getUri()); - } catch (Exception e) { - Timber.e(e); - } - } - - public static void delete(Context context, Uri uri) { - if (uri == null) { - return; - } - - switch (uri.getScheme()) { - case "content": - DocumentFile documentFile = DocumentFile.fromSingleUri(context, uri); - documentFile.delete(); - break; - case "file": - delete(new File(uri.getPath())); - break; - } - } - - private static void delete(File... files) { - if (files == null) { - return; - } - - for (File file : files) { - if (file.isDirectory()) { - delete(file.listFiles()); - } else { - file.delete(); - } - } - } - - public @Nullable static String getFilename(Context context, Uri uri) { - switch (uri.getScheme()) { - case ContentResolver.SCHEME_FILE: - return uri.getLastPathSegment(); - case 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 String getExtension(Context context, Uri uri) { - if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { - String mimeType = context.getContentResolver().getType(uri); - String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); - if (!isNullOrEmpty(extension)) { - return extension; - } - } - - String extension = MimeTypeMap.getFileExtensionFromUrl(uri.getPath()); - if (!isNullOrEmpty(extension)) { - return extension; - } - - return Files.getFileExtension(getFilename(context, uri)); - } - - public static String getMimeType(Context context, Uri uri) { - String mimeType = context.getContentResolver().getType(uri); - if (!isNullOrEmpty(mimeType)) { - return mimeType; - } - String extension = getExtension(context, uri); - return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - } - - public static void startActionView(Activity context, Uri uri) { - if (uri == null) { - return; - } - - String mimeType = getMimeType(context, uri); - Intent intent = new Intent(Intent.ACTION_VIEW); - if (uri.getScheme().equals(SCHEME_CONTENT)) { - uri = copyToUri(context, Uri.fromFile(context.getExternalCacheDir()), uri); - } - Uri share = getUriForFile(context, FILE_PROVIDER_AUTHORITY, new File(uri.getPath())); - intent.setDataAndType(share, mimeType); - grantReadPermissions(intent); - 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(); - } - } - - private static void grantReadPermissions(Intent intent) { - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - - 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); - DocumentFile f1 = tree.createFile(mimeType, filename); - if (f1 == null) { - throw new FileNotFoundException("Failed to create " + filename); - } - return f1.getUri(); - case "file": - File dir = new File(destination.getPath()); - if (!dir.exists() && !dir.mkdirs()) { - throw new IOException("Failed to create %s" + dir.getAbsolutePath()); - } - File f2 = new File(dir.getAbsolutePath() + File.separator + filename); - if (f2.createNewFile()) { - return Uri.fromFile(f2); - } - throw new FileNotFoundException("Failed to create " + filename); - default: - throw new IllegalArgumentException("Unknown URI scheme: " + destination.getScheme()); - } - } - - public static Uri copyToUri(Context context, Uri destination, Uri input) { - String filename = getFilename(context, input); - return copyToUri(context, destination, input, Files.getNameWithoutExtension(filename)); - } - - public static Uri copyToUri(Context context, Uri destination, Uri input, String basename) { - try { - Uri output = - newFile( - context, - destination, - getMimeType(context, input), - basename, - getExtension(context, input)); - copyStream(context, input, output); - return output; - } catch (IOException e) { - throw new IllegalStateException(e); - } - } - - public static void copyStream(Context context, Uri input, Uri output) { - ContentResolver contentResolver = context.getContentResolver(); - try { - InputStream inputStream = contentResolver.openInputStream(input); - OutputStream outputStream = contentResolver.openOutputStream(output); - ByteStreams.copy(inputStream, outputStream); - inputStream.close(); - outputStream.close(); - } 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 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; - } - - public static String uri2String(@Nullable Uri uri) { - if (uri == null) { - return ""; - } - if (uri.getScheme().equals(ContentResolver.SCHEME_FILE)) { - return new File(uri.getPath()).getAbsolutePath(); - } - return uri.toString(); - } -} diff --git a/app/src/main/java/org/tasks/files/FileHelper.kt b/app/src/main/java/org/tasks/files/FileHelper.kt new file mode 100644 index 000000000..2262e22e0 --- /dev/null +++ b/app/src/main/java/org/tasks/files/FileHelper.kt @@ -0,0 +1,265 @@ +package org.tasks.files + +import android.annotation.TargetApi +import android.app.Activity +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.DocumentsContract +import android.provider.OpenableColumns +import android.webkit.MimeTypeMap +import android.widget.Toast +import androidx.core.content.FileProvider +import androidx.documentfile.provider.DocumentFile +import androidx.fragment.app.Fragment +import com.google.common.collect.Iterables +import com.google.common.io.ByteStreams +import com.google.common.io.Files +import com.todoroo.astrid.utility.Constants +import org.tasks.R +import org.tasks.Strings.isNullOrEmpty +import timber.log.Timber +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.util.* + +object FileHelper { + const val MAX_FILENAME_LENGTH = 40 + fun newFilePickerIntent(activity: Activity?, initial: Uri?, vararg mimeTypes: String?): Intent { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.putExtra("android.content.extra.SHOW_ADVANCED", true) + intent.putExtra("android.content.extra.FANCY", true) + intent.putExtra("android.content.extra.SHOW_FILESIZE", true) + intent.addCategory(Intent.CATEGORY_OPENABLE) + setInitialUri(activity, intent, initial) + if (mimeTypes.size == 1) { + intent.type = mimeTypes[0] + } else { + intent.type = "*/*" + if (mimeTypes.size > 1) { + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) + } + } + return intent + } + + fun newDirectoryPicker(fragment: Fragment, rc: Int, initial: Uri?) { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags( + Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + or Intent.FLAG_GRANT_READ_URI_PERMISSION + or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) + intent.putExtra("android.content.extra.SHOW_ADVANCED", true) + intent.putExtra("android.content.extra.FANCY", true) + intent.putExtra("android.content.extra.SHOW_FILESIZE", true) + setInitialUri(fragment.context, intent, initial) + fragment.startActivityForResult(intent, rc) + } + + @TargetApi(Build.VERSION_CODES.O) + private fun setInitialUri(context: Context?, intent: Intent, uri: Uri?) { + if (uri == null || uri.scheme != ContentResolver.SCHEME_CONTENT) { + return + } + try { + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, DocumentFile.fromTreeUri(context!!, uri)!!.uri) + } catch (e: Exception) { + Timber.e(e) + } + } + + fun delete(context: Context?, uri: Uri?) { + if (uri == null) { + return + } + when (uri.scheme) { + "content" -> { + val documentFile = DocumentFile.fromSingleUri(context!!, uri) + documentFile!!.delete() + } + "file" -> delete(File(uri.path)) + } + } + + private fun delete(vararg files: File) { + if (files == null) { + return + } + for (file in files) { + if (file.isDirectory) { + delete(*file.listFiles()) + } else { + file.delete() + } + } + } + + fun getFilename(context: Context, uri: Uri): String? { + when (uri.scheme) { + ContentResolver.SCHEME_FILE -> return uri.lastPathSegment + ContentResolver.SCHEME_CONTENT -> { + val cursor = context.contentResolver.query(uri, null, null, null, null) + if (cursor != null && cursor.moveToFirst()) { + return try { + cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + } finally { + cursor.close() + } + } + } + } + return null + } + + fun getExtension(context: Context, uri: Uri): String? { + if (uri.scheme == ContentResolver.SCHEME_CONTENT) { + val mimeType = context.contentResolver.getType(uri) + val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) + if (!isNullOrEmpty(extension)) { + return extension + } + } + val extension = MimeTypeMap.getFileExtensionFromUrl(uri.path) + return if (!isNullOrEmpty(extension)) { + extension + } else Files.getFileExtension(getFilename(context, uri)!!) + } + + fun getMimeType(context: Context, uri: Uri): String? { + val mimeType = context.contentResolver.getType(uri) + if (!isNullOrEmpty(mimeType)) { + return mimeType + } + val extension = getExtension(context, uri) + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + } + + fun startActionView(context: Activity, uri: Uri?) { + var uri = uri ?: return + val mimeType = getMimeType(context, uri) + val intent = Intent(Intent.ACTION_VIEW) + if (uri.scheme == ContentResolver.SCHEME_CONTENT) { + uri = copyToUri(context, Uri.fromFile(context.externalCacheDir), uri) + } + val share = FileProvider.getUriForFile(context, Constants.FILE_PROVIDER_AUTHORITY, File(uri.path)) + intent.setDataAndType(share, mimeType) + grantReadPermissions(intent) + val packageManager = context.packageManager + if (intent.resolveActivity(packageManager) != null) { + context.startActivity(intent) + } else { + Toast.makeText(context, R.string.no_application_found, Toast.LENGTH_SHORT).show() + } + } + + private fun grantReadPermissions(intent: Intent) { + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + @JvmStatic + @Throws(IOException::class) + fun newFile( + context: Context, destination: Uri, mimeType: String?, baseName: String, extension: String?): Uri { + val filename = getNonCollidingFileName(context, destination, baseName, extension) + return when (destination.scheme) { + "content" -> { + val tree = DocumentFile.fromTreeUri(context, destination) + val f1 = tree!!.createFile(mimeType!!, filename) + ?: throw FileNotFoundException("Failed to create $filename") + f1.uri + } + "file" -> { + val dir = File(destination.path) + if (!dir.exists() && !dir.mkdirs()) { + throw IOException("Failed to create %s" + dir.absolutePath) + } + val f2 = File(dir.absolutePath + File.separator + filename) + if (f2.createNewFile()) { + return Uri.fromFile(f2) + } + throw FileNotFoundException("Failed to create $filename") + } + else -> throw IllegalArgumentException("Unknown URI scheme: " + destination.scheme) + } + } + + fun copyToUri(context: Context, destination: Uri, input: Uri): Uri { + val filename = getFilename(context, input) + return copyToUri(context, destination, input, Files.getNameWithoutExtension(filename!!)) + } + + fun copyToUri(context: Context, destination: Uri, input: Uri, basename: String): Uri { + return try { + val output = newFile( + context, + destination, + getMimeType(context, input), + basename, + getExtension(context, input)) + copyStream(context, input, output) + output + } catch (e: IOException) { + throw IllegalStateException(e) + } + } + + fun copyStream(context: Context, input: Uri?, output: Uri?) { + val contentResolver = context.contentResolver + try { + val inputStream = contentResolver.openInputStream(input!!) + val outputStream = contentResolver.openOutputStream(output!!) + ByteStreams.copy(inputStream!!, outputStream!!) + inputStream.close() + outputStream.close() + } catch (e: IOException) { + throw IllegalStateException(e) + } + } + + private fun getNonCollidingFileName( + context: Context, uri: Uri, baseName: String, extension: String?): String { + var extension = extension + var tries = 1 + if (!extension!!.startsWith(".")) { + extension = ".$extension" + } + var tempName = baseName + when (uri.scheme) { + ContentResolver.SCHEME_CONTENT -> { + val dir = DocumentFile.fromTreeUri(context, uri) + val documentFiles = Arrays.asList(*dir!!.listFiles()) + while (true) { + val result = tempName + extension + if (Iterables.any(documentFiles) { f: DocumentFile? -> f!!.name == result }) { + tempName = "$baseName-$tries" + tries++ + } else { + break + } + } + } + ContentResolver.SCHEME_FILE -> { + var f = File(uri.path, baseName + extension) + while (f.exists()) { + tempName = "$baseName-$tries" // $NON-NLS-1$ + f = File(uri.path, tempName + extension) + tries++ + } + } + } + return tempName + extension + } + + fun uri2String(uri: Uri?): String { + if (uri == null) { + return "" + } + return if (uri.scheme == ContentResolver.SCHEME_FILE) { + File(uri.path).absolutePath + } else uri.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/preferences/fragments/Backups.kt b/app/src/main/java/org/tasks/preferences/fragments/Backups.kt index 0c8ac54d4..189eb31e2 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/Backups.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/Backups.kt @@ -162,7 +162,7 @@ class Backups : InjectingPreferenceFragment() { } else if (requestCode == REQUEST_PICKER) { if (resultCode == RESULT_OK) { val uri = data!!.data - val extension = FileHelper.getExtension(activity, uri) + val extension = FileHelper.getExtension(requireContext(), uri!!) if (!("json".equals(extension, ignoreCase = true) || "xml".equals( extension, ignoreCase = true