mirror of
https://github.com/moparisthebest/k-9
synced 2024-11-23 09:52:16 -05:00
Merge branch 'open_attachment_improvements'
Conflicts: src/com/fsck/k9/helper/Utility.java
This commit is contained in:
commit
98b5d63909
@ -226,7 +226,7 @@ K-9 Mail — почтовый клиент для Android.
|
||||
<string name="message_view_show_attachments_action">Вложения</string>
|
||||
<string name="message_view_show_more_attachments_action">Показать вложения</string>
|
||||
<string name="message_view_fetching_attachment_toast">Извлечение вложений</string>
|
||||
<string name="message_view_no_viewer">Отсутствует просмотрщик %s <xliff:g id="mimetype">%s</xliff:g></string>
|
||||
<string name="message_view_no_viewer">Отсутствует просмотрщик <xliff:g id="mimetype">%s</xliff:g></string>
|
||||
<string name="message_view_download_remainder">Загрузить полностью</string>
|
||||
<string name="message_view_downloading">Загрузка…</string>
|
||||
<!--NOTE: The following message refers to strings with id account_setup_incoming_save_all_headers_label and account_setup_incoming_title-->
|
||||
|
@ -283,33 +283,6 @@ public class K9 extends Application {
|
||||
*/
|
||||
private static boolean sDatabasesUpToDate = false;
|
||||
|
||||
|
||||
/**
|
||||
* The MIME type(s) of attachments we're willing to view.
|
||||
*/
|
||||
public static final String[] ACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
|
||||
"*/*",
|
||||
};
|
||||
|
||||
/**
|
||||
* The MIME type(s) of attachments we're not willing to view.
|
||||
*/
|
||||
public static final String[] UNACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
|
||||
};
|
||||
|
||||
/**
|
||||
* The MIME type(s) of attachments we're willing to download to SD.
|
||||
*/
|
||||
public static final String[] ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
|
||||
"*/*",
|
||||
};
|
||||
|
||||
/**
|
||||
* The MIME type(s) of attachments we're not willing to download to SD.
|
||||
*/
|
||||
public static final String[] UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
|
||||
};
|
||||
|
||||
/**
|
||||
* For use when displaying that no folder is selected
|
||||
*/
|
||||
|
65
src/com/fsck/k9/cache/TemporaryAttachmentStore.java
vendored
Normal file
65
src/com/fsck/k9/cache/TemporaryAttachmentStore.java
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
package com.fsck.k9.cache;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.helper.FileHelper;
|
||||
|
||||
|
||||
public class TemporaryAttachmentStore {
|
||||
private static String TEMPORARY_ATTACHMENT_DIRECTORY = "attachments";
|
||||
private static long MAX_FILE_AGE = 12 * 60 * 60 * 1000; // 12h
|
||||
|
||||
public static File getFile(Context context, String attachmentName) {
|
||||
File directory = getTemporaryAttachmentDirectory(context);
|
||||
String filename = FileHelper.sanitizeFilename(attachmentName);
|
||||
return new File(directory, filename);
|
||||
}
|
||||
|
||||
public static File getFileForWriting(Context context, String attachmentName) throws IOException {
|
||||
File directory = createOrCleanAttachmentDirectory(context);
|
||||
String filename = FileHelper.sanitizeFilename(attachmentName);
|
||||
return new File(directory, filename);
|
||||
}
|
||||
|
||||
private static File createOrCleanAttachmentDirectory(Context context) throws IOException {
|
||||
File directory = getTemporaryAttachmentDirectory(context);
|
||||
if (directory.exists()) {
|
||||
cleanOldFiles(directory);
|
||||
} else {
|
||||
if (!directory.mkdir()) {
|
||||
throw new IOException("Couldn't create temporary attachment store: " + directory.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
return directory;
|
||||
}
|
||||
|
||||
private static File getTemporaryAttachmentDirectory(Context context) {
|
||||
return new File(context.getExternalCacheDir(), TEMPORARY_ATTACHMENT_DIRECTORY);
|
||||
}
|
||||
|
||||
private static void cleanOldFiles(File directory) {
|
||||
File[] files = directory.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
long cutOffTime = System.currentTimeMillis() - MAX_FILE_AGE;
|
||||
for (File file : files) {
|
||||
if (file.lastModified() < cutOffTime) {
|
||||
if (file.delete()) {
|
||||
if (K9.DEBUG) {
|
||||
Log.d(K9.LOG_TAG, "Deleted from temporary attachment store: " + file.getName());
|
||||
}
|
||||
} else {
|
||||
Log.w(K9.LOG_TAG, "Couldn't delete from temporary attachment store: " + file.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -206,7 +206,7 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
|
||||
mMessageView.setAttachmentCallback(new AttachmentFileDownloadCallback() {
|
||||
|
||||
@Override
|
||||
public void showFileBrowser(final AttachmentView caller) {
|
||||
public void pickDirectoryToSaveAttachmentTo(final AttachmentView caller) {
|
||||
FileBrowserHelper.getInstance()
|
||||
.showFileBrowserActivity(MessageViewFragment.this,
|
||||
null,
|
||||
@ -498,15 +498,8 @@ public class MessageViewFragment extends Fragment implements OnClickListener,
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
switch (view.getId()) {
|
||||
case R.id.download: {
|
||||
((AttachmentView)view).saveFile();
|
||||
break;
|
||||
}
|
||||
case R.id.download_remainder: {
|
||||
onDownloadRemainder();
|
||||
break;
|
||||
}
|
||||
if (view.getId() == R.id.download_remainder) {
|
||||
onDownloadRemainder();
|
||||
}
|
||||
}
|
||||
|
||||
|
166
src/com/fsck/k9/helper/FileHelper.java
Normal file
166
src/com/fsck/k9/helper/FileHelper.java
Normal file
@ -0,0 +1,166 @@
|
||||
package com.fsck.k9.helper;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.K9;
|
||||
|
||||
|
||||
public class FileHelper {
|
||||
|
||||
/**
|
||||
* Regular expression that represents characters we won't allow in file names.
|
||||
*
|
||||
* <p>
|
||||
* Allowed are:
|
||||
* <ul>
|
||||
* <li>word characters (letters, digits, and underscores): {@code \w}</li>
|
||||
* <li>spaces: {@code " "}</li>
|
||||
* <li>special characters: {@code !}, {@code #}, {@code $}, {@code %}, {@code &}, {@code '},
|
||||
* {@code (}, {@code )}, {@code -}, {@code @}, {@code ^}, {@code `}, <code>{</code>,
|
||||
* <code>}</code>, {@code ~}, {@code .}, {@code ,}</li>
|
||||
* </ul></p>
|
||||
*
|
||||
* @see #sanitizeFilename(String)
|
||||
*/
|
||||
private static final String INVALID_CHARACTERS = "[^\\w !#$%&'()\\-@\\^`{}~.,]";
|
||||
|
||||
/**
|
||||
* Invalid characters in a file name are replaced by this character.
|
||||
*
|
||||
* @see #sanitizeFilename(String)
|
||||
*/
|
||||
private static final String REPLACEMENT_CHARACTER = "_";
|
||||
|
||||
|
||||
/**
|
||||
* Creates a unique file in the given directory by appending a hyphen
|
||||
* and a number to the given filename.
|
||||
*/
|
||||
public static File createUniqueFile(File directory, String filename) {
|
||||
File file = new File(directory, filename);
|
||||
if (!file.exists()) {
|
||||
return file;
|
||||
}
|
||||
// Get the extension of the file, if any.
|
||||
int index = filename.lastIndexOf('.');
|
||||
String format;
|
||||
if (index != -1) {
|
||||
String name = filename.substring(0, index);
|
||||
String extension = filename.substring(index);
|
||||
format = name + "-%d" + extension;
|
||||
} else {
|
||||
format = filename + "-%d";
|
||||
}
|
||||
for (int i = 2; i < Integer.MAX_VALUE; i++) {
|
||||
file = new File(directory, String.format(Locale.US, format, i));
|
||||
if (!file.exists()) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void touchFile(final File parentDir, final String name) {
|
||||
final File file = new File(parentDir, name);
|
||||
try {
|
||||
if (!file.exists()) {
|
||||
file.createNewFile();
|
||||
} else {
|
||||
file.setLastModified(System.currentTimeMillis());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(K9.LOG_TAG, "Unable to touch file: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean move(final File from, final File to) {
|
||||
if (to.exists()) {
|
||||
to.delete();
|
||||
}
|
||||
to.getParentFile().mkdirs();
|
||||
|
||||
try {
|
||||
FileInputStream in = new FileInputStream(from);
|
||||
try {
|
||||
FileOutputStream out = new FileOutputStream(to);
|
||||
try {
|
||||
byte[] buffer = new byte[1024];
|
||||
int count = -1;
|
||||
while ((count = in.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, count);
|
||||
}
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
} finally {
|
||||
try { in.close(); } catch (Throwable ignore) {}
|
||||
}
|
||||
from.delete();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.w(K9.LOG_TAG, "cannot move " + from.getAbsolutePath() + " to " + to.getAbsolutePath(), e);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void moveRecursive(final File fromDir, final File toDir) {
|
||||
if (!fromDir.exists()) {
|
||||
return;
|
||||
}
|
||||
if (!fromDir.isDirectory()) {
|
||||
if (toDir.exists()) {
|
||||
if (!toDir.delete()) {
|
||||
Log.w(K9.LOG_TAG, "cannot delete already existing file/directory " + toDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
if (!fromDir.renameTo(toDir)) {
|
||||
Log.w(K9.LOG_TAG, "cannot rename " + fromDir.getAbsolutePath() + " to " + toDir.getAbsolutePath() + " - moving instead");
|
||||
move(fromDir, toDir);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!toDir.exists() || !toDir.isDirectory()) {
|
||||
if (toDir.exists()) {
|
||||
toDir.delete();
|
||||
}
|
||||
if (!toDir.mkdirs()) {
|
||||
Log.w(K9.LOG_TAG, "cannot create directory " + toDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
File[] files = fromDir.listFiles();
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
moveRecursive(file, new File(toDir, file.getName()));
|
||||
file.delete();
|
||||
} else {
|
||||
File target = new File(toDir, file.getName());
|
||||
if (!file.renameTo(target)) {
|
||||
Log.w(K9.LOG_TAG, "cannot rename " + file.getAbsolutePath() + " to " + target.getAbsolutePath() + " - moving instead");
|
||||
move(file, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!fromDir.delete()) {
|
||||
Log.w(K9.LOG_TAG, "cannot delete " + fromDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace characters we don't allow in file names with a replacement character.
|
||||
*
|
||||
* @param filename
|
||||
* The original file name.
|
||||
*
|
||||
* @return The sanitized file name containing only allowed characters.
|
||||
*/
|
||||
public static String sanitizeFilename(String filename) {
|
||||
return filename.replaceAll(INVALID_CHARACTERS, REPLACEMENT_CHARACTER);
|
||||
}
|
||||
}
|
@ -1,40 +1,15 @@
|
||||
package com.fsck.k9.helper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaScannerConnection;
|
||||
|
||||
public class MediaScannerNotifier implements MediaScannerConnectionClient {
|
||||
private MediaScannerConnection mConnection;
|
||||
private File mFile;
|
||||
private Context mContext;
|
||||
|
||||
public MediaScannerNotifier(Context context, File file) {
|
||||
mFile = file;
|
||||
mConnection = new MediaScannerConnection(context, this);
|
||||
mConnection.connect();
|
||||
mContext = context;
|
||||
|
||||
}
|
||||
|
||||
public void onMediaScannerConnected() {
|
||||
mConnection.scanFile(mFile.getAbsolutePath(), null);
|
||||
}
|
||||
|
||||
public void onScanCompleted(String path, Uri uri) {
|
||||
try {
|
||||
if (uri != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(uri);
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
} finally {
|
||||
mConnection.disconnect();
|
||||
}
|
||||
public class MediaScannerNotifier {
|
||||
public static void notify(Context context, File file) {
|
||||
String[] paths = { file.getAbsolutePath() };
|
||||
MediaScannerConnection.scanFile(context, paths, null, null);
|
||||
}
|
||||
}
|
||||
|
@ -17,43 +17,13 @@ import android.widget.TextView;
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.mail.filter.Base64;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Utility {
|
||||
/**
|
||||
* Regular expression that represents characters we won't allow in file names.
|
||||
*
|
||||
* <p>
|
||||
* Allowed are:
|
||||
* <ul>
|
||||
* <li>word characters (letters, digits, and underscores): {@code \w}</li>
|
||||
* <li>spaces: {@code " "}</li>
|
||||
* <li>special characters: {@code !}, {@code #}, {@code $}, {@code %}, {@code &}, {@code '},
|
||||
* {@code (}, {@code )}, {@code -}, {@code @}, {@code ^}, {@code `}, <code>{</code>,
|
||||
* <code>}</code>, {@code ~}, {@code .}, {@code ,}</li>
|
||||
* </ul></p>
|
||||
*
|
||||
* @see #sanitizeFilename(String)
|
||||
*/
|
||||
private static final String INVALID_CHARACTERS = "[^\\w !#$%&'()\\-@\\^`{}~.,]+";
|
||||
|
||||
/**
|
||||
* Invalid characters in a file name are replaced by this character.
|
||||
*
|
||||
* @see #sanitizeFilename(String)
|
||||
*/
|
||||
private static final String REPLACEMENT_CHARACTER = "_";
|
||||
|
||||
// \u00A0 (non-breaking space) happens to be used by French MUA
|
||||
|
||||
@ -471,139 +441,6 @@ public class Utility {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param parentDir
|
||||
* @param name
|
||||
* Never <code>null</code>.
|
||||
*/
|
||||
public static void touchFile(final File parentDir, final String name) {
|
||||
final File file = new File(parentDir, name);
|
||||
try {
|
||||
if (!file.exists()) {
|
||||
file.createNewFile();
|
||||
} else {
|
||||
file.setLastModified(System.currentTimeMillis());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(K9.LOG_TAG, "Unable to touch file: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unique file in the given directory by appending a hyphen
|
||||
* and a number to the given filename.
|
||||
*
|
||||
* @param directory
|
||||
* @param filename
|
||||
* @return
|
||||
*/
|
||||
public static File createUniqueFile(File directory, String filename) {
|
||||
File file = new File(directory, filename);
|
||||
if (!file.exists()) {
|
||||
return file;
|
||||
}
|
||||
// Get the extension of the file, if any.
|
||||
int index = filename.lastIndexOf('.');
|
||||
String format;
|
||||
if (index != -1) {
|
||||
String name = filename.substring(0, index);
|
||||
String extension = filename.substring(index);
|
||||
format = name + "-%d" + extension;
|
||||
} else {
|
||||
format = filename + "-%d";
|
||||
}
|
||||
for (int i = 2; i < Integer.MAX_VALUE; i++) {
|
||||
file = new File(directory, String.format(Locale.US, format, i));
|
||||
if (!file.exists()) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param from
|
||||
* @param to
|
||||
* @return
|
||||
*/
|
||||
public static boolean move(final File from, final File to) {
|
||||
if (to.exists()) {
|
||||
to.delete();
|
||||
}
|
||||
to.getParentFile().mkdirs();
|
||||
|
||||
try {
|
||||
FileInputStream in = new FileInputStream(from);
|
||||
try {
|
||||
FileOutputStream out = new FileOutputStream(to);
|
||||
try {
|
||||
byte[] buffer = new byte[1024];
|
||||
int count = -1;
|
||||
while ((count = in.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, count);
|
||||
}
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
} finally {
|
||||
try { in.close(); } catch (Throwable ignore) {}
|
||||
}
|
||||
from.delete();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.w(K9.LOG_TAG, "cannot move " + from.getAbsolutePath() + " to " + to.getAbsolutePath(), e);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fromDir
|
||||
* @param toDir
|
||||
*/
|
||||
public static void moveRecursive(final File fromDir, final File toDir) {
|
||||
if (!fromDir.exists()) {
|
||||
return;
|
||||
}
|
||||
if (!fromDir.isDirectory()) {
|
||||
if (toDir.exists()) {
|
||||
if (!toDir.delete()) {
|
||||
Log.w(K9.LOG_TAG, "cannot delete already existing file/directory " + toDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
if (!fromDir.renameTo(toDir)) {
|
||||
Log.w(K9.LOG_TAG, "cannot rename " + fromDir.getAbsolutePath() + " to " + toDir.getAbsolutePath() + " - moving instead");
|
||||
move(fromDir, toDir);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!toDir.exists() || !toDir.isDirectory()) {
|
||||
if (toDir.exists()) {
|
||||
toDir.delete();
|
||||
}
|
||||
if (!toDir.mkdirs()) {
|
||||
Log.w(K9.LOG_TAG, "cannot create directory " + toDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
File[] files = fromDir.listFiles();
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
moveRecursive(file, new File(toDir, file.getName()));
|
||||
file.delete();
|
||||
} else {
|
||||
File target = new File(toDir, file.getName());
|
||||
if (!file.renameTo(target)) {
|
||||
Log.w(K9.LOG_TAG, "cannot rename " + file.getAbsolutePath() + " to " + target.getAbsolutePath() + " - moving instead");
|
||||
move(file, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!fromDir.delete()) {
|
||||
Log.w(K9.LOG_TAG, "cannot delete " + fromDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
private static final String IMG_SRC_REGEX = "(?is:<img[^>]+src\\s*=\\s*['\"]?([a-z]+)\\:)";
|
||||
private static final Pattern IMG_PATTERN = Pattern.compile(IMG_SRC_REGEX);
|
||||
@ -641,18 +478,6 @@ public class Utility {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace characters we don't allow in file names with a replacement character.
|
||||
*
|
||||
* @param filename
|
||||
* The original file name.
|
||||
*
|
||||
* @return The sanitized file name containing only allowed characters.
|
||||
*/
|
||||
public static String sanitizeFilename(String filename) {
|
||||
return filename.replaceAll(INVALID_CHARACTERS, REPLACEMENT_CHARACTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if we have network connectivity.
|
||||
* @param app Current application (Hint: see if your base class has a getApplication() method.)
|
||||
|
@ -1111,20 +1111,8 @@ public class MimeUtility {
|
||||
return p.matcher(mimeType).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given mimeType matches any of the matchAgainst specifications.
|
||||
* @param mimeType A MIME type to check.
|
||||
* @param matchAgainst An array of MIME types to check against. May include wildcards such
|
||||
* as image/* or * /*.
|
||||
* @return
|
||||
*/
|
||||
public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
|
||||
for (String matchType : matchAgainst) {
|
||||
if (mimeTypeMatches(mimeType, matchType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
public static boolean isDefaultMimeType(String mimeType) {
|
||||
return DEFAULT_ATTACHMENT_MIME_TYPE.equalsIgnoreCase(mimeType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,7 +14,7 @@ import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.helper.FileHelper;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
|
||||
public class LockableDatabase {
|
||||
@ -337,9 +337,10 @@ public class LockableDatabase {
|
||||
prepareStorage(newProviderId);
|
||||
|
||||
// move all database files
|
||||
Utility.moveRecursive(oldDatabase, storageManager.getDatabase(uUid, newProviderId));
|
||||
FileHelper.moveRecursive(oldDatabase, storageManager.getDatabase(uUid, newProviderId));
|
||||
// move all attachment files
|
||||
Utility.moveRecursive(storageManager.getAttachmentDirectory(uUid, oldProviderId), storageManager.getAttachmentDirectory(uUid, newProviderId));
|
||||
FileHelper.moveRecursive(storageManager.getAttachmentDirectory(uUid, oldProviderId),
|
||||
storageManager.getAttachmentDirectory(uUid, newProviderId));
|
||||
// remove any remaining old journal files
|
||||
deleteDatabase(oldDatabase);
|
||||
|
||||
@ -421,14 +422,14 @@ public class LockableDatabase {
|
||||
// Android seems to be unmounting the storage...
|
||||
throw new UnavailableStorageException("Unable to access: " + databaseParentDir);
|
||||
}
|
||||
Utility.touchFile(databaseParentDir, ".nomedia");
|
||||
FileHelper.touchFile(databaseParentDir, ".nomedia");
|
||||
}
|
||||
|
||||
final File attachmentDir = storageManager.getAttachmentDirectory(uUid, providerId);
|
||||
final File attachmentParentDir = attachmentDir.getParentFile();
|
||||
if (!attachmentParentDir.exists()) {
|
||||
attachmentParentDir.mkdirs();
|
||||
Utility.touchFile(attachmentParentDir, ".nomedia");
|
||||
FileHelper.touchFile(attachmentParentDir, ".nomedia");
|
||||
}
|
||||
if (!attachmentDir.exists()) {
|
||||
attachmentDir.mkdirs();
|
||||
|
@ -12,6 +12,8 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import com.fsck.k9.helper.FileHelper;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import android.content.Context;
|
||||
@ -23,7 +25,6 @@ import android.util.Xml;
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.Preferences;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.mail.Store;
|
||||
import com.fsck.k9.mail.ServerSettings;
|
||||
import com.fsck.k9.mail.Transport;
|
||||
@ -87,7 +88,7 @@ public class SettingsExporter {
|
||||
File dir = new File(Environment.getExternalStorageDirectory() + File.separator
|
||||
+ context.getPackageName());
|
||||
dir.mkdirs();
|
||||
File file = Utility.createUniqueFile(dir, EXPORT_FILENAME);
|
||||
File file = FileHelper.createUniqueFile(dir, EXPORT_FILENAME);
|
||||
filename = file.getAbsolutePath();
|
||||
os = new FileOutputStream(filename);
|
||||
|
||||
|
@ -51,11 +51,21 @@ public class AttachmentProvider extends ContentProvider {
|
||||
|
||||
|
||||
public static Uri getAttachmentUri(Account account, long id) {
|
||||
return getAttachmentUri(account.getUuid(), id, true);
|
||||
return CONTENT_URI.buildUpon()
|
||||
.appendPath(account.getUuid())
|
||||
.appendPath(Long.toString(id))
|
||||
.appendPath(FORMAT_RAW)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Uri getAttachmentUriForViewing(Account account, long id) {
|
||||
return getAttachmentUri(account.getUuid(), id, false);
|
||||
public static Uri getAttachmentUriForViewing(Account account, long id, String mimeType, String filename) {
|
||||
return CONTENT_URI.buildUpon()
|
||||
.appendPath(account.getUuid())
|
||||
.appendPath(Long.toString(id))
|
||||
.appendPath(FORMAT_VIEW)
|
||||
.appendPath(mimeType)
|
||||
.appendPath(filename)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Uri getAttachmentThumbnailUri(Account account, long id, int width, int height) {
|
||||
@ -68,14 +78,6 @@ public class AttachmentProvider extends ContentProvider {
|
||||
.build();
|
||||
}
|
||||
|
||||
private static Uri getAttachmentUri(String db, long id, boolean raw) {
|
||||
return CONTENT_URI.buildUpon()
|
||||
.appendPath(db)
|
||||
.appendPath(Long.toString(id))
|
||||
.appendPath(raw ? FORMAT_RAW : FORMAT_VIEW)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static void clear(Context context) {
|
||||
/*
|
||||
* We use the cache dir as a temporary directory (since Android doesn't give us one) so
|
||||
@ -146,8 +148,9 @@ public class AttachmentProvider extends ContentProvider {
|
||||
String dbName = segments.get(0);
|
||||
String id = segments.get(1);
|
||||
String format = segments.get(2);
|
||||
String mimeType = (segments.size() < 4) ? null : segments.get(3);
|
||||
|
||||
return getType(dbName, id, format);
|
||||
return getType(dbName, id, format, mimeType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -165,7 +168,7 @@ public class AttachmentProvider extends ContentProvider {
|
||||
|
||||
file = getThumbnailFile(getContext(), accountUuid, attachmentId);
|
||||
if (!file.exists()) {
|
||||
String type = getType(accountUuid, attachmentId, FORMAT_VIEW);
|
||||
String type = getType(accountUuid, attachmentId, FORMAT_VIEW, null);
|
||||
try {
|
||||
FileInputStream in = new FileInputStream(getFile(accountUuid, attachmentId));
|
||||
try {
|
||||
@ -258,7 +261,7 @@ public class AttachmentProvider extends ContentProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getType(String dbName, String id, String format) {
|
||||
private String getType(String dbName, String id, String format, String mimeType) {
|
||||
String type;
|
||||
if (FORMAT_THUMBNAIL.equals(format)) {
|
||||
type = "image/png";
|
||||
@ -269,10 +272,9 @@ public class AttachmentProvider extends ContentProvider {
|
||||
final LocalStore localStore = LocalStore.getLocalInstance(account, K9.app);
|
||||
|
||||
AttachmentInfo attachmentInfo = localStore.getAttachmentInfo(id);
|
||||
if (FORMAT_VIEW.equals(format)) {
|
||||
type = MimeUtility.getMimeTypeForViewing(attachmentInfo.type, attachmentInfo.name);
|
||||
if (FORMAT_VIEW.equals(format) && mimeType != null) {
|
||||
type = mimeType;
|
||||
} else {
|
||||
// When accessing the "raw" message we deliver the original MIME type.
|
||||
type = attachmentInfo.type;
|
||||
}
|
||||
} catch (MessagingException e) {
|
||||
|
@ -1,16 +1,18 @@
|
||||
package com.fsck.k9.view;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
@ -30,11 +32,12 @@ import android.widget.Toast;
|
||||
import com.fsck.k9.Account;
|
||||
import com.fsck.k9.K9;
|
||||
import com.fsck.k9.R;
|
||||
import com.fsck.k9.cache.TemporaryAttachmentStore;
|
||||
import com.fsck.k9.controller.MessagingController;
|
||||
import com.fsck.k9.controller.MessagingListener;
|
||||
import com.fsck.k9.helper.FileHelper;
|
||||
import com.fsck.k9.helper.MediaScannerNotifier;
|
||||
import com.fsck.k9.helper.SizeFormatter;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
import com.fsck.k9.mail.Message;
|
||||
import com.fsck.k9.mail.MessagingException;
|
||||
import com.fsck.k9.mail.Part;
|
||||
@ -42,53 +45,48 @@ import com.fsck.k9.mail.internet.MimeHeader;
|
||||
import com.fsck.k9.mail.internet.MimeUtility;
|
||||
import com.fsck.k9.mail.store.local.LocalAttachmentBodyPart;
|
||||
import com.fsck.k9.provider.AttachmentProvider;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
|
||||
public class AttachmentView extends FrameLayout implements OnClickListener, OnLongClickListener {
|
||||
private Context mContext;
|
||||
public Button viewButton;
|
||||
public Button downloadButton;
|
||||
public LocalAttachmentBodyPart part;
|
||||
private Message mMessage;
|
||||
private Account mAccount;
|
||||
private MessagingController mController;
|
||||
private MessagingListener mListener;
|
||||
public String name;
|
||||
public String contentType;
|
||||
public long size;
|
||||
public ImageView iconView;
|
||||
|
||||
private Context context;
|
||||
private Message message;
|
||||
private LocalAttachmentBodyPart part;
|
||||
private Account account;
|
||||
private MessagingController controller;
|
||||
private MessagingListener listener;
|
||||
private AttachmentFileDownloadCallback callback;
|
||||
|
||||
private Button viewButton;
|
||||
private Button downloadButton;
|
||||
|
||||
private String name;
|
||||
private String contentType;
|
||||
private long size;
|
||||
|
||||
|
||||
public AttachmentView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
mContext = context;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public AttachmentView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mContext = context;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public AttachmentView(Context context) {
|
||||
super(context);
|
||||
mContext = context;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
|
||||
public interface AttachmentFileDownloadCallback {
|
||||
/**
|
||||
* this method i called by the attachmentview when
|
||||
* he wants to show a filebrowser
|
||||
* the provider should show the filebrowser activity
|
||||
* and save the reference to the attachment view for later.
|
||||
* in his onActivityResult he can get the saved reference and
|
||||
* call the saveFile method of AttachmentView
|
||||
* @param view
|
||||
*/
|
||||
public void showFileBrowser(AttachmentView caller);
|
||||
public void setButtonsEnabled(boolean enabled) {
|
||||
viewButton.setEnabled(enabled);
|
||||
downloadButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates this view with information about the attachment.
|
||||
*
|
||||
* <p>
|
||||
* This method also decides which attachments are displayed when the "show attachments" button
|
||||
* is pressed, and which attachments are only displayed after the "show more attachments"
|
||||
@ -96,26 +94,33 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo
|
||||
* Inline attachments with content ID and unnamed attachments fall into the second category.
|
||||
* </p>
|
||||
*
|
||||
* @param inputPart
|
||||
* @param message
|
||||
* @param account
|
||||
* @param controller
|
||||
* @param listener
|
||||
*
|
||||
* @return {@code true} for a regular attachment. {@code false}, otherwise.
|
||||
*
|
||||
* @throws MessagingException
|
||||
* In case of an error
|
||||
* @return {@code true} for a regular attachment. {@code false} for attachments that should be initially hidden.
|
||||
*/
|
||||
public boolean populateFromPart(Part inputPart, Message message, Account account,
|
||||
MessagingController controller, MessagingListener listener) throws MessagingException {
|
||||
boolean firstClassAttachment = true;
|
||||
part = (LocalAttachmentBodyPart) inputPart;
|
||||
|
||||
contentType = MimeUtility.unfoldAndDecode(part.getContentType());
|
||||
part = (LocalAttachmentBodyPart) inputPart;
|
||||
this.message = message;
|
||||
this.account = account;
|
||||
this.controller = controller;
|
||||
this.listener = listener;
|
||||
|
||||
boolean firstClassAttachment = extractAttachmentInformation(part);
|
||||
|
||||
displayAttachmentInformation();
|
||||
|
||||
return firstClassAttachment;
|
||||
}
|
||||
|
||||
//TODO: extract this code to a helper class
|
||||
private boolean extractAttachmentInformation(Part part) throws MessagingException {
|
||||
boolean firstClassAttachment = true;
|
||||
|
||||
contentType = part.getMimeType();
|
||||
String contentTypeHeader = MimeUtility.unfoldAndDecode(part.getContentType());
|
||||
String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
|
||||
|
||||
name = MimeUtility.getHeaderParameter(contentType, "name");
|
||||
name = MimeUtility.getHeaderParameter(contentTypeHeader, "name");
|
||||
if (name == null) {
|
||||
name = MimeUtility.getHeaderParameter(contentDisposition, "filename");
|
||||
}
|
||||
@ -135,11 +140,6 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo
|
||||
firstClassAttachment = false;
|
||||
}
|
||||
|
||||
mAccount = account;
|
||||
mMessage = message;
|
||||
mController = controller;
|
||||
mListener = listener;
|
||||
|
||||
String sizeParam = MimeUtility.getHeaderParameter(contentDisposition, "size");
|
||||
if (sizeParam != null) {
|
||||
try {
|
||||
@ -147,20 +147,15 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo
|
||||
} catch (NumberFormatException e) { /* ignore */ }
|
||||
}
|
||||
|
||||
contentType = MimeUtility.getMimeTypeForViewing(part.getMimeType(), name);
|
||||
return firstClassAttachment;
|
||||
}
|
||||
|
||||
private void displayAttachmentInformation() {
|
||||
TextView attachmentName = (TextView) findViewById(R.id.attachment_name);
|
||||
TextView attachmentInfo = (TextView) findViewById(R.id.attachment_info);
|
||||
final ImageView attachmentIcon = (ImageView) findViewById(R.id.attachment_icon);
|
||||
viewButton = (Button) findViewById(R.id.view);
|
||||
downloadButton = (Button) findViewById(R.id.download);
|
||||
if ((!MimeUtility.mimeTypeMatches(contentType, K9.ACCEPTABLE_ATTACHMENT_VIEW_TYPES))
|
||||
|| (MimeUtility.mimeTypeMatches(contentType, K9.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) {
|
||||
viewButton.setVisibility(View.GONE);
|
||||
}
|
||||
if ((!MimeUtility.mimeTypeMatches(contentType, K9.ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))
|
||||
|| (MimeUtility.mimeTypeMatches(contentType, K9.UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))) {
|
||||
downloadButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (size > K9.MAX_ATTACHMENT_DOWNLOAD_SIZE) {
|
||||
viewButton.setVisibility(View.GONE);
|
||||
downloadButton.setVisibility(View.GONE);
|
||||
@ -171,23 +166,10 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo
|
||||
downloadButton.setOnLongClickListener(this);
|
||||
|
||||
attachmentName.setText(name);
|
||||
attachmentInfo.setText(SizeFormatter.formatSize(mContext, size));
|
||||
new AsyncTask<Void, Void, Bitmap>() {
|
||||
protected Bitmap doInBackground(Void... asyncTaskArgs) {
|
||||
Bitmap previewIcon = getPreviewIcon();
|
||||
return previewIcon;
|
||||
}
|
||||
attachmentInfo.setText(SizeFormatter.formatSize(context, size));
|
||||
|
||||
protected void onPostExecute(Bitmap previewIcon) {
|
||||
if (previewIcon != null) {
|
||||
attachmentIcon.setImageBitmap(previewIcon);
|
||||
} else {
|
||||
attachmentIcon.setImageResource(R.drawable.attached_image_placeholder);
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
|
||||
return firstClassAttachment;
|
||||
ImageView thumbnail = (ImageView) findViewById(R.id.attachment_icon);
|
||||
new LoadAndDisplayThumbnailAsyncTask(thumbnail).execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -207,156 +189,291 @@ public class AttachmentView extends FrameLayout implements OnClickListener, OnLo
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
if (view.getId() == R.id.download) {
|
||||
callback.showFileBrowser(this);
|
||||
callback.pickDirectoryToSaveAttachmentTo(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Bitmap getPreviewIcon() {
|
||||
Bitmap icon = null;
|
||||
try {
|
||||
InputStream input = mContext.getContentResolver().openInputStream(
|
||||
AttachmentProvider.getAttachmentThumbnailUri(mAccount,
|
||||
part.getAttachmentId(),
|
||||
62,
|
||||
62));
|
||||
icon = BitmapFactory.decodeStream(input);
|
||||
input.close();
|
||||
} catch (Exception e) {
|
||||
/*
|
||||
* We don't care what happened, we just return null for the preview icon.
|
||||
*/
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
private void onViewButtonClicked() {
|
||||
if (mMessage != null) {
|
||||
mController.loadAttachment(mAccount, mMessage, part, new Object[] { false, this }, mListener);
|
||||
if (message != null) {
|
||||
controller.loadAttachment(account, message, part, new Object[] {false, this}, listener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void onSaveButtonClicked() {
|
||||
saveFile();
|
||||
}
|
||||
boolean isExternalStorageMounted = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
|
||||
if (!isExternalStorageMounted) {
|
||||
String message = context.getString(R.string.message_view_status_attachment_not_saved);
|
||||
displayMessageToUser(message);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the attachment onto the given path
|
||||
* @param directory: the base dir where the file should be saved.
|
||||
*/
|
||||
public void writeFile(File directory) {
|
||||
try {
|
||||
String filename = Utility.sanitizeFilename(name);
|
||||
File file = Utility.createUniqueFile(directory, filename);
|
||||
Uri uri = AttachmentProvider.getAttachmentUri(mAccount, part.getAttachmentId());
|
||||
InputStream in = mContext.getContentResolver().openInputStream(uri);
|
||||
OutputStream out = new FileOutputStream(file);
|
||||
IOUtils.copy(in, out);
|
||||
out.flush();
|
||||
out.close();
|
||||
in.close();
|
||||
attachmentSaved(file.toString());
|
||||
new MediaScannerNotifier(mContext, file);
|
||||
} catch (IOException ioe) {
|
||||
if (K9.DEBUG) {
|
||||
Log.e(K9.LOG_TAG, "Error saving attachment", ioe);
|
||||
}
|
||||
attachmentNotSaved();
|
||||
if (message != null) {
|
||||
controller.loadAttachment(account, message, part, new Object[] {true, this}, listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* saves the file to the defaultpath setting in the config, or if the config
|
||||
* is not set => to the Environment
|
||||
*/
|
||||
public void writeFile() {
|
||||
writeFile(new File(K9.getAttachmentDefaultPath()));
|
||||
}
|
||||
|
||||
public void saveFile() {
|
||||
//TODO: Can the user save attachments on the internal filesystem or sd card only?
|
||||
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
/*
|
||||
* Abort early if there's no place to save the attachment. We don't want to spend
|
||||
* the time downloading it and then abort.
|
||||
*/
|
||||
Toast.makeText(mContext,
|
||||
mContext.getString(R.string.message_view_status_attachment_not_saved),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if (mMessage != null) {
|
||||
mController.loadAttachment(mAccount, mMessage, part, new Object[] {true, this}, mListener);
|
||||
/**
|
||||
* Saves the attachment as file in the given directory
|
||||
*/
|
||||
public void writeFile(File directory) {
|
||||
try {
|
||||
File file = saveAttachmentWithUniqueFileName(directory);
|
||||
|
||||
displayAttachmentSavedMessage(file.toString());
|
||||
|
||||
MediaScannerNotifier.notify(context, file);
|
||||
} catch (IOException ioe) {
|
||||
if (K9.DEBUG) {
|
||||
Log.e(K9.LOG_TAG, "Error saving attachment", ioe);
|
||||
}
|
||||
displayAttachmentNotSavedMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private File saveAttachmentWithUniqueFileName(File directory) throws IOException {
|
||||
String filename = FileHelper.sanitizeFilename(name);
|
||||
File file = FileHelper.createUniqueFile(directory, filename);
|
||||
|
||||
writeAttachmentToStorage(file);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
private void writeAttachmentToStorage(File file) throws IOException {
|
||||
Uri uri = AttachmentProvider.getAttachmentUri(account, part.getAttachmentId());
|
||||
InputStream in = context.getContentResolver().openInputStream(uri);
|
||||
try {
|
||||
OutputStream out = new FileOutputStream(file);
|
||||
try {
|
||||
IOUtils.copy(in, out);
|
||||
out.flush();
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
} finally {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void showFile() {
|
||||
Uri uri = AttachmentProvider.getAttachmentUriForViewing(mAccount, part.getAttachmentId());
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
// We explicitly set the ContentType in addition to the URI because some attachment viewers (such as Polaris office 3.0.x) choke on documents without a mime type
|
||||
intent.setDataAndType(uri, contentType);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
|
||||
|
||||
try {
|
||||
mContext.startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
Log.e(K9.LOG_TAG, "Could not display attachment of type " + contentType, e);
|
||||
Toast toast = Toast.makeText(mContext, mContext.getString(R.string.message_view_no_viewer, contentType), Toast.LENGTH_LONG);
|
||||
toast.show();
|
||||
}
|
||||
new ViewAttachmentAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the {@link PackageManager} if the phone has an application
|
||||
* installed to view this type of attachment.
|
||||
* If not, {@link #viewButton} is disabled.
|
||||
* This should be done in any place where
|
||||
* attachment.viewButton.setEnabled(enabled); is called.
|
||||
* This method is safe to be called from the UI-thread.
|
||||
*/
|
||||
public void checkViewable() {
|
||||
if (viewButton.getVisibility() == View.GONE) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
if (!viewButton.isEnabled()) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Uri uri = AttachmentProvider.getAttachmentUriForViewing(mAccount, part.getAttachmentId());
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(uri);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
|
||||
if (intent.resolveActivity(mContext.getPackageManager()) == null) {
|
||||
viewButton.setEnabled(false);
|
||||
private Intent getBestViewIntentAndSaveFileIfNecessary() {
|
||||
String inferredMimeType = MimeUtility.getMimeTypeByExtension(name);
|
||||
|
||||
IntentAndResolvedActivitiesCount resolvedIntentInfo;
|
||||
if (MimeUtility.isDefaultMimeType(contentType)) {
|
||||
resolvedIntentInfo = getBestViewIntentForMimeType(inferredMimeType);
|
||||
} else {
|
||||
resolvedIntentInfo = getBestViewIntentForMimeType(contentType);
|
||||
if (!resolvedIntentInfo.hasResolvedActivities() && !inferredMimeType.equals(contentType)) {
|
||||
resolvedIntentInfo = getBestViewIntentForMimeType(inferredMimeType);
|
||||
}
|
||||
// currently we do not cache re result.
|
||||
} catch (Exception e) {
|
||||
Log.e(K9.LOG_TAG, "Cannot resolve activity to determine if we shall show the 'view'-button for an attachment", e);
|
||||
}
|
||||
|
||||
if (!resolvedIntentInfo.hasResolvedActivities()) {
|
||||
resolvedIntentInfo = getBestViewIntentForMimeType(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE);
|
||||
}
|
||||
|
||||
Intent viewIntent;
|
||||
if (resolvedIntentInfo.hasResolvedActivities() && resolvedIntentInfo.containsFileUri()) {
|
||||
try {
|
||||
File tempFile = TemporaryAttachmentStore.getFileForWriting(context, name);
|
||||
writeAttachmentToStorage(tempFile);
|
||||
viewIntent = createViewIntentForFileUri(resolvedIntentInfo.getMimeType(), Uri.fromFile(tempFile));
|
||||
} catch (IOException e) {
|
||||
if (K9.DEBUG) {
|
||||
Log.e(K9.LOG_TAG, "Error while saving attachment to use file:// URI with ACTION_VIEW Intent", e);
|
||||
}
|
||||
viewIntent = createViewIntentForAttachmentProviderUri(MimeUtility.DEFAULT_ATTACHMENT_MIME_TYPE);
|
||||
}
|
||||
} else {
|
||||
viewIntent = resolvedIntentInfo.getIntent();
|
||||
}
|
||||
|
||||
return viewIntent;
|
||||
}
|
||||
|
||||
public void attachmentSaved(final String filename) {
|
||||
Toast.makeText(mContext, String.format(
|
||||
mContext.getString(R.string.message_view_status_attachment_saved), filename),
|
||||
Toast.LENGTH_LONG).show();
|
||||
private IntentAndResolvedActivitiesCount getBestViewIntentForMimeType(String mimeType) {
|
||||
Intent contentUriIntent = createViewIntentForAttachmentProviderUri(mimeType);
|
||||
int contentUriActivitiesCount = getResolvedIntentActivitiesCount(contentUriIntent);
|
||||
|
||||
if (contentUriActivitiesCount > 0) {
|
||||
return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount);
|
||||
}
|
||||
|
||||
File tempFile = TemporaryAttachmentStore.getFile(context, name);
|
||||
Uri tempFileUri = Uri.fromFile(tempFile);
|
||||
Intent fileUriIntent = createViewIntentForFileUri(mimeType, tempFileUri);
|
||||
int fileUriActivitiesCount = getResolvedIntentActivitiesCount(fileUriIntent);
|
||||
|
||||
if (fileUriActivitiesCount > 0) {
|
||||
return new IntentAndResolvedActivitiesCount(fileUriIntent, fileUriActivitiesCount);
|
||||
}
|
||||
|
||||
return new IntentAndResolvedActivitiesCount(contentUriIntent, contentUriActivitiesCount);
|
||||
}
|
||||
|
||||
public void attachmentNotSaved() {
|
||||
Toast.makeText(mContext,
|
||||
mContext.getString(R.string.message_view_status_attachment_not_saved),
|
||||
Toast.LENGTH_LONG).show();
|
||||
private Intent createViewIntentForAttachmentProviderUri(String mimeType) {
|
||||
Uri uri = AttachmentProvider.getAttachmentUriForViewing(account, part.getAttachmentId(), mimeType, name);
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(uri, mimeType);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
addUiIntentFlags(intent);
|
||||
|
||||
return intent;
|
||||
}
|
||||
public AttachmentFileDownloadCallback getCallback() {
|
||||
return callback;
|
||||
|
||||
private Intent createViewIntentForFileUri(String mimeType, Uri uri) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(uri, mimeType);
|
||||
addUiIntentFlags(intent);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
private void addUiIntentFlags(Intent intent) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
|
||||
}
|
||||
|
||||
private int getResolvedIntentActivitiesCount(Intent intent) {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
|
||||
List<ResolveInfo> resolveInfos =
|
||||
packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
|
||||
|
||||
return resolveInfos.size();
|
||||
}
|
||||
|
||||
private void displayAttachmentSavedMessage(final String filename) {
|
||||
String message = context.getString(R.string.message_view_status_attachment_saved, filename);
|
||||
displayMessageToUser(message);
|
||||
}
|
||||
|
||||
private void displayAttachmentNotSavedMessage() {
|
||||
String message = context.getString(R.string.message_view_status_attachment_not_saved);
|
||||
displayMessageToUser(message);
|
||||
}
|
||||
|
||||
private void displayMessageToUser(String message) {
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public void setCallback(AttachmentFileDownloadCallback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
|
||||
public interface AttachmentFileDownloadCallback {
|
||||
/**
|
||||
* This method is called to ask the user to pick a directory to save the attachment to.
|
||||
* <p/>
|
||||
* After the user has selected a directory, the implementation of this interface has to call
|
||||
* {@link #writeFile(File)} on the object supplied as argument in order for the attachment to be saved.
|
||||
*/
|
||||
public void pickDirectoryToSaveAttachmentTo(AttachmentView caller);
|
||||
}
|
||||
|
||||
|
||||
private static class IntentAndResolvedActivitiesCount {
|
||||
private Intent intent;
|
||||
private int activitiesCount;
|
||||
|
||||
IntentAndResolvedActivitiesCount(Intent intent, int activitiesCount) {
|
||||
this.intent = intent;
|
||||
this.activitiesCount = activitiesCount;
|
||||
}
|
||||
|
||||
public Intent getIntent() {
|
||||
return intent;
|
||||
}
|
||||
|
||||
public boolean hasResolvedActivities() {
|
||||
return activitiesCount > 0;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return intent.getType();
|
||||
}
|
||||
|
||||
public boolean containsFileUri() {
|
||||
return "file".equals(intent.getData().getScheme());
|
||||
}
|
||||
}
|
||||
|
||||
private class LoadAndDisplayThumbnailAsyncTask extends AsyncTask<Void, Void, Bitmap> {
|
||||
private final ImageView thumbnail;
|
||||
|
||||
public LoadAndDisplayThumbnailAsyncTask(ImageView thumbnail) {
|
||||
this.thumbnail = thumbnail;
|
||||
}
|
||||
|
||||
protected Bitmap doInBackground(Void... asyncTaskArgs) {
|
||||
return getPreviewIcon();
|
||||
}
|
||||
|
||||
private Bitmap getPreviewIcon() {
|
||||
Bitmap icon = null;
|
||||
try {
|
||||
InputStream input = context.getContentResolver().openInputStream(
|
||||
AttachmentProvider.getAttachmentThumbnailUri(account,
|
||||
part.getAttachmentId(),
|
||||
62,
|
||||
62));
|
||||
icon = BitmapFactory.decodeStream(input);
|
||||
input.close();
|
||||
} catch (Exception e) {
|
||||
// We don't care what happened, we just return null for the preview icon.
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
protected void onPostExecute(Bitmap previewIcon) {
|
||||
if (previewIcon != null) {
|
||||
thumbnail.setImageBitmap(previewIcon);
|
||||
} else {
|
||||
thumbnail.setImageResource(R.drawable.attached_image_placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewAttachmentAsyncTask extends AsyncTask<Void, Void, Intent> {
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
viewButton.setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Intent doInBackground(Void... params) {
|
||||
return getBestViewIntentAndSaveFileIfNecessary();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Intent intent) {
|
||||
viewAttachment(intent);
|
||||
viewButton.setEnabled(true);
|
||||
}
|
||||
|
||||
private void viewAttachment(Intent intent) {
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.e(K9.LOG_TAG, "Could not display attachment of type " + contentType, e);
|
||||
|
||||
String message = context.getString(R.string.message_view_no_viewer, contentType);
|
||||
displayMessageToUser(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ import com.fsck.k9.crypto.PgpData;
|
||||
import com.fsck.k9.fragment.MessageViewFragment;
|
||||
import com.fsck.k9.helper.ClipboardManager;
|
||||
import com.fsck.k9.helper.Contacts;
|
||||
import com.fsck.k9.helper.FileHelper;
|
||||
import com.fsck.k9.helper.HtmlConverter;
|
||||
import com.fsck.k9.helper.UrlEncodingHelper;
|
||||
import com.fsck.k9.helper.Utility;
|
||||
@ -598,8 +599,7 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
|
||||
public void setAttachmentsEnabled(boolean enabled) {
|
||||
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
|
||||
AttachmentView attachment = (AttachmentView) mAttachments.getChildAt(i);
|
||||
attachment.viewButton.setEnabled(enabled);
|
||||
attachment.downloadButton.setEnabled(enabled);
|
||||
attachment.setButtonsEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@ -834,10 +834,10 @@ public class SingleMessageView extends LinearLayout implements OnClickListener,
|
||||
filename += "." + extension;
|
||||
}
|
||||
|
||||
String sanitized = Utility.sanitizeFilename(filename);
|
||||
String sanitized = FileHelper.sanitizeFilename(filename);
|
||||
|
||||
File directory = new File(K9.getAttachmentDefaultPath());
|
||||
File file = Utility.createUniqueFile(directory, sanitized);
|
||||
File file = FileHelper.createUniqueFile(directory, sanitized);
|
||||
FileOutputStream out = new FileOutputStream(file);
|
||||
try {
|
||||
IOUtils.copy(in, out);
|
||||
|
32
tests/src/com/fsck/k9/helper/FileHelperTest.java
Normal file
32
tests/src/com/fsck/k9/helper/FileHelperTest.java
Normal file
@ -0,0 +1,32 @@
|
||||
package com.fsck.k9.helper;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import java.lang.String;
|
||||
|
||||
public class FileHelperTest extends TestCase {
|
||||
|
||||
public void testSanitize1() {
|
||||
checkSanitization(".._bla_", "../bla_");
|
||||
}
|
||||
|
||||
public void testSanitize2() {
|
||||
checkSanitization("_etc_bla", "/etc/bla");
|
||||
}
|
||||
|
||||
public void testSanitize3() {
|
||||
checkSanitization("_пPп", "+пPп");
|
||||
}
|
||||
|
||||
public void testSanitize4() {
|
||||
checkSanitization(".東京_!", ".東京?!");
|
||||
}
|
||||
|
||||
public void testSanitize5() {
|
||||
checkSanitization("Plan 9", "Plan 9");
|
||||
}
|
||||
|
||||
private void checkSanitization(String expected, String actual) {
|
||||
assertEquals(expected, FileHelper.sanitizeFilename(actual));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user