From c26335f3e366110366eb89025a42875bcb7840a9 Mon Sep 17 00:00:00 2001 From: steckbrief Date: Wed, 16 Dec 2015 00:53:04 +0100 Subject: Implements FS#19, FS#84; Introduces ImageResizeException, MessageUtil and distinguishes between image resizing and compressing/saving --- Conversations-Plus-ChangeLog.md | 3 + .../ConversationsPlusApplication.java | 12 ++ .../ConversationsPlusPreferences.java | 8 ++ .../conversationsplus/crypto/PgpEngine.java | 4 +- .../exceptions/FileCopyException.java | 15 ++- .../exceptions/ImageResizeException.java | 16 +++ .../conversationsplus/exceptions/UiException.java | 22 ++++ .../http/HttpDownloadConnection.java | 3 +- .../http/HttpUploadConnection.java | 3 +- .../conversationsplus/persistance/FileBackend.java | 105 ++++------------- .../services/XmppConnectionService.java | 125 ++++----------------- .../ResizePictureUserDecisionListener.java | 92 ++++++++++++++- .../conversationsplus/utils/ImageUtil.java | 60 ++++++++++ .../conversationsplus/utils/MessageUtil.java | 72 ++++++++++++ .../xmpp/jingle/JingleConnection.java | 3 +- src/main/res/values-de/strings.xml | 5 + src/main/res/values/strings.xml | 5 + src/main/res/xml/preferences.xml | 49 ++++---- 18 files changed, 385 insertions(+), 217 deletions(-) create mode 100644 src/main/java/de/thedevstack/conversationsplus/exceptions/ImageResizeException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/exceptions/UiException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java diff --git a/Conversations-Plus-ChangeLog.md b/Conversations-Plus-ChangeLog.md index 0f23c1f1..8cb7db2f 100644 --- a/Conversations-Plus-ChangeLog.md +++ b/Conversations-Plus-ChangeLog.md @@ -1,7 +1,9 @@ ###Conversations+ ChangeLog ####Version 0.0.6 +* Fixes FS#95: NPE when opening message details for failed file transfer * Implements FS#89: Change about information +* Implements FS#84: Setting for location to store received pictures * Implements FS#83: Reload from last received message * Fixes FS#82: Strange layout in share with activity * Fixes FS#81: Interactive message loading causes "jumps" @@ -12,6 +14,7 @@ * Fixes FS#47: Setting "WLAN only" no longer works for received links * Implements FS#26: Introduce dialog to choose whether to send resized picture or original picture * Implements FS#24: Introduce setting for picture resizing +* Implements FS#19: Received and Sent pictures are automatically stored in public picture folder * Partially implements FS#6: Change "Report bug to developer" - Reporting conference changed to c+bugs@conference.thedevstack.de ####Version 0.0.5 diff --git a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java index a9b09551..e4fabda5 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java +++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java @@ -8,6 +8,7 @@ import android.preference.PreferenceManager; import java.io.File; import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.SerialSingleThreadExecutor; /** * This class is used to provide static access to the applicationcontext. @@ -18,6 +19,9 @@ public class ConversationsPlusApplication extends Application { */ private static ConversationsPlusApplication instance; + private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor(); + private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor(); + /** * Initializes the application and saves its instance. */ @@ -36,6 +40,14 @@ public class ConversationsPlusApplication extends Application { return ConversationsPlusApplication.instance; } + public static void executeFileAdding(Runnable r) { + getInstance().mFileAddingExecutor.execute(r); + } + + public static void executeDatabaseOperation(Runnable r) { + getInstance().mDatabaseExecutor.execute(r); + } + /** * Returns the application's context. * @return Context the application's context diff --git a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusPreferences.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusPreferences.java index b7b7fe47..17829998 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusPreferences.java +++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusPreferences.java @@ -14,6 +14,14 @@ public class ConversationsPlusPreferences extends Settings { private static ConversationsPlusPreferences instance; private final SharedPreferences sharedPreferences; + public static String imgTransferFolder() { + return getString("img_transfer_folder", getString("app_name", "Conversations+")); + } + + public static String fileTransferFolder() { + return getString("file_transfer_folder", getString("app_name", "Conversations+")); + } + public static UserDecision resizePicture() { return getEnumFromStringPref("resize_picture", UserDecision.ASK); } diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java index fe7eefdf..08aa8272 100644 --- a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java @@ -24,6 +24,8 @@ import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.http.HttpConnectionManager; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.ui.UiCallback; +import de.thedevstack.conversationsplus.utils.MessageUtil; + import android.app.PendingIntent; import android.content.Intent; import android.net.Uri; @@ -101,7 +103,7 @@ public class PgpEngine { OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: URL url = message.getFileParams().url; - FileBackend.updateFileParams(message, url); + MessageUtil.updateFileParams(message, url); message.setEncryption(Message.ENCRYPTION_DECRYPTED); PgpEngine.this.mXmppConnectionService .updateMessage(message); diff --git a/src/main/java/de/thedevstack/conversationsplus/exceptions/FileCopyException.java b/src/main/java/de/thedevstack/conversationsplus/exceptions/FileCopyException.java index 58363c0f..858b4563 100644 --- a/src/main/java/de/thedevstack/conversationsplus/exceptions/FileCopyException.java +++ b/src/main/java/de/thedevstack/conversationsplus/exceptions/FileCopyException.java @@ -1,14 +1,13 @@ package de.thedevstack.conversationsplus.exceptions; -public class FileCopyException extends Exception { +public class FileCopyException extends UiException { private static final long serialVersionUID = -1010013599132881427L; - private int resId; - public FileCopyException(int resId) { - this.resId = resId; - } + public FileCopyException(int resId) { + super(resId); + } - public int getResId() { - return resId; - } + public FileCopyException(int resId, Throwable e) { + super(resId, e); + } } \ No newline at end of file diff --git a/src/main/java/de/thedevstack/conversationsplus/exceptions/ImageResizeException.java b/src/main/java/de/thedevstack/conversationsplus/exceptions/ImageResizeException.java new file mode 100644 index 00000000..b5786990 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/exceptions/ImageResizeException.java @@ -0,0 +1,16 @@ +package de.thedevstack.conversationsplus.exceptions; + +/** + * Created by tzur on 15.12.2015. + */ +public class ImageResizeException extends UiException { + private static final long serialVersionUID = -1010013599112881427L; + + public ImageResizeException(int resId) { + super(resId); + } + + public ImageResizeException(int resId, Throwable e) { + super(resId, e); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/exceptions/UiException.java b/src/main/java/de/thedevstack/conversationsplus/exceptions/UiException.java new file mode 100644 index 00000000..b05c5025 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/exceptions/UiException.java @@ -0,0 +1,22 @@ +package de.thedevstack.conversationsplus.exceptions; + +/** + * Exception to be shown in UI. + */ +public class UiException extends Exception { + private static final long serialVersionUID = -1010015239132881427L; + private int resId; + + public UiException(int resId) { + this.resId = resId; + } + + public UiException(int resId, Throwable e) { + super(e); + this.resId = resId; + } + + public int getResId() { + return resId; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java index c76ed6d7..d8859df5 100644 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java @@ -25,6 +25,7 @@ import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.MessageUtil; public class HttpDownloadConnection implements Transferable { @@ -236,7 +237,7 @@ public class HttpDownloadConnection implements Transferable { private void updateImageBounds() { message.setType(Message.TYPE_FILE); - FileBackend.updateFileParams(message, mUrl); + MessageUtil.updateFileParams(message, mUrl); mXmppConnectionService.updateMessage(message); } diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java index d5d7ad02..58488c99 100644 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpUploadConnection.java @@ -21,6 +21,7 @@ import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.ui.UiCallback; import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.StreamUtil; import de.thedevstack.conversationsplus.utils.Xmlns; import de.thedevstack.conversationsplus.xml.Element; @@ -164,7 +165,7 @@ public class HttpUploadConnection implements Transferable { if (key != null) { mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key)); } - FileBackend.updateFileParams(message, mGetUrl); + MessageUtil.updateFileParams(message, mGetUrl); message.setTransferable(null); message.setCounterpart(message.getConversation().getJid().toBareJid()); if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java b/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java index d442abe2..0a118ccb 100644 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java @@ -21,18 +21,18 @@ import android.webkit.MimeTypeMap; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Transferable; import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.exceptions.FileCopyException; import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.StreamUtil; public final class FileBackend { - private static int IMAGE_SIZE = 1920; - private static final SimpleDateFormat imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); public static DownloadableFile getFile(Message message) { @@ -63,29 +63,35 @@ public final class FileBackend { return new DownloadableFile(path); } else { if (Arrays.asList(Transferable.VALID_IMAGE_EXTENSIONS).contains(extension)) { - return new DownloadableFile(getConversationsFileDirectory() + path); - } else { return new DownloadableFile(getConversationsImageDirectory() + path); + } else { + return new DownloadableFile(getConversationsFileDirectory() + path); } } } } public static String getConversationsFileDirectory() { - return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/"; + return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + ConversationsPlusPreferences.fileTransferFolder() + File.separator; } public static String getConversationsImageDirectory() { - return Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES).getAbsolutePath() - + "/Conversations/"; + return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + File.separator + ConversationsPlusPreferences.imgTransferFolder() + File.separator; } + private static String getPrivateFileDirectoryPath() { + return ConversationsPlusApplication.getPrivateFilesDir().getAbsolutePath(); + } + + private static String getPrivateImageDirectoryPath() { + return FileBackend.getPrivateFileDirectoryPath() + File.separator + "Images" + File.separator; + } + public static DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { Logging.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage"); String mime = ConversationsPlusApplication.getInstance().getContentResolver().getType(uri); String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); - message.setRelativeFilePath(message.getUuid() + "." + extension); + message.setRelativeFilePath(FileBackend.getPrivateFileDirectoryPath() + message.getUuid() + "." + extension); DownloadableFile file = getFile(message); file.getParentFile().mkdirs(); OutputStream os = null; @@ -113,68 +119,30 @@ public final class FileBackend { return file; } - public static DownloadableFile copyImageToPrivateStorage(Message message, Uri image) - throws FileCopyException { - return copyImageToPrivateStorage(message, image, 0); - } - - private static DownloadableFile copyImageToPrivateStorage(Message message, - Uri image, int sampleSize) throws FileCopyException { + public static DownloadableFile compressImageAndCopyToPrivateStorage(Message message, Bitmap scaledBitmap) throws FileCopyException { + message.setRelativeFilePath(FileBackend.getPrivateImageDirectoryPath() + message.getUuid() + ".webp"); DownloadableFile file = getFile(message); file.getParentFile().mkdirs(); - InputStream is = null; OutputStream os = null; try { file.createNewFile(); - is = StreamUtil.openInputStreamFromContentResolver(image); - os = new FileOutputStream(file); - - Bitmap originalBitmap; - BitmapFactory.Options options = new BitmapFactory.Options(); - int inSampleSize = (int) Math.pow(2, sampleSize); - Logging.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize); - options.inSampleSize = inSampleSize; - originalBitmap = BitmapFactory.decodeStream(is, null, options); - is.close(); - if (originalBitmap == null) { - throw new FileCopyException(R.string.error_not_an_image_file); - } - Bitmap scaledBitmap = ImageUtil.resize(originalBitmap, IMAGE_SIZE); - int rotation = ImageUtil.getRotation(image); - if (rotation > 0) { - scaledBitmap = ImageUtil.rotate(scaledBitmap, rotation); - } + os = new FileOutputStream(file); boolean success = scaledBitmap.compress(Bitmap.CompressFormat.WEBP, 75, os); if (!success) { throw new FileCopyException(R.string.error_compressing_image); } os.flush(); - long size = file.getSize(); - int width = scaledBitmap.getWidth(); - int height = scaledBitmap.getHeight(); - message.setBody(Long.toString(size) + '|' + width + '|' + height); - return file; - } catch (FileNotFoundException e) { - throw new FileCopyException(R.string.error_file_not_found); - } catch (IOException e) { - e.printStackTrace(); - throw new FileCopyException(R.string.error_io_exception); + } catch (IOException e) { + throw new FileCopyException(R.string.error_io_exception, e); } catch (SecurityException e) { - throw new FileCopyException(R.string.error_security_exception_during_image_copy); - } catch (OutOfMemoryError e) { - ++sampleSize; - if (sampleSize <= 3) { - return copyImageToPrivateStorage(message, image, sampleSize); - } else { - throw new FileCopyException(R.string.error_out_of_memory); - } - } catch (NullPointerException e) { + throw new FileCopyException(R.string.error_security_exception_during_image_copy); + } catch (NullPointerException e) { throw new FileCopyException(R.string.error_io_exception); } finally { StreamUtil.close(os); - StreamUtil.close(is); } + return file; } public static Uri getTakePhotoUri() { @@ -195,33 +163,6 @@ public final class FileBackend { return Uri.parse("file://" + file.getAbsolutePath()); } - public static void updateFileParams(Message message) { - updateFileParams(message,null); - } - - public static void updateFileParams(Message message, URL url) { - DownloadableFile file = getFile(message); - if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(file.getAbsolutePath(), options); - int imageHeight = options.outHeight; - int imageWidth = options.outWidth; - if (url == null) { - message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight); - } else { - message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight); - } - } else { - if (url != null) { - message.setBody(url.toString()+"|"+Long.toString(file.getSize())); - } else { - message.setBody(Long.toString(file.getSize())); - } - } - - } - public static boolean isFileAvailable(Message message) { return getFile(message).exists(); } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java index 159fcf44..7472ad21 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java @@ -53,10 +53,13 @@ import de.duenndns.ssl.MemorizingTrustManager; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.exceptions.FileCopyException; +import de.thedevstack.conversationsplus.exceptions.UiException; import de.thedevstack.conversationsplus.utils.AvatarUtil; import de.thedevstack.conversationsplus.utils.FileHelper; import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.MessageUtil; import de.tzur.conversations.Settings; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; @@ -86,7 +89,6 @@ import de.thedevstack.conversationsplus.utils.ExceptionHelper; import de.thedevstack.conversationsplus.utils.OnPhoneContactsLoadedListener; import de.thedevstack.conversationsplus.utils.PRNGFixes; import de.thedevstack.conversationsplus.utils.PhoneHelper; -import de.thedevstack.conversationsplus.utils.SerialSingleThreadExecutor; import de.thedevstack.conversationsplus.utils.Xmlns; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnBindListener; @@ -130,9 +132,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } }; - private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor(); - private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor(); - private final IBinder mBinder = new XmppConnectionBinder(); private final List conversations = new CopyOnWriteArrayList<>(); private final FileObserver fileObserver = new FileObserver( @@ -375,110 +374,33 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_FILE); String path = FileHelper.getRealPathFromUri(uri); - if (path!=null) { + if (path != null) { message.setRelativeFilePath(path); - FileBackend.updateFileParams(message); + MessageUtil.updateFileParams(message); if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { getPgpEngine().encrypt(message, callback); } else { callback.success(message); } } else { - mFileAddingExecutor.execute(new Runnable() { - @Override - public void run() { - try { - FileBackend.copyFileToPrivateStorage(message, uri); - FileBackend.updateFileParams(message); - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - getPgpEngine().encrypt(message, callback); - } else { - callback.success(message); - } - } catch (FileCopyException e) { - callback.error(e.getResId(), message); - } - } - }); - } - } - - public void attachImageToConversationWithoutResizing(final Conversation conversation, final Uri uri, final UiCallback callback) { - final Message message; - final boolean forceEncryption = ConversationsPlusPreferences.forceEncryption(); - if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", - Message.ENCRYPTION_DECRYPTED); - } else { - message = new Message(conversation, "", - conversation.getNextEncryption(forceEncryption)); - } - message.setCounterpart(conversation.getNextCounterpart()); - message.setType(Message.TYPE_IMAGE); - mFileAddingExecutor.execute(new Runnable() { - @Override - public void run() { - InputStream is = null; - try { - is = ConversationsPlusApplication.getInstance().getContentResolver().openInputStream(uri); - long imageSize = is.available(); - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(is, null, options); - int imageHeight = options.outHeight; - int imageWidth = options.outWidth; - message.setRelativeFilePath(FileHelper.getRealPathFromUri(uri)); - message.setBody(Long.toString(imageSize) + '|' + imageWidth + '|' + imageHeight); - callback.success(message); - } catch (FileNotFoundException e) { - Logging.e("pictureresize", "File not found to send not resized. " + e.getMessage()); - callback.error(R.string.error_file_not_found, message); - } catch (IOException e) { - Logging.e("pictureresize", "Error while sending not resized picture. " + e.getMessage()); - callback.error(R.string.error_io_exception, message); - } finally { - if (null != is) { - try { - is.close(); - } catch (IOException e) { - Logging.w("pictureresize", "Error while closing stream for sending not resized picture. " + e.getMessage()); + ConversationsPlusApplication.executeFileAdding(new Runnable() { + @Override + public void run() { + try { + FileBackend.copyFileToPrivateStorage(message, uri); + MessageUtil.updateFileParams(message); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); } + } catch (FileCopyException e) { + callback.error(e.getResId(), message); } } - } - }); - } - - public void attachImageToConversation(final Conversation conversation, - final Uri uri, final UiCallback callback) { - final Message message; - final boolean forceEncryption = ConversationsPlusPreferences.forceEncryption(); - if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", - Message.ENCRYPTION_DECRYPTED); - } else { - message = new Message(conversation, "", - conversation.getNextEncryption(forceEncryption)); - } - message.setCounterpart(conversation.getNextCounterpart()); - message.setType(Message.TYPE_IMAGE); - mFileAddingExecutor.execute(new Runnable() { - - @Override - public void run() { - try { - FileBackend.copyImageToPrivateStorage(message, uri); - if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) { - getPgpEngine().encrypt(message, callback); - } else { - callback.success(message); - } - } catch (final FileCopyException e) { - callback.error(e.getResId(), message); - } - } - }); - } + }); + } + } public Conversation find(Bookmark bookmark) { return find(bookmark.getAccount(), bookmark.getJid()); @@ -1019,7 +941,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa updateConversationUi(); } }; - mDatabaseExecutor.execute(runnable); + ConversationsPlusApplication.executeDatabaseOperation(runnable); } } @@ -1123,7 +1045,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } }; - mDatabaseExecutor.execute(runnable); + ConversationsPlusApplication.executeDatabaseOperation(runnable); } public List getAccounts() { @@ -2390,8 +2312,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa databaseBackend.writeRoster(account.getRoster()); } }; - mDatabaseExecutor.execute(runnable); - + ConversationsPlusApplication.executeDatabaseOperation(runnable); } public List getKnownHosts() { diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java index ae2b3b79..a9c245ed 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java @@ -1,18 +1,31 @@ package de.thedevstack.conversationsplus.ui.listeners; import android.app.PendingIntent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import android.widget.Toast; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.enums.UserDecision; +import de.thedevstack.conversationsplus.exceptions.UiException; +import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.ui.UiCallback; import de.thedevstack.conversationsplus.ui.XmppActivity; +import de.thedevstack.conversationsplus.utils.FileHelper; +import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.MessageUtil; /** * Created by tzur on 31.10.2015. @@ -81,13 +94,88 @@ public class ResizePictureUserDecisionListener implements UserDecisionListener { @Override public void onYes() { this.showPrepareFileToast(); - xmppConnectionService.attachImageToConversation(this.conversation, this.uri, this.callback); + final Message message; + final boolean forceEncryption = ConversationsPlusPreferences.forceEncryption(); + if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) { + message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); + } else { + message = new Message(conversation, "", conversation.getNextEncryption(forceEncryption)); + } + message.setCounterpart(conversation.getNextCounterpart()); + message.setType(Message.TYPE_IMAGE); + ConversationsPlusApplication.executeFileAdding(new Runnable() { + + @Override + public void run() { + try { + Bitmap resizedAndRotatedImage = ImageUtil.resizeAndRotateImage(uri); + DownloadableFile file = FileBackend.compressImageAndCopyToPrivateStorage(message, resizedAndRotatedImage); + String filePath = file.getAbsolutePath(); + long imageSize = file.getSize(); + int imageWidth = resizedAndRotatedImage.getWidth(); + int imageHeight = resizedAndRotatedImage.getHeight(); + MessageUtil.updateMessageWithImageDetails(message, filePath, imageSize, imageWidth, imageHeight); + if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) { + xmppConnectionService.getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); + } + } catch (final UiException e) { + Logging.e("pictureresizesending", "Error while sending resized picture. " + e.getMessage()); + callback.error(e.getResId(), message); + } + } + }); } @Override public void onNo() { this.showPrepareFileToast(); - xmppConnectionService.attachImageToConversationWithoutResizing(this.conversation, this.uri, this.callback); + final Message message; + final boolean forceEncryption = ConversationsPlusPreferences.forceEncryption(); + if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) { + message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); + } else { + message = new Message(conversation, "", conversation.getNextEncryption(forceEncryption)); + } + message.setCounterpart(conversation.getNextCounterpart()); + message.setType(Message.TYPE_IMAGE); + ConversationsPlusApplication.executeFileAdding(new Runnable() { + @Override + public void run() { + InputStream is = null; + try { + is = ConversationsPlusApplication.getInstance().getContentResolver().openInputStream(uri); + long imageSize = is.available(); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, options); + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + String filePath = FileHelper.getRealPathFromUri(uri); + MessageUtil.updateMessageWithImageDetails(message, filePath, imageSize, imageWidth, imageHeight); + if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) { + xmppConnectionService.getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); + } + } catch (FileNotFoundException e) { + Logging.e("picturesending", "File not found to send not resized. " + e.getMessage()); + callback.error(R.string.error_file_not_found, message); + } catch (IOException e) { + Logging.e("picturesending", "Error while sending not resized picture. " + e.getMessage()); + callback.error(R.string.error_io_exception, message); + } finally { + if (null != is) { + try { + is.close(); + } catch (IOException e) { + Logging.w("picturesending", "Error while closing stream for sending not resized picture. " + e.getMessage()); + } + } + } + } + }); } @Override diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java index 7e8cacd0..9250c432 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java @@ -11,17 +11,24 @@ import android.util.LruCache; import java.io.File; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.exceptions.FileCopyException; +import de.thedevstack.conversationsplus.exceptions.ImageResizeException; import de.thedevstack.conversationsplus.persistance.FileBackend; /** * This util provides */ public final class ImageUtil { + + private static int IMAGE_SIZE = 1920; private static LruCache BITMAP_CACHE; /** @@ -103,6 +110,59 @@ public final class ImageUtil { } } + /** + * Resizes and rotates an image given by uri and returns the bitmap. + * @param image the uri of the image to be resized and rotated + * @return resized and rotated bitmap + * @throws ImageResizeException + */ + public static Bitmap resizeAndRotateImage(Uri image) throws ImageResizeException { + return ImageUtil.resizeAndRotateImage(image, 0); + } + + /** + * Resizes and rotates an image given by uri and returns the bitmap. + * @param image the uri of the image to be resized and rotated + * @return resized and rotated bitmap + * @throws ImageResizeException + */ + private static Bitmap resizeAndRotateImage(Uri image, int sampleSize) throws ImageResizeException { + InputStream imageInputStream = null; + try { + imageInputStream = StreamUtil.openInputStreamFromContentResolver(image); + Bitmap originalBitmap; + BitmapFactory.Options options = new BitmapFactory.Options(); + int inSampleSize = (int) Math.pow(2, sampleSize); + Logging.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize); + options.inSampleSize = inSampleSize; + originalBitmap = BitmapFactory.decodeStream(imageInputStream, null, options); + imageInputStream.close(); + if (originalBitmap == null) { + throw new ImageResizeException(R.string.error_not_an_image_file); + } + Bitmap scaledBitmap = ImageUtil.resize(originalBitmap, IMAGE_SIZE); + int rotation = ImageUtil.getRotation(image); + if (rotation > 0) { + scaledBitmap = ImageUtil.rotate(scaledBitmap, rotation); + } + + return scaledBitmap; + } catch (FileNotFoundException e) { + throw new ImageResizeException(R.string.error_file_not_found); + } catch (IOException e) { + throw new ImageResizeException(R.string.error_io_exception); + } catch (OutOfMemoryError e) { + ++sampleSize; + if (sampleSize <= 3) { + return resizeAndRotateImage(image, sampleSize); + } else { + throw new ImageResizeException(R.string.error_out_of_memory); + } + } finally { + StreamUtil.close(imageInputStream); + } + } + /** * Returns the rotation from the exif information of an image identified with the given uri. * The orientation is retrieved by parsing the stream of the image. diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java new file mode 100644 index 00000000..c04ebdb8 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java @@ -0,0 +1,72 @@ +package de.thedevstack.conversationsplus.utils; + +import android.graphics.BitmapFactory; + +import java.net.URL; + +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.persistance.FileBackend; + +/** + * Created by tzur on 15.12.2015. + */ +public final class MessageUtil { + public static void updateMessageWithImageDetails(Message message, String filePath, long size, int imageWidth, int imageHeight) { + message.setRelativeFilePath(filePath); + MessageUtil.updateMessageBodyWithImageParams(message, size, imageWidth, imageHeight); + } + + public static void updateFileParams(Message message) { + updateFileParams(message, null); + } + + public static void updateFileParams(Message message, URL url) { + DownloadableFile file = FileBackend.getFile(message); + int imageWidth = -1; + int imageHeight = -1; + if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + imageHeight = options.outHeight; + imageWidth = options.outWidth; + } + + MessageUtil.updateMessageBodyWithFileParams(message, url, file.getSize(), imageWidth, imageHeight); + } + + private static void updateMessageBodyWithFileParams(Message message, URL url, long fileSize, int imageWidth, int imageHeight) { + message.setBody(MessageUtil.getMessageBodyWithImageParams(url, fileSize, imageWidth, imageHeight)); + } + + private static void updateMessageBodyWithImageParams(Message message, long size, int imageWidth, int imageHeight) { + MessageUtil.updateMessageBodyWithImageParams(message, null, size, imageWidth, imageHeight); + } + + private static void updateMessageBodyWithImageParams(Message message, URL url, long size, int imageWidth, int imageHeight) { + message.setBody(MessageUtil.getMessageBodyWithImageParams(url, size, imageWidth, imageHeight)); + } + + private static String getMessageBodyWithImageParams(URL url, long size, int imageWidth, int imageHeight) { + StringBuilder sb = new StringBuilder(); + if (null != url) { + sb.append(url.toString()); + sb.append('|'); + } + sb.append(size); + if (-1 < imageWidth) { + sb.append('|'); + sb.append(imageWidth); + } + if (-1 < imageHeight) { + sb.append('|'); + sb.append(imageHeight); + } + return sb.toString(); + } + + private MessageUtil() { + // Static helper class + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java index 6ddf99b8..7885cc0f 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java @@ -23,6 +23,7 @@ import de.thedevstack.conversationsplus.entities.TransferablePlaceholder; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -92,7 +93,7 @@ public class JingleConnection implements Transferable { JingleConnection.this.mXmppConnectionService .getNotificationService().push(message); } - FileBackend.updateFileParams(message); + MessageUtil.updateFileParams(message); mXmppConnectionService.databaseBackend.createMessage(message); mXmppConnectionService.markMessage(message, Message.STATUS_RECEIVED); diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 29b2ec88..e6f6273e 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -505,4 +505,9 @@ In Zwischenablage kopiert Zeige logcat Ausgabe Zeigt die Ausgabe von logcat an. Hilfreich für die Fehlersuche. + Ordnername, um eingehnde Datein zu speichern + Unterordner des globalen Dateiordners, um eingehende Dateien zu speichern. + Ordnername, um eingehende Bilder zu speichern + Unterordner des globalen Bilderordners, um eingehende Bilder zu speichern. + Dateiübertragung diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 96a89c9c..3431e831 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -538,4 +538,9 @@ Show logcat output Shows the output of logcat. This is useful for debugging. c+bugs@conference.thedevstack.de + Folder to save incoming files + This is the subdirectory for incoming files. + Folder to save incoming pictures + This is the subdirectory in the pictures directory for incoming files. + File Transfer diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index a90385bc..0232fe02 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -17,6 +17,26 @@ android:summary="@string/pref_xmpp_resource_summary" android:title="@string/pref_xmpp_resource" /> + + + + + + @@ -42,6 +62,16 @@ android:title="@string/pref_accept_files_download" /> + + - - - - -