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() {}
+ }
+}