From dd3cef5ec2c69277fa7b38133214a8cae9d9186a Mon Sep 17 00:00:00 2001 From: Jonas Vautherin Date: Wed, 29 Mar 2017 00:20:35 +0200 Subject: [PATCH] Refactor storage management to have a centralized, clearer way to get the Signal output directories Closes #6476 // FREEBIE --- .../database/EncryptedBackupExporter.java | 121 --------- .../database/PlaintextBackupExporter.java | 14 +- .../database/PlaintextBackupImporter.java | 39 +-- .../securesms/util/SaveAttachmentTask.java | 147 ++++++----- .../securesms/util/StorageUtil.java | 52 ++++ .../util/SaveAttachmentTaskTest.java | 242 ++++++++++++++++++ 6 files changed, 389 insertions(+), 226 deletions(-) delete mode 100644 src/org/thoughtcrime/securesms/database/EncryptedBackupExporter.java create mode 100644 src/org/thoughtcrime/securesms/util/StorageUtil.java create mode 100644 test/androidTest/java/org/thoughtcrime/securesms/util/SaveAttachmentTaskTest.java diff --git a/src/org/thoughtcrime/securesms/database/EncryptedBackupExporter.java b/src/org/thoughtcrime/securesms/database/EncryptedBackupExporter.java deleted file mode 100644 index cafe2cf4af..0000000000 --- a/src/org/thoughtcrime/securesms/database/EncryptedBackupExporter.java +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.thoughtcrime.securesms.database; - -import android.content.Context; -import android.os.Environment; -import android.util.Log; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.channels.FileChannel; - -public class EncryptedBackupExporter { - - public static void exportToSd(Context context) throws NoExternalStorageException, IOException { - verifyExternalStorageForExport(); - exportDirectory(context, ""); - } - - public static void importFromSd(Context context) throws NoExternalStorageException, IOException { - verifyExternalStorageForImport(); - importDirectory(context, ""); - } - - private static String getExportDirectoryPath() { - File sdDirectory = Environment.getExternalStorageDirectory(); - return sdDirectory.getAbsolutePath() + File.separator + "TextSecureExport"; - } - - private static void verifyExternalStorageForExport() throws NoExternalStorageException { - if (!Environment.getExternalStorageDirectory().canWrite()) - throw new NoExternalStorageException(); - - String exportDirectoryPath = getExportDirectoryPath(); - File exportDirectory = new File(exportDirectoryPath); - - if (!exportDirectory.exists()) - exportDirectory.mkdir(); - } - - private static void verifyExternalStorageForImport() throws NoExternalStorageException { - if (!Environment.getExternalStorageDirectory().canRead() || - !(new File(getExportDirectoryPath()).exists())) - throw new NoExternalStorageException(); - } - - private static void migrateFile(File from, File to) { - try { - if (from.exists()) { - FileChannel source = new FileInputStream(from).getChannel(); - FileChannel destination = new FileOutputStream(to).getChannel(); - - destination.transferFrom(source, 0, source.size()); - source.close(); - destination.close(); - } - } catch (IOException ioe) { - Log.w("EncryptedBackupExporter", ioe); - } - } - - private static void exportDirectory(Context context, String directoryName) throws IOException { - File directory = new File(context.getFilesDir().getParent() + File.separatorChar + directoryName); - File exportDirectory = new File(getExportDirectoryPath() + File.separatorChar + directoryName); - - if (directory.exists()) { - exportDirectory.mkdirs(); - - File[] contents = directory.listFiles(); - - for (int i=0;i modifiedThreads = new HashSet(); + Set modifiedThreads = new HashSet<>(); XmlBackup.XmlBackupItem item; while ((item = backup.getNext()) != null) { @@ -78,7 +54,7 @@ public class PlaintextBackupImporter { addTranslatedTypeToStatement(statement, 8, item.getType()); addNullToStatement(statement, 9); addStringToStatement(statement, 10, item.getSubject()); - addEncryptedStingToStatement(masterCipher, statement, 11, item.getBody()); + addEncryptedStringToStatement(masterCipher, statement, 11, item.getBody()); addStringToStatement(statement, 12, item.getServiceCenter()); addLongToStatement(statement, 13, threadId); modifiedThreads.add(threadId); @@ -98,7 +74,14 @@ public class PlaintextBackupImporter { } } - private static void addEncryptedStingToStatement(MasterCipher masterCipher, SQLiteStatement statement, int index, String value) { + private static File getPlaintextExportFile() throws NoExternalStorageException { + File backup = PlaintextBackupExporter.getPlaintextExportFile(); + File oldBackup = new File(Environment.getExternalStorageDirectory(), "TextSecurePlaintextBackup.xml"); + + return !backup.exists() && oldBackup.exists() ? oldBackup : backup; + } + + private static void addEncryptedStringToStatement(MasterCipher masterCipher, SQLiteStatement statement, int index, String value) { if (value == null || value.equals("null")) { statement.bindNull(index); } else { @@ -130,6 +113,4 @@ public class PlaintextBackupImporter { ourType == MmsSmsColumns.Types.BASE_SENT_TYPE || ourType == MmsSmsColumns.Types.BASE_SENT_FAILED_TYPE; } - - } diff --git a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java index a97b185b3a..e3168a7fd5 100644 --- a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java +++ b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java @@ -5,7 +5,6 @@ import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.media.MediaScannerConnection; import android.net.Uri; -import android.os.Environment; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; @@ -17,6 +16,7 @@ import android.widget.Toast; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.NoExternalStorageException; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.whispersystems.libsignal.util.Pair; @@ -32,9 +32,9 @@ import java.text.SimpleDateFormat; public class SaveAttachmentTask extends ProgressDialogAsyncTask> { private static final String TAG = SaveAttachmentTask.class.getSimpleName(); - private static final int SUCCESS = 0; - private static final int FAILURE = 1; - private static final int WRITE_ACCESS_FAILURE = 2; + protected static final int SUCCESS = 0; + protected static final int FAILURE = 1; + protected static final int WRITE_ACCESS_FAILURE = 2; private final WeakReference contextReference; private final WeakReference masterSecretReference; @@ -67,7 +67,7 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask(WRITE_ACCESS_FAILURE, null); } @@ -84,17 +84,23 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask 1) return new Pair<>(SUCCESS, null); else return new Pair<>(SUCCESS, directory); - } catch (IOException ioe) { + } catch (NoExternalStorageException|IOException ioe) { Log.w(TAG, ioe); return new Pair<>(FAILURE, null); } } private @Nullable File saveAttachment(Context context, MasterSecret masterSecret, Attachment attachment) - throws IOException + throws NoExternalStorageException, IOException { String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType); - File mediaFile = constructOutputFile(attachment.fileName, contentType, attachment.date); + String fileName = attachment.fileName; + + if (fileName == null) fileName = generateOutputFileName(contentType, attachment.date); + fileName = sanitizeOutputFileName(fileName); + + File outputDirectory = createOutputDirectoryFromContentType(contentType); + File mediaFile = createOutputFile(outputDirectory, fileName); InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, attachment.uri); if (inputStream == null) { @@ -110,6 +116,73 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask 1) result[1] = tokens[1]; + else result[1] = ""; + + return result; + } + @Override protected void onPostExecute(final Pair result) { super.onPostExecute(result); @@ -152,64 +225,6 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask 1) result[1] = tokens[1]; - else result[1] = ""; - - return result; - } - public static class Attachment { public Uri uri; public String fileName; diff --git a/src/org/thoughtcrime/securesms/util/StorageUtil.java b/src/org/thoughtcrime/securesms/util/StorageUtil.java new file mode 100644 index 0000000000..527818abdc --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/StorageUtil.java @@ -0,0 +1,52 @@ +package org.thoughtcrime.securesms.util; + +import android.os.Environment; + +import org.thoughtcrime.securesms.database.NoExternalStorageException; + +import java.io.File; + +public class StorageUtil +{ + private static File getSignalStorageDir() throws NoExternalStorageException { + final File storage = Environment.getExternalStorageDirectory(); + + if (!storage.canWrite()) { + throw new NoExternalStorageException(); + } + + return storage; + } + + public static boolean canWriteInSignalStorageDir() { + File storage; + + try { + storage = getSignalStorageDir(); + } catch (NoExternalStorageException e) { + return false; + } + + return storage.canWrite(); + } + + public static File getBackupDir() throws NoExternalStorageException { + return getSignalStorageDir(); + } + + public static File getVideoDir() throws NoExternalStorageException { + return new File(getSignalStorageDir(), Environment.DIRECTORY_MOVIES); + } + + public static File getAudioDir() throws NoExternalStorageException { + return new File(getSignalStorageDir(), Environment.DIRECTORY_MUSIC); + } + + public static File getImageDir() throws NoExternalStorageException { + return new File(getSignalStorageDir(), Environment.DIRECTORY_PICTURES); + } + + public static File getDownloadDir() throws NoExternalStorageException { + return new File(getSignalStorageDir(), Environment.DIRECTORY_DOWNLOADS); + } +} diff --git a/test/androidTest/java/org/thoughtcrime/securesms/util/SaveAttachmentTaskTest.java b/test/androidTest/java/org/thoughtcrime/securesms/util/SaveAttachmentTaskTest.java new file mode 100644 index 0000000000..a285e4e2f1 --- /dev/null +++ b/test/androidTest/java/org/thoughtcrime/securesms/util/SaveAttachmentTaskTest.java @@ -0,0 +1,242 @@ +package org.thoughtcrime.securesms.util; + +import android.content.Context; +import android.net.Uri; +import android.view.View; + +import org.thoughtcrime.securesms.TextSecureTestCase; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.NoExternalStorageException; +import org.whispersystems.libsignal.util.Pair; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; + +public class SaveAttachmentTaskTest extends TextSecureTestCase +{ + private static final long TEST_TIMESTAMP = 585001320000L; + + private TestSaveAttachmentTask saveAttachmentTask; + + @Override + public void setUp() { + super.setUp(); + saveAttachmentTask = createTestSaveAttachmentTask(); + } + + private TestSaveAttachmentTask createTestSaveAttachmentTask() { + return new TestSaveAttachmentTask(getInstrumentation().getTargetContext(), null, null); + } + + public void testDoInBackground_emptyImageAttachmentWithFileNameIsCorrectlySaved() + throws IOException, NoExternalStorageException + { + final String name = "testImageThatShouldNotAlreadyExist"; + final String extension = "png"; + final String outputFileName = name + "." + extension; + final String contentType = "image/png"; + final File outputDir = StorageUtil.getImageDir(); + final File expectedOutputFile = generateOutputFileForKnownFilename(name, extension, outputDir); + + verifyAttachmentSavedCorrectly(outputFileName, contentType, outputDir, expectedOutputFile); + + assertTrue(expectedOutputFile.delete()); + } + + public void testDoInBackground_emptyImageAttachmentWithFileNameIsCorrectlySavedWithIndex() + throws IOException, NoExternalStorageException + { + final String name = "testImageThatShouldNotAlreadyExist"; + final String extension = "png"; + final String outputFileName = name + "." + extension; + final String contentType = "image/png"; + final File outputDir = StorageUtil.getImageDir(); + final ArrayList testFiles = populateWithTestFiles(name, extension, outputDir); + final File expectedOutputFile = generateOutputFileForKnownFilename(name, extension, outputDir); + testFiles.add(expectedOutputFile); + + verifyAttachmentSavedCorrectly(outputFileName, contentType, outputDir, expectedOutputFile); + + for (File tmpFile : testFiles) { + assertTrue(tmpFile.delete()); + } + } + + public void testDoInBackground_emptyImageAttachmentWithoutFileNameIsCorrectlySaved() + throws IOException, NoExternalStorageException + { + final String extension = "png"; + final String contentType = "image/png"; + final File outputDir = StorageUtil.getImageDir(); + final File expectedOutputFile = generateOutputFileForUnknownFilename(extension, TEST_TIMESTAMP, outputDir); + + verifyAttachmentSavedCorrectly(null, contentType, outputDir, expectedOutputFile); + + assertTrue(expectedOutputFile.delete()); + } + + public void testDoInBackground_emptyImageAttachmentWithoutFileNameNorExtensionIsCorrectlySaved() + throws IOException, NoExternalStorageException + { + final String extension = "attach"; + final String contentType = "image/"; + final File outputDir = StorageUtil.getImageDir(); + final File expectedOutputFile = generateOutputFileForUnknownFilename(extension, TEST_TIMESTAMP, outputDir); + + verifyAttachmentSavedCorrectly(null, contentType, outputDir, expectedOutputFile); + + assertTrue(expectedOutputFile.delete()); + } + + public void testDoInBackground_emptyAudioAttachmentWithFileNameIsCorrectlySaved() + throws IOException, NoExternalStorageException + { + final String name = "testAudioThatShouldNotAlreadyExist"; + final String extension = "mp3"; + final String outputFileName = name + "." + extension; + final String contentType = "audio/"; + final File outputDir = StorageUtil.getAudioDir(); + final File expectedOutputFile = generateOutputFileForKnownFilename(name, extension, outputDir); + + verifyAttachmentSavedCorrectly(outputFileName, contentType, outputDir, expectedOutputFile); + + assertTrue(expectedOutputFile.delete()); + } + + public void testDoInBackground_emptyVideoAttachmentWithFileNameIsCorrectlySaved() + throws IOException, NoExternalStorageException + { + final String name = "testVideoThatShouldNotAlreadyExist"; + final String extension = "mp4"; + final String outputFileName = name + "." + extension; + final String contentType = "video/"; + final File outputDir = StorageUtil.getVideoDir(); + final File expectedOutputFile = generateOutputFileForKnownFilename(name, extension, outputDir); + + verifyAttachmentSavedCorrectly(outputFileName, contentType, outputDir, expectedOutputFile); + + assertTrue(expectedOutputFile.delete()); + } + + public void testDoInBackground_emptyUnknownAttachmentWithFileNameIsCorrectlySaved() + throws IOException, NoExternalStorageException + { + final String name = "testFileThatShouldNotAlreadyExist"; + final String extension = "rand"; + final String outputFileName = name + "." + extension; + final String contentType = "somethingweird/"; + final File outputDir = StorageUtil.getDownloadDir(); + final File expectedOutputFile = generateOutputFileForKnownFilename(name, extension, outputDir); + + verifyAttachmentSavedCorrectly(outputFileName, contentType, outputDir, expectedOutputFile); + + assertTrue(expectedOutputFile.delete()); + } + + private ArrayList populateWithTestFiles(String name, String extension, final File outputDir) + throws IOException + { + ArrayList testFiles = new ArrayList<>(); + + for (int i = 0; i < 4; i++) { + File tmpFile = generateOutputFileForKnownFilename(name, extension, outputDir); + if (tmpFile.createNewFile()) { + testFiles.add(tmpFile); + } + } + + return testFiles; + } + + private File generateOutputFileForKnownFilename(String name, + String extension, + final File outputDir) + { + final String outputFileName = guessOutputFileNameIndex(name, extension, outputDir); + final File outputFile = new File(outputDir, outputFileName); + + assertFalse(outputFile.exists()); + return outputFile; + } + + private String guessOutputFileNameIndex(String name, String extension, final File outputDir) { + final File outputFile = new File(outputDir, name + "." + extension); + + if (outputFile.exists()) { + String newName; + + if (name.charAt(name.length() - 2) == '-') { + int newIndex = Integer.parseInt("" + name.charAt(name.length() - 1)) + 1; + newName = name.substring(0, name.length() - 1) + newIndex; + } else { + newName = name + "-1"; + } + + return guessOutputFileNameIndex(newName, extension, outputDir); + } else { + return name + "." + extension; + } + } + + private void verifyAttachmentSavedCorrectly(String outputFileName, + String contentType, + final File outputDir, + final File expectedOutputFile) + throws IOException + { + final File testFile = createEmptyTempFile("testFile", "ext"); + final SaveAttachmentTask.Attachment attachment + = new SaveAttachmentTask.Attachment(Uri.fromFile(testFile), + contentType, + TEST_TIMESTAMP, + outputFileName); + + Pair result = saveAttachmentTask.doInBackground(attachment); + + assertTrue(result.first() == SaveAttachmentTask.SUCCESS); + assertEquals(result.second().getAbsolutePath(), outputDir.getAbsolutePath()); + assertTrue(expectedOutputFile.exists()); + } + + private File createEmptyTempFile(String fileName, String extension) throws IOException + { + String fullName = fileName + "." + extension; + File file = new File(getInstrumentation().getTargetContext().getCacheDir(), fullName); + + if (file.exists()) { + file = createEmptyTempFile(fileName + "-" + System.currentTimeMillis(), extension); + } else { + file.createNewFile(); + } + + return file; + } + + private File generateOutputFileForUnknownFilename(String extension, + long date, + final File outputDir) + { + if (extension == null) extension = "attach"; + + SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss"); + String base = "signal-" + dateFormatter.format(date); + + final String outputFileName = guessOutputFileNameIndex(base, extension, outputDir); + final File outputFile = new File(outputDir, outputFileName); + + assertFalse(outputFile.exists()); + return outputFile; + } + + private class TestSaveAttachmentTask extends SaveAttachmentTask { + private TestSaveAttachmentTask(Context context, MasterSecret masterSecret, View view) + { + super(context, masterSecret, view); + } + + @Override + protected void onPreExecute() {} + } +}