From 8838a5094812e0540ccfef3334e49e1a5c1a564b Mon Sep 17 00:00:00 2001 From: steckbrief Date: Fri, 6 Nov 2015 20:23:43 +0100 Subject: FileBackend splitted into several util classes for separate concerns: AvatarUtil, StreamUtil, ImageUtil. Unused imports removed. --- .../ConversationsPlusApplication.java | 21 ++ .../conversationsplus/crypto/PgpEngine.java | 16 +- .../exceptions/FileCopyException.java | 14 + .../http/HttpDownloadConnection.java | 5 +- .../http/HttpUploadConnection.java | 9 +- .../conversationsplus/parser/MessageParser.java | 4 +- .../conversationsplus/parser/PresenceParser.java | 3 +- .../conversationsplus/persistance/FileBackend.java | 401 ++------------------- .../conversationsplus/services/AvatarService.java | 40 +- .../services/NotificationService.java | 4 +- .../services/XmppConnectionService.java | 260 ++++++------- .../conversationsplus/ui/ConversationActivity.java | 3 +- .../conversationsplus/ui/ConversationFragment.java | 7 +- .../ui/PublishProfilePictureActivity.java | 4 +- .../conversationsplus/ui/XmppActivity.java | 8 +- .../ui/adapter/MessageAdapter.java | 10 +- .../conversationsplus/utils/AvatarUtil.java | 162 +++++++++ .../conversationsplus/utils/ImageUtil.java | 313 ++++++++++++++++ .../conversationsplus/utils/StreamUtil.java | 48 +++ .../xmpp/jingle/JingleConnection.java | 9 +- .../xmpp/jingle/JingleInbandTransport.java | 5 +- .../xmpp/jingle/JingleSocks5Transport.java | 5 +- 22 files changed, 771 insertions(+), 580 deletions(-) create mode 100644 src/main/java/de/thedevstack/conversationsplus/exceptions/FileCopyException.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/AvatarUtil.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java create mode 100644 src/main/java/de/thedevstack/conversationsplus/utils/StreamUtil.java (limited to 'src') diff --git a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java index 9ec39de8..a9b09551 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java +++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java @@ -7,6 +7,8 @@ import android.preference.PreferenceManager; import java.io.File; +import de.thedevstack.conversationsplus.utils.ImageUtil; + /** * This class is used to provide static access to the applicationcontext. */ @@ -23,8 +25,13 @@ public class ConversationsPlusApplication extends Application { super.onCreate(); ConversationsPlusApplication.instance = this; ConversationsPlusPreferences.init(PreferenceManager.getDefaultSharedPreferences(getAppContext())); + ImageUtil.initBitmapCache(); } + /** + * Returns the instance of the application + * @return this application instance + */ public static ConversationsPlusApplication getInstance() { return ConversationsPlusApplication.instance; } @@ -45,6 +52,11 @@ public class ConversationsPlusApplication extends Application { return ConversationsPlusApplication.instance.getFilesDir(); } + /** + * Returns the version of the application. + * @see android.content.pm.PackageInfo#versionName + * @return a string representation of the version stored in packageInfo + */ public static String getVersion() { final String packageName = ConversationsPlusApplication.getAppContext().getPackageName(); if (packageName != null) { @@ -58,10 +70,19 @@ public class ConversationsPlusApplication extends Application { } } + /** + * Returns the application's name. + * @return the name as it is defined in R.string.app_name + */ public static String getName() { return ConversationsPlusApplication.getAppContext().getString(R.string.app_name); } + /** + * Returns the name and the version of this application. + * @see #getName() and #getVersion + * @return a concatination of name and version with a whitespace in between + */ public static String getNameAndVersion() { return getName() + " " + getVersion(); } diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java index 531aca32..fe7eefdf 100644 --- a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java @@ -14,7 +14,7 @@ import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; -import de.tzur.conversations.Settings; +import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Contact; @@ -87,10 +87,8 @@ public class PgpEngine { }); } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { try { - final DownloadableFile inputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, false); - final DownloadableFile outputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, true); + final DownloadableFile inputFile = FileBackend.getFile(message, false); + final DownloadableFile outputFile = FileBackend.getFile(message, true); outputFile.getParentFile().mkdirs(); outputFile.createNewFile(); InputStream is = new FileInputStream(inputFile); @@ -103,7 +101,7 @@ public class PgpEngine { OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: URL url = message.getFileParams().url; - mXmppConnectionService.getFileBackend().updateFileParams(message,url); + FileBackend.updateFileParams(message, url); message.setEncryption(Message.ENCRYPTION_DECRYPTED); PgpEngine.this.mXmppConnectionService .updateMessage(message); @@ -194,10 +192,8 @@ public class PgpEngine { }); } else { try { - DownloadableFile inputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, true); - DownloadableFile outputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, false); + DownloadableFile inputFile = FileBackend.getFile(message, true); + DownloadableFile outputFile = FileBackend.getFile(message, false); outputFile.getParentFile().mkdirs(); outputFile.createNewFile(); InputStream is = new FileInputStream(inputFile); diff --git a/src/main/java/de/thedevstack/conversationsplus/exceptions/FileCopyException.java b/src/main/java/de/thedevstack/conversationsplus/exceptions/FileCopyException.java new file mode 100644 index 00000000..58363c0f --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/exceptions/FileCopyException.java @@ -0,0 +1,14 @@ +package de.thedevstack.conversationsplus.exceptions; + +public class FileCopyException extends Exception { + private static final long serialVersionUID = -1010013599132881427L; + private int resId; + + public FileCopyException(int resId) { + this.resId = resId; + } + + public int getResId() { + return resId; + } +} \ No newline at end of file diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java index b7f3cc67..b958e57e 100644 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java @@ -22,6 +22,7 @@ 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.persistance.FileBackend; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.utils.CryptoHelper; @@ -81,7 +82,7 @@ public class HttpDownloadConnection implements Transferable { extension = lastPart; } message.setRelativeFilePath(message.getUuid()+"."+extension); - this.file = mXmppConnectionService.getFileBackend().getFile(message, false); + this.file = FileBackend.getFile(message, false); String reference = mUrl.getRef(); if (reference != null && reference.length() == 96) { this.file.setKey(CryptoHelper.hexToBytes(reference)); @@ -235,7 +236,7 @@ public class HttpDownloadConnection implements Transferable { private void updateImageBounds() { message.setType(Message.TYPE_FILE); - mXmppConnectionService.getFileBackend().updateFileParams(message, mUrl); + FileBackend.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 a9682718..05d3a129 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.StreamUtil; import de.thedevstack.conversationsplus.utils.Xmlns; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; @@ -85,7 +86,7 @@ public class HttpUploadConnection implements Transferable { message.setTransferable(this); mXmppConnectionService.markMessage(message,Message.STATUS_UNSEND); this.account = message.getConversation().getAccount(); - this.file = mXmppConnectionService.getFileBackend().getFile(message, false); + this.file = FileBackend.getFile(message, false); this.file.setExpectedSize(this.file.getSize()); if (Config.ENCRYPT_ON_HTTP_UPLOADED) { @@ -163,7 +164,7 @@ public class HttpUploadConnection implements Transferable { if (key != null) { mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key)); } - mXmppConnectionService.getFileBackend().updateFileParams(message, mGetUrl); + FileBackend.updateFileParams(message, mGetUrl); message.setTransferable(null); message.setCounterpart(message.getConversation().getJid().toBareJid()); if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { @@ -193,8 +194,8 @@ public class HttpUploadConnection implements Transferable { Log.d(Config.LOGTAG, e.getMessage()); fail(); } finally { - FileBackend.close(is); - FileBackend.close(os); + StreamUtil.close(is); + StreamUtil.close(os); if (connection != null) { connection.disconnect(); } diff --git a/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java b/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java index aa04df2b..f346eafc 100644 --- a/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java +++ b/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java @@ -16,6 +16,7 @@ import de.thedevstack.conversationsplus.entities.MucOptions; import de.thedevstack.conversationsplus.http.HttpConnectionManager; import de.thedevstack.conversationsplus.services.MessageArchiveService; import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.AvatarUtil; import de.thedevstack.conversationsplus.utils.CryptoHelper; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnMessagePacketReceived; @@ -23,7 +24,6 @@ import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; import de.thedevstack.conversationsplus.xmpp.jid.Jid; import de.thedevstack.conversationsplus.xmpp.pep.Avatar; import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; -import de.tzur.conversations.Settings; public class MessageParser extends AbstractParser implements OnMessagePacketReceived { @@ -143,7 +143,7 @@ public class MessageParser extends AbstractParser implements Avatar avatar = Avatar.parseMetadata(items); if (avatar != null) { avatar.owner = from; - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { + if (AvatarUtil.isAvatarCached(avatar)) { if (account.getJid().toBareJid().equals(from)) { if (account.setAvatar(avatar.getFilename())) { mXmppConnectionService.databaseBackend.updateAccount(account); diff --git a/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java b/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java index 888845c2..ce52bbeb 100644 --- a/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java +++ b/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java @@ -10,6 +10,7 @@ import de.thedevstack.conversationsplus.entities.MucOptions; import de.thedevstack.conversationsplus.entities.Presences; import de.thedevstack.conversationsplus.generator.PresenceGenerator; import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.utils.AvatarUtil; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnPresencePacketReceived; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -58,7 +59,7 @@ public class PresenceParser extends AbstractParser implements Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); if (avatar != null && !contact.isSelf()) { avatar.owner = from.toBareJid(); - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { + if (AvatarUtil.isAvatarCached(avatar)) { if (contact.setAvatar(avatar)) { mXmppConnectionService.getAvatarService().clear(contact); mXmppConnectionService.updateConversationUi(); diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java b/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java index da5cbf09..7ee67a15 100644 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/FileBackend.java @@ -1,7 +1,5 @@ package de.thedevstack.conversationsplus.persistance; -import java.io.ByteArrayOutputStream; -import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -9,58 +7,39 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; -import java.security.DigestOutputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.Locale; -import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.RectF; -import android.media.ExifInterface; import android.net.Uri; import android.os.Environment; -import android.provider.MediaStore; -import android.util.Base64; -import android.util.Base64OutputStream; import android.util.Log; import android.webkit.MimeTypeMap; -import android.widget.ImageView; import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; 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.services.XmppConnectionService; -import de.thedevstack.conversationsplus.utils.CryptoHelper; -import de.thedevstack.conversationsplus.utils.ExifHelper; -import de.thedevstack.conversationsplus.utils.FileHelper; -import de.thedevstack.conversationsplus.xmpp.pep.Avatar; +import de.thedevstack.conversationsplus.exceptions.FileCopyException; +import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.StreamUtil; -public class FileBackend { +public final class FileBackend { private static int IMAGE_SIZE = 1920; - private final SimpleDateFormat imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); + private static final SimpleDateFormat imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); - private XmppConnectionService mXmppConnectionService; - - public FileBackend(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - public DownloadableFile getFile(Message message) { + public static DownloadableFile getFile(Message message) { return getFile(message, true); } - public DownloadableFile getFile(Message message, boolean decrypted) { + public static DownloadableFile getFile(Message message, boolean decrypted) { String path = message.getRelativeFilePath(); String extension; if (path != null && !path.isEmpty()) { @@ -102,50 +81,19 @@ public class FileBackend { + "/Conversations/"; } - public Bitmap resize(Bitmap originalBitmap, int size) { - int w = originalBitmap.getWidth(); - int h = originalBitmap.getHeight(); - if (Math.max(w, h) > size) { - int scalledW; - int scalledH; - if (w <= h) { - scalledW = (int) (w / ((double) h / size)); - scalledH = size; - } else { - scalledW = size; - scalledH = (int) (h / ((double) w / size)); - } - return Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true); - } else { - return originalBitmap; - } - } - - public Bitmap rotate(Bitmap bitmap, int degree) { - int w = bitmap.getWidth(); - int h = bitmap.getHeight(); - Matrix mtx = new Matrix(); - mtx.postRotate(degree); - return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); - } - - public String getOriginalPath(Uri uri) { - return FileHelper.getRealPathFromUri(uri); - } - - public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { + public static DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage"); - String mime = mXmppConnectionService.getContentResolver().getType(uri); + String mime = ConversationsPlusApplication.getInstance().getContentResolver().getType(uri); String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); message.setRelativeFilePath(message.getUuid() + "." + extension); - DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); + DownloadableFile file = getFile(message); file.getParentFile().mkdirs(); OutputStream os = null; InputStream is = null; try { file.createNewFile(); os = new FileOutputStream(file); - is = mXmppConnectionService.getContentResolver().openInputStream(uri); + is = StreamUtil.openInputStreamFromContentResolver(uri); byte[] buffer = new byte[1024]; int length; while ((length = is.read(buffer)) > 0) { @@ -158,19 +106,19 @@ public class FileBackend { e.printStackTrace(); throw new FileCopyException(R.string.error_io_exception); } finally { - close(os); - close(is); + StreamUtil.close(os); + StreamUtil.close(is); } - Log.d(Config.LOGTAG, "output file name " + mXmppConnectionService.getFileBackend().getFile(message)); + Log.d(Config.LOGTAG, "output file name " + file); return file; } - public DownloadableFile copyImageToPrivateStorage(Message message, Uri image) + public static DownloadableFile copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException { - return this.copyImageToPrivateStorage(message, image, 0); + return copyImageToPrivateStorage(message, image, 0); } - private DownloadableFile copyImageToPrivateStorage(Message message, + private static DownloadableFile copyImageToPrivateStorage(Message message, Uri image, int sampleSize) throws FileCopyException { DownloadableFile file = getFile(message); file.getParentFile().mkdirs(); @@ -178,7 +126,7 @@ public class FileBackend { OutputStream os = null; try { file.createNewFile(); - is = mXmppConnectionService.getContentResolver().openInputStream(image); + is = StreamUtil.openInputStreamFromContentResolver(image); os = new FileOutputStream(file); Bitmap originalBitmap; @@ -191,10 +139,10 @@ public class FileBackend { if (originalBitmap == null) { throw new FileCopyException(R.string.error_not_an_image_file); } - Bitmap scaledBitmap = resize(originalBitmap, IMAGE_SIZE); - int rotation = getRotation(image); + Bitmap scaledBitmap = ImageUtil.resize(originalBitmap, IMAGE_SIZE); + int rotation = ImageUtil.getRotation(image); if (rotation > 0) { - scaledBitmap = rotate(scaledBitmap, rotation); + scaledBitmap = ImageUtil.rotate(scaledBitmap, rotation); } boolean success = scaledBitmap.compress(Bitmap.CompressFormat.WEBP, 75, os); @@ -224,286 +172,34 @@ public class FileBackend { } catch (NullPointerException e) { throw new FileCopyException(R.string.error_io_exception); } finally { - close(os); - close(is); - } - } - - private int getRotation(Uri image) { - InputStream is = null; - try { - is = mXmppConnectionService.getContentResolver().openInputStream(image); - return ExifHelper.getOrientation(is); - } catch (FileNotFoundException e) { - return 0; - } finally { - close(is); - } - } - - public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) - throws FileNotFoundException { - Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get( - message.getUuid()); - if ((thumbnail == null) && (!cacheOnly)) { - File file = getFile(message); - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = calcSampleSize(file, size); - Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),options); - if (fullsize == null) { - throw new FileNotFoundException(); - } - thumbnail = resize(fullsize, size); - try { - thumbnail = rotate(thumbnail, file.getAbsolutePath()); - } catch (IOException e) { - throw new FileNotFoundException(); - } - this.mXmppConnectionService.getBitmapCache().put(message.getUuid(), - thumbnail); + StreamUtil.close(os); + StreamUtil.close(is); } - return thumbnail; } - private Bitmap rotate(Bitmap original, String srcPath) throws IOException { - try { - ExifInterface exif = new ExifInterface(srcPath); - int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); - int rotation = 0; - switch (orientation) { - case ExifInterface.ORIENTATION_ROTATE_90: - rotation = 90; - break; - case ExifInterface.ORIENTATION_ROTATE_180: - rotation = 180; - break; - case ExifInterface.ORIENTATION_ROTATE_270: - rotation = 270; - break; - } - if (rotation > 0) { - return rotate(original, rotation); - } - } catch (IOException e) { - Log.w("filebackend", "Error while rotating image, returning original"); - } - return original; - } - - public Uri getTakePhotoUri() { + public static Uri getTakePhotoUri() { StringBuilder pathBuilder = new StringBuilder(); pathBuilder.append(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)); pathBuilder.append('/'); pathBuilder.append("Camera"); pathBuilder.append('/'); - pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date()) + ".jpg"); + pathBuilder.append("IMG_" + imageDateFormat.format(new Date()) + ".jpg"); Uri uri = Uri.parse("file://" + pathBuilder.toString()); File file = new File(uri.toString()); file.getParentFile().mkdirs(); return uri; } - public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { - try { - Avatar avatar = new Avatar(); - Bitmap bm = cropCenterSquare(image, size); - if (bm == null) { - return null; - } - ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); - Base64OutputStream mBase64OutputSttream = new Base64OutputStream( - mByteArrayOutputStream, Base64.DEFAULT); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - DigestOutputStream mDigestOutputStream = new DigestOutputStream( - mBase64OutputSttream, digest); - if (!bm.compress(format, 75, mDigestOutputStream)) { - return null; - } - mDigestOutputStream.flush(); - mDigestOutputStream.close(); - avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest()); - avatar.image = new String(mByteArrayOutputStream.toByteArray()); - return avatar; - } catch (NoSuchAlgorithmException e) { - return null; - } catch (IOException e) { - return null; - } - } - - public boolean isAvatarCached(Avatar avatar) { - File file = new File(getAvatarPath(avatar.getFilename())); - return file.exists(); - } - - public boolean save(Avatar avatar) { - File file; - if (isAvatarCached(avatar)) { - file = new File(getAvatarPath(avatar.getFilename())); - } else { - String filename = getAvatarPath(avatar.getFilename()); - file = new File(filename + ".tmp"); - file.getParentFile().mkdirs(); - OutputStream os = null; - try { - file.createNewFile(); - os = new FileOutputStream(file); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - digest.reset(); - DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest); - mDigestOutputStream.write(avatar.getImageAsBytes()); - mDigestOutputStream.flush(); - mDigestOutputStream.close(); - String sha1sum = CryptoHelper.bytesToHex(digest.digest()); - if (sha1sum.equals(avatar.sha1sum)) { - file.renameTo(new File(filename)); - } else { - Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner); - file.delete(); - return false; - } - } catch (FileNotFoundException e) { - return false; - } catch (IOException e) { - return false; - } catch (NoSuchAlgorithmException e) { - return false; - } finally { - close(os); - } - } - avatar.size = file.length(); - return true; - } - - public String getAvatarPath(String avatar) { - return mXmppConnectionService.getFilesDir().getAbsolutePath()+ "/avatars/" + avatar; - } - - public Uri getAvatarUri(String avatar) { - return Uri.parse("file:" + getAvatarPath(avatar)); - } - - public Bitmap cropCenterSquare(Uri image, int size) { - if (image == null) { - return null; - } - InputStream is = null; - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = calcSampleSize(image, size); - is = mXmppConnectionService.getContentResolver().openInputStream(image); - Bitmap input = BitmapFactory.decodeStream(is, null, options); - if (input == null) { - return null; - } else { - int rotation = getRotation(image); - if (rotation > 0) { - input = rotate(input, rotation); - } - return cropCenterSquare(input, size); - } - } catch (FileNotFoundException e) { - return null; - } finally { - close(is); - } - } - - public Bitmap cropCenter(Uri image, int newHeight, int newWidth) { - if (image == null) { - return null; - } - InputStream is = null; - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = calcSampleSize(image,Math.max(newHeight, newWidth)); - is = mXmppConnectionService.getContentResolver().openInputStream(image); - Bitmap source = BitmapFactory.decodeStream(is, null, options); - if (source == null) { - return null; - } - int sourceWidth = source.getWidth(); - int sourceHeight = source.getHeight(); - float xScale = (float) newWidth / sourceWidth; - float yScale = (float) newHeight / sourceHeight; - float scale = Math.max(xScale, yScale); - float scaledWidth = scale * sourceWidth; - float scaledHeight = scale * sourceHeight; - float left = (newWidth - scaledWidth) / 2; - float top = (newHeight - scaledHeight) / 2; - - RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight); - Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(dest); - canvas.drawBitmap(source, null, targetRect, null); - return dest; - } catch (FileNotFoundException e) { - return null; - } finally { - close(is); - } - } - - public Bitmap cropCenterSquare(Bitmap input, int size) { - int w = input.getWidth(); - int h = input.getHeight(); - - float scale = Math.max((float) size / h, (float) size / w); - - float outWidth = scale * w; - float outHeight = scale * h; - float left = (size - outWidth) / 2; - float top = (size - outHeight) / 2; - RectF target = new RectF(left, top, left + outWidth, top + outHeight); - - Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(output); - canvas.drawBitmap(input, null, target, null); - return output; - } - - private int calcSampleSize(Uri image, int size) throws FileNotFoundException { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options); - return calcSampleSize(options, size); - } - - private int calcSampleSize(File image, int size) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(image.getAbsolutePath(), options); - return calcSampleSize(options, size); - } - - private int calcSampleSize(BitmapFactory.Options options, int size) { - int height = options.outHeight; - int width = options.outWidth; - int inSampleSize = 1; - - if (height > size || width > size) { - int halfHeight = height / 2; - int halfWidth = width / 2; - - while ((halfHeight / inSampleSize) > size - && (halfWidth / inSampleSize) > size) { - inSampleSize *= 2; - } - } - return inSampleSize; - } - - public Uri getJingleFileUri(Message message) { + public static Uri getJingleFileUri(Message message) { File file = getFile(message); return Uri.parse("file://" + file.getAbsolutePath()); } - public void updateFileParams(Message message) { + public static void updateFileParams(Message message) { updateFileParams(message,null); } - public void updateFileParams(Message message, URL url) { + 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(); @@ -526,40 +222,11 @@ public class FileBackend { } - public class FileCopyException extends Exception { - private static final long serialVersionUID = -1010013599132881427L; - private int resId; - - public FileCopyException(int resId) { - this.resId = resId; - } - - public int getResId() { - return resId; - } - } - - public Bitmap getAvatar(String avatar, int size) { - if (avatar == null) { - return null; - } - Bitmap bm = cropCenter(getAvatarUri(avatar), size, size); - if (bm == null) { - return null; - } - return bm; - } - - public boolean isFileAvailable(Message message) { + public static boolean isFileAvailable(Message message) { return getFile(message).exists(); } - public static void close(Closeable stream) { - if (stream != null) { - try { - stream.close(); - } catch (IOException e) { - } - } - } -} + private FileBackend() { + // Static helper class + } +} \ No newline at end of file diff --git a/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java b/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java index 7321fc08..83aa83e3 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/AvatarService.java @@ -17,6 +17,8 @@ import de.thedevstack.conversationsplus.entities.Contact; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.ListItem; import de.thedevstack.conversationsplus.entities.MucOptions; +import de.thedevstack.conversationsplus.utils.AvatarUtil; +import de.thedevstack.conversationsplus.utils.ImageUtil; import de.thedevstack.conversationsplus.utils.UIHelper; public class AvatarService { @@ -40,28 +42,27 @@ public class AvatarService { private Bitmap get(final Contact contact, final int size, boolean cachedOnly) { final String KEY = key(contact, size); - Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); + Bitmap avatar = ImageUtil.getBitmapFromCache(KEY); if (avatar != null || cachedOnly) { return avatar; } if (contact.getProfilePhoto() != null) { - avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size); + avatar = ImageUtil.cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size); } if (avatar == null && contact.getAvatar() != null) { - avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size); + avatar = AvatarUtil.getAvatar(contact.getAvatar(), size); } if (avatar == null) { avatar = get(contact.getDisplayName(), size, cachedOnly); } - this.mXmppConnectionService.getBitmapCache().put(KEY, avatar); + ImageUtil.addBitmapToCache(KEY, avatar); return avatar; } public void clear(Contact contact) { synchronized (this.sizes) { for (Integer size : sizes) { - this.mXmppConnectionService.getBitmapCache().remove( - key(contact, size)); + ImageUtil.removeBitmapFromCache(key(contact, size)); } } } @@ -117,7 +118,7 @@ public class AvatarService { private Bitmap get(MucOptions mucOptions, int size, boolean cachedOnly) { final String KEY = key(mucOptions, size); - Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY); + Bitmap bitmap = ImageUtil.getBitmapFromCache(KEY); if (bitmap != null || cachedOnly) { return bitmap; } @@ -155,15 +156,14 @@ public class AvatarService { drawTile(canvas, "\u2026", PLACEHOLDER_COLOR, size / 2 + 1, size / 2 + 1, size, size); } - this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap); + ImageUtil.addBitmapToCache(KEY, bitmap); return bitmap; } public void clear(MucOptions options) { synchronized (this.sizes) { for (Integer size : sizes) { - this.mXmppConnectionService.getBitmapCache().remove( - key(options, size)); + ImageUtil.removeBitmapFromCache(key(options, size)); } } } @@ -180,24 +180,22 @@ public class AvatarService { public Bitmap get(Account account, int size) { final String KEY = key(account, size); - Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY); + Bitmap avatar = ImageUtil.getBitmapFromCache(KEY); if (avatar != null) { return avatar; } - avatar = mXmppConnectionService.getFileBackend().getAvatar( - account.getAvatar(), size); + avatar = AvatarUtil.getAvatar(account.getAvatar(), size); if (avatar == null) { avatar = get(account.getJid().toBareJid().toString(), size,false); } - mXmppConnectionService.getBitmapCache().put(KEY, avatar); + ImageUtil.addBitmapToCache(KEY, avatar); return avatar; } public void clear(Account account) { synchronized (this.sizes) { for (Integer size : sizes) { - this.mXmppConnectionService.getBitmapCache().remove( - key(account, size)); + ImageUtil.removeBitmapFromCache(key(account, size)); } } } @@ -218,7 +216,7 @@ public class AvatarService { public Bitmap get(final String name, final int size, boolean cachedOnly) { final String KEY = key(name, size); - Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY); + Bitmap bitmap = ImageUtil.getBitmapFromCache(KEY); if (bitmap != null || cachedOnly) { return bitmap; } @@ -228,7 +226,7 @@ public class AvatarService { final String letter = trimmedName.isEmpty() ? "X" : trimmedName.substring(0,1); final int color = UIHelper.getColorForName(name); drawTile(canvas, letter, color, 0, 0, size, size); - mXmppConnectionService.getBitmapCache().put(KEY, bitmap); + ImageUtil.addBitmapToCache(KEY, bitmap); return bitmap; } @@ -268,12 +266,10 @@ public class AvatarService { if (contact.getProfilePhoto() != null) { uri = Uri.parse(contact.getProfilePhoto()); } else if (contact.getAvatar() != null) { - uri = mXmppConnectionService.getFileBackend().getAvatarUri( - contact.getAvatar()); + uri = AvatarUtil.getAvatarUri(contact.getAvatar()); } if (uri != null) { - Bitmap bitmap = mXmppConnectionService.getFileBackend() - .cropCenter(uri, bottom - top, right - left); + Bitmap bitmap = ImageUtil.cropCenter(uri, bottom - top, right - left); if (bitmap != null) { drawTile(canvas, bitmap, left, top, right, bottom); return; diff --git a/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java b/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java index 1536e8b1..dd448360 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java @@ -32,6 +32,7 @@ import java.util.regex.Pattern; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.utils.ImageUtil; import de.tzur.conversations.Settings; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; @@ -286,8 +287,7 @@ public class NotificationService { private void modifyForImage(final Builder builder, final Message message, final ArrayList messages, final boolean notify) { try { - final Bitmap bitmap = mXmppConnectionService.getFileBackend() - .getThumbnail(message, getPixel(288), false); + final Bitmap bitmap = ImageUtil.getThumbnail(message, getPixel(288), false); final ArrayList tmp = new ArrayList<>(); for (final Message msg : messages) { if (msg.getType() == Message.TYPE_TEXT diff --git a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java index 62ed2781..624914dd 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java @@ -1,14 +1,12 @@ package de.thedevstack.conversationsplus.services; import android.annotation.SuppressLint; -import android.app.Activity; import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; -import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.ConnectivityManager; @@ -23,9 +21,7 @@ import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; import android.provider.ContactsContract; -import android.provider.MediaStore; import android.util.Log; -import android.util.LruCache; import net.java.otr4j.OtrException; import net.java.otr4j.session.Session; @@ -36,7 +32,6 @@ import net.java.otr4j.session.SessionStatus; import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpServiceConnection; -import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -57,10 +52,10 @@ import java.util.concurrent.CopyOnWriteArrayList; import de.duenndns.ssl.MemorizingTrustManager; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; -import de.thedevstack.conversationsplus.enums.UserDecision; -import de.thedevstack.conversationsplus.ui.dialogs.UserDecisionDialog; -import de.thedevstack.conversationsplus.ui.listeners.ResizePictureUserDecisionListener; +import de.thedevstack.conversationsplus.exceptions.FileCopyException; +import de.thedevstack.conversationsplus.utils.AvatarUtil; import de.thedevstack.conversationsplus.utils.FileHelper; +import de.thedevstack.conversationsplus.utils.ImageUtil; import de.tzur.conversations.Settings; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; @@ -215,7 +210,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } }; - private FileBackend fileBackend = new FileBackend(this); private MemorizingTrustManager mMemorizingTrustManager; private NotificationService mNotificationService = new NotificationService( this); @@ -322,7 +316,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private PgpEngine mPgpEngine = null; private WakeLock wakeLock; private PowerManager pm; - private LruCache mBitmapCache; private Thread mPhoneContactMergerThread; private boolean mRestoredFromDatabase = false; @@ -344,10 +337,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } - public FileBackend getFileBackend() { - return this.fileBackend; - } - public AvatarService getAvatarService() { return this.mAvatarService; } @@ -384,10 +373,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_FILE); - String path = getFileBackend().getOriginalPath(uri); + String path = FileHelper.getRealPathFromUri(uri); if (path!=null) { message.setRelativeFilePath(path); - getFileBackend().updateFileParams(message); + FileBackend.updateFileParams(message); if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { getPgpEngine().encrypt(message, callback); } else { @@ -395,21 +384,21 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } else { mFileAddingExecutor.execute(new Runnable() { - @Override - public void run() { - try { - getFileBackend().copyFileToPrivateStorage(message, uri); - getFileBackend().updateFileParams(message); - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - getPgpEngine().encrypt(message, callback); - } else { - callback.success(message); - } - } catch (FileBackend.FileCopyException e) { - callback.error(e.getResId(), message); - } - } - }); + @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); + } + } + }); } } @@ -477,13 +466,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @Override public void run() { try { - getFileBackend().copyImageToPrivateStorage(message, uri); + FileBackend.copyImageToPrivateStorage(message, uri); if (conversation.getNextEncryption(forceEncryption) == Message.ENCRYPTION_PGP) { getPgpEngine().encrypt(message, callback); } else { callback.success(message); } - } catch (final FileBackend.FileCopyException e) { + } catch (final FileCopyException e) { callback.error(e.getResId(), message); } } @@ -659,14 +648,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa PRNGFixes.apply(); this.mRandom = new SecureRandom(); updateMemorizingTrustmanager(); - final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); - final int cacheSize = maxMemory / 8; - this.mBitmapCache = new LruCache(cacheSize) { - @Override - protected int sizeOf(final String key, final Bitmap bitmap) { - return bitmap.getByteCount() / 1024; - } - }; this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); this.accounts = databaseBackend.getAccounts(); @@ -908,7 +889,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } else { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster"); } - iqPacket.query(Xmlns.ROSTER).setAttribute("ver",account.getRosterVersion()); + iqPacket.query(Xmlns.ROSTER).setAttribute("ver", account.getRosterVersion()); sendIqPacket(account, iqPacket, mIqParser); } @@ -1022,7 +1003,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa for(Account account : accounts) { databaseBackend.readRoster(account.getRoster()); } - getBitmapCache().evictAll(); + ImageUtil.evictBitmapCache(); Looper.prepare(); PhoneHelper.loadPhoneContacts(getApplicationContext(), new CopyOnWriteArrayList(), @@ -1050,7 +1031,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @Override public void onMessageFound(Message message) { - if (!getFileBackend().isFileAvailable(message)) { + if (!FileBackend.isFileAvailable(message)) { message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); } } @@ -1061,7 +1042,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa for (Conversation conversation : getConversations()) { Message message = conversation.findMessageWithFileAndUuid(uuid); if (message != null) { - if (!getFileBackend().isFileAvailable(message)) { + if (!FileBackend.isFileAvailable(message)) { message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); updateConversationUi(); } @@ -1087,19 +1068,19 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } Collections.sort(list, new Comparator() { - @Override - public int compare(Conversation lhs, Conversation rhs) { - Message left = lhs.getLatestMessage(); - Message right = rhs.getLatestMessage(); - if (left.getTimeSent() > right.getTimeSent()) { - return -1; - } else if (left.getTimeSent() < right.getTimeSent()) { - return 1; - } else { - return 0; - } - } - }); + @Override + public int compare(Conversation lhs, Conversation rhs) { + Message left = lhs.getLatestMessage(); + Message right = rhs.getLatestMessage(); + if (left.getTimeSent() > right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() < right.getTimeSent()) { + return 1; + } else { + return 0; + } + } + }); } public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) { @@ -1271,17 +1252,17 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) { final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword); sendIqPacket(account, iq, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.setPassword(newPassword); - databaseBackend.updateAccount(account); - callback.onPasswordChangeSucceeded(); - } else { - callback.onPasswordChangeFailed(); - } - } - }); + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.setPassword(newPassword); + databaseBackend.updateAccount(account); + callback.onPasswordChangeSucceeded(); + } else { + callback.onPasswordChangeFailed(); + } + } + }); } public void deleteAccount(final Account account) { @@ -1616,7 +1597,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa conversation.getMucOptions().setOffline(); conversation.deregisterWithBookmark(); Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() - + ": leaving muc " + conversation.getJid()); + + ": leaving muc " + conversation.getJid()); } else { account.pendingConferenceLeaves.add(conversation); } @@ -1700,23 +1681,23 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa request.setTo(conversation.getJid().toBareJid()); request.query("http://jabber.org/protocol/disco#info"); sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() != IqPacket.TYPE.ERROR) { - ArrayList features = new ArrayList<>(); - for (Element child : packet.query().getChildren()) { - if (child != null && child.getName().equals("feature")) { - String var = child.getAttribute("var"); - if (var != null) { - features.add(var); - } - } - } - conversation.getMucOptions().updateFeatures(features); - updateConversationUi(); - } - } - }); + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() != IqPacket.TYPE.ERROR) { + ArrayList features = new ArrayList<>(); + for (Element child : packet.query().getChildren()) { + if (child != null && child.getName().equals("feature")) { + String var = child.getAttribute("var"); + if (var != null) { + features.add(var); + } + } + } + conversation.getMucOptions().updateFeatures(features); + updateConversationUi(); + } + } + }); } public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) { @@ -1876,27 +1857,27 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa + otrSession.getSessionID().getUserID()); conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { - @Override - public void onMessageFound(Message message) { - SessionID id = otrSession.getSessionID(); - try { - message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID())); - } catch (InvalidJidException e) { - return; - } - if (message.needsUploading()) { - mJingleConnectionManager.createNewConnection(message); - } else { - MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true); - if (outPacket != null) { - message.setStatus(Message.STATUS_SEND); - databaseBackend.updateMessage(message); - sendMessagePacket(account, outPacket); - } - } - updateConversationUi(); - } - }); + @Override + public void onMessageFound(Message message) { + SessionID id = otrSession.getSessionID(); + try { + message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID())); + } catch (InvalidJidException e) { + return; + } + if (message.needsUploading()) { + mJingleConnectionManager.createNewConnection(message); + } else { + MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true); + if (outPacket != null) { + message.setStatus(Message.STATUS_SEND); + databaseBackend.updateMessage(message); + sendMessagePacket(account, outPacket); + } + } + updateConversationUi(); + } + }); } public boolean renewSymmetricKey(Conversation conversation) { @@ -1954,8 +1935,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa final UiCallback callback) { final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; final int size = Config.AVATAR_SIZE; - final Avatar avatar = getFileBackend() - .getPepAvatar(image, size, format); + final Avatar avatar = AvatarUtil.getPepAvatar(image, size, format); if (avatar != null) { avatar.height = size; avatar.width = size; @@ -1966,7 +1946,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } else if (format.equals(Bitmap.CompressFormat.PNG)) { avatar.type = "image/png"; } - if (!getFileBackend().save(avatar)) { + if (!AvatarUtil.save(avatar)) { callback.error(R.string.error_saving_avatar, avatar); return; } @@ -2050,7 +2030,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (result.getType() == IqPacket.TYPE.RESULT) { avatar.image = mIqParser.avatarData(result); if (avatar.image != null) { - if (getFileBackend().save(avatar)) { + if (AvatarUtil.save(avatar)) { if (account.getJid().toBareJid().equals(avatar.owner)) { if (account.setAvatar(avatar.getFilename())) { databaseBackend.updateAccount(account); @@ -2096,31 +2076,31 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback callback) { IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar); this.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - synchronized (mInProgressAvatarFetches) { - mInProgressAvatarFetches.remove(generateFetchKey(account, avatar)); - } - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element vCard = packet.findChild("vCard", "vcard-temp"); - Element photo = vCard != null ? vCard.findChild("PHOTO") : null; - String image = photo != null ? photo.findChildContent("BINVAL") : null; - if (image != null) { - avatar.image = image; - if (getFileBackend().save(avatar)) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + ": successfully fetched vCard avatar for " + avatar.owner); - Contact contact = account.getRoster() - .getContact(avatar.owner); - contact.setAvatar(avatar); - getAvatarService().clear(contact); - updateConversationUi(); - updateRosterUi(); - } - } - } - } - }); + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + synchronized (mInProgressAvatarFetches) { + mInProgressAvatarFetches.remove(generateFetchKey(account, avatar)); + } + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element vCard = packet.findChild("vCard", "vcard-temp"); + Element photo = vCard != null ? vCard.findChild("PHOTO") : null; + String image = photo != null ? photo.findChildContent("BINVAL") : null; + if (image != null) { + avatar.image = image; + if (AvatarUtil.save(avatar)) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": successfully fetched vCard avatar for " + avatar.owner); + Contact contact = account.getRoster() + .getContact(avatar.owner); + contact.setAvatar(avatar); + getAvatarService().clear(contact); + updateConversationUi(); + updateRosterUi(); + } + } + } + } + }); } public void checkForAvatar(Account account, final UiCallback callback) { @@ -2138,7 +2118,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Avatar avatar = Avatar.parseMetadata(items); if (avatar != null) { avatar.owner = account.getJid().toBareJid(); - if (fileBackend.isAvatarCached(avatar)) { + if (AvatarUtil.isAvatarCached(avatar)) { if (account.setAvatar(avatar.getFilename())) { databaseBackend.updateAccount(account); } @@ -2401,10 +2381,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return this.pm; } - public LruCache getBitmapCache() { - return this.mBitmapCache; - } - public void syncRosterToDisk(final Account account) { Runnable runnable = new Runnable() { diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java index 3ae2a07a..94d1e2ea 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java @@ -30,6 +30,7 @@ import android.widget.Toast; import net.java.otr4j.session.SessionStatus; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.ui.dialogs.UserDecisionDialog; import de.thedevstack.conversationsplus.ui.listeners.ResizePictureUserDecisionListener; import de.timroes.android.listview.EnhancedListView; @@ -424,7 +425,7 @@ public class ConversationActivity extends XmppActivity chooser = true; break; case ATTACHMENT_CHOICE_TAKE_PHOTO: - Uri uri = xmppConnectionService.getFileBackend().getTakePhotoUri(); + Uri uri = FileBackend.getTakePhotoUri(); intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); mPendingImageUris.clear(); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java index 3fbe59f4..94cb896b 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java @@ -42,6 +42,7 @@ import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentLinkedQueue; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.ui.dialogs.MessageDetailsDialog; import de.thedevstack.conversationsplus.ui.listeners.ConversationSwipeRefreshListener; import de.thedevstack.conversationsplus.Config; @@ -535,9 +536,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody()); shareIntent.setType("text/plain"); } else { - shareIntent.putExtra(Intent.EXTRA_STREAM, - activity.xmppConnectionService.getFileBackend() - .getJingleFileUri(message)); + shareIntent.putExtra(Intent.EXTRA_STREAM, FileBackend.getJingleFileUri(message)); shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); String mime = message.getMimeType(); if (mime == null) { @@ -563,7 +562,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa private void resendMessage(Message message) { if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) { - DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + DownloadableFile file = FileBackend.getFile(message); if (!file.exists()) { Toast.makeText(activity, R.string.file_deleted, Toast.LENGTH_SHORT).show(); message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/PublishProfilePictureActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/PublishProfilePictureActivity.java index be08da14..966cc379 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/PublishProfilePictureActivity.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/PublishProfilePictureActivity.java @@ -15,6 +15,7 @@ import android.widget.Toast; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.utils.ImageUtil; import de.thedevstack.conversationsplus.utils.PhoneHelper; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -211,8 +212,7 @@ public class PublishProfilePictureActivity extends XmppActivity { } protected void loadImageIntoPreview(Uri uri) { - Bitmap bm = xmppConnectionService.getFileBackend().cropCenterSquare( - uri, 384); + Bitmap bm = ImageUtil.cropCenterSquare(uri, 384); if (bm == null) { disablePublishButton(); this.hintOrWarning.setTextColor(getWarningTextColor()); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/XmppActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/XmppActivity.java index ecf1ce77..97582fc7 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/XmppActivity.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/XmppActivity.java @@ -56,7 +56,6 @@ import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import net.java.otr4j.session.SessionID; import java.io.FileNotFoundException; -import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Hashtable; @@ -76,6 +75,7 @@ import de.thedevstack.conversationsplus.services.AvatarService; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.services.XmppConnectionService.XmppConnectionBinder; import de.thedevstack.conversationsplus.utils.ExceptionHelper; +import de.thedevstack.conversationsplus.utils.ImageUtil; import de.thedevstack.conversationsplus.xmpp.OnUpdateBlocklist; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -881,8 +881,7 @@ public abstract class XmppActivity extends Activity { protected Bitmap doInBackground(Message... params) { message = params[0]; try { - return xmppConnectionService.getFileBackend().getThumbnail( - message, (int) (metrics.density * 288), false); + return ImageUtil.getThumbnail(message, (int) (metrics.density * 288), false); } catch (FileNotFoundException e) { return null; } @@ -907,8 +906,7 @@ public abstract class XmppActivity extends Activity { public void loadBitmap(Message message, ImageView imageView, boolean setSize) { Bitmap bm; try { - bm = xmppConnectionService.getFileBackend().getThumbnail(message, - (int) (metrics.density * 288), true); + bm = ImageUtil.getThumbnail(message,(int) (metrics.density * 288), true); } catch (FileNotFoundException e) { bm = null; } diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java index bbe0d362..9ab7abf0 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java @@ -11,7 +11,6 @@ import android.text.Spanned; import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; -import android.util.DisplayMetrics; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; @@ -34,6 +33,7 @@ import de.thedevstack.conversationsplus.entities.Transferable; import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.Message.FileParams; +import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.ui.ConversationActivity; import de.thedevstack.conversationsplus.utils.GeoHelper; import de.thedevstack.conversationsplus.utils.UIHelper; @@ -47,8 +47,6 @@ public class MessageAdapter extends ArrayAdapter { private ConversationActivity activity; - private DisplayMetrics metrics; - private OnContactPictureClicked mOnContactPictureClickedListener; private OnContactPictureLongClicked mOnContactPictureLongClickedListener; @@ -64,7 +62,6 @@ public class MessageAdapter extends ArrayAdapter { public MessageAdapter(ConversationActivity activity, List messages) { super(activity, 0, messages); this.activity = activity; - metrics = getContext().getResources().getDisplayMetrics(); } public void setOnContactPictureClicked(OnContactPictureClicked listener) { @@ -363,8 +360,7 @@ public class MessageAdapter extends ArrayAdapter { @Override public void onClick(View v) { Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(activity.xmppConnectionService - .getFileBackend().getJingleFileUri(message), "image/*"); + intent.setDataAndType(FileBackend.getJingleFileUri(message), "image/*"); getContext().startActivity(intent); } }); @@ -551,7 +547,7 @@ public class MessageAdapter extends ArrayAdapter { } public void openDownloadable(Message message) { - DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + DownloadableFile file = FileBackend.getFile(message); if (!file.exists()) { Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show(); return; diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/AvatarUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/AvatarUtil.java new file mode 100644 index 00000000..287f4b50 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/AvatarUtil.java @@ -0,0 +1,162 @@ +package de.thedevstack.conversationsplus.utils; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.util.Base64; +import android.util.Base64OutputStream; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.xmpp.pep.Avatar; + +/** + * This util provides access to saved avatars, creating avatars. + */ +public final class AvatarUtil { + + /** + * Get the PEP Avatar. + * TODO: Why PEP Avatar? + * @param image the uri to the avatar's image + * @param size the image width/height to resize to + * @param format the format for the avatar + * @return the avatar + */ + public static Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { + try { + Avatar avatar = new Avatar(); + Bitmap bm = ImageUtil.cropCenterSquare(image, size); + if (bm == null) { + return null; + } + ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); + Base64OutputStream mBase64OutputSttream = new Base64OutputStream( + mByteArrayOutputStream, Base64.DEFAULT); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + DigestOutputStream mDigestOutputStream = new DigestOutputStream( + mBase64OutputSttream, digest); + if (!bm.compress(format, 75, mDigestOutputStream)) { + return null; + } + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest()); + avatar.image = new String(mByteArrayOutputStream.toByteArray()); + return avatar; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (IOException e) { + return null; + } + } + + /** + * Returns whether the avatar is cached or not. + * @param avatar the avatar to check the existance + * @return true if the file of the avatar exists, false otherwise + */ + public static boolean isAvatarCached(Avatar avatar) { + File file = new File(getAvatarPath(avatar.getFilename())); + return file.exists(); + } + + /** + * Saves an avatar to the file system. + * All exceptions are silently ignored. + * TODO: Move real saving operation to FileBackend + * @param avatar the avatar to save + * @return true if the avatar was saved successfully, false otherwise. + */ + public static boolean save(Avatar avatar) { + File file; + if (isAvatarCached(avatar)) { + file = new File(getAvatarPath(avatar.getFilename())); + } else { + String filename = getAvatarPath(avatar.getFilename()); + file = new File(filename + ".tmp"); + file.getParentFile().mkdirs(); + OutputStream os = null; + try { + file.createNewFile(); + os = new FileOutputStream(file); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest); + mDigestOutputStream.write(avatar.getImageAsBytes()); + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + String sha1sum = CryptoHelper.bytesToHex(digest.digest()); + if (sha1sum.equals(avatar.sha1sum)) { + file.renameTo(new File(filename)); + } else { + Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner); + file.delete(); + return false; + } + } catch (FileNotFoundException e) { + return false; + } catch (IOException e) { + return false; + } catch (NoSuchAlgorithmException e) { + return false; + } finally { + StreamUtil.close(os); + } + } + avatar.size = file.length(); + return true; + } + + /** + * Returns the avatar for an uri. + * @param avatar the avatar's uri + * @param size the height/width the avatar should have + * @return the bitmap of the uri + */ + public static Bitmap getAvatar(String avatar, int size) { + if (avatar == null) { + return null; + } + Bitmap bm = ImageUtil.cropCenter(getAvatarUri(avatar), size, size); + if (bm == null) { + return null; + } + return bm; + } + + /** + * Returns the path to an avatar + * @param avatar the name of the avatar. + * @return the path as string + */ + public static String getAvatarPath(String avatar) { + return ConversationsPlusApplication.getInstance().getFilesDir().getAbsolutePath()+ "/avatars/" + avatar; + } + + /** + * Returns the path to an avatar as an uri. + * @param avatar the name of the avatar + * @return the path as uri + */ + public static Uri getAvatarUri(String avatar) { + return Uri.parse("file:" + getAvatarPath(avatar)); + } + + /** + * Avoid instantiation it's an helper class. + */ + private AvatarUtil() { + // Static helper class + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java new file mode 100644 index 00000000..3c3e1c99 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java @@ -0,0 +1,313 @@ +package de.thedevstack.conversationsplus.utils; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.media.ExifInterface; +import android.net.Uri; +import android.util.Log; +import android.util.LruCache; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.persistance.FileBackend; + +/** + * This util provides + */ +public final class ImageUtil { + private static LruCache BITMAP_CACHE; + + /** + * Returns a bitmap from the cache. + * @see LruCache#get(Object) for details + * @param key the key of the bitmap to get + * @return the bitmap + */ + public static Bitmap getBitmapFromCache(String key) { + return BITMAP_CACHE.get(key); + } + + /** + * Adds a bitmap with the given key to the cache. + * @see LruCache#put(Object, Object) for details + * @param key the key to identify this bitmap + * @param bitmap the bitmap to cache + */ + public static void addBitmapToCache(String key, Bitmap bitmap) { + BITMAP_CACHE.put(key, bitmap); + } + + /** + * Removes the bitmap with given key from the cache. + * @param key the key of the bitmap to remove + */ + public static void removeBitmapFromCache(String key) { + BITMAP_CACHE.remove(key); + } + + /** + * Clears the cache. + * @see LruCache#evictAll() for more details. + */ + public static void evictBitmapCache() { + BITMAP_CACHE.evictAll(); + } + + /** + * Initializes the bitmap cache. + * This has to be executed once on application start. + * @see LruCache#LruCache(int) for details + */ + public static void initBitmapCache() { + Log.i("Conversations+ImageUtil", "Initializing BitmapCache"); + final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + final int cacheSize = maxMemory / 8; + BITMAP_CACHE = new LruCache(cacheSize) { + @Override + protected int sizeOf(final String key, final Bitmap bitmap) { + return bitmap.getByteCount() / 1024; + } + }; + } + + /** + * Resizes a given bitmap and return a new and resized bitmap. + * The bitmap is only resized if either the width or the height of the original bitmap is smaller than the given size. + * @param originalBitmap the bitmap to resize + * @param size the size to scale to + * @return new and resized bitmap or the original bitmap if width and height are smaller than size + */ + public static Bitmap resize(Bitmap originalBitmap, int size) { + int w = originalBitmap.getWidth(); + int h = originalBitmap.getHeight(); + if (Math.max(w, h) > size) { + int scalledW; + int scalledH; + if (w <= h) { + scalledW = (int) (w / ((double) h / size)); + scalledH = size; + } else { + scalledW = size; + scalledH = (int) (h / ((double) w / size)); + } + return Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true); + } else { + return originalBitmap; + } + } + + /** + * 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. + * FileNotFoundException is silently ignored. + * @param image the uri of the image to get the rotation + * @return the rotation value for the image, 0 if the file cannot be found. + */ + public static int getRotation(Uri image) { + InputStream is = null; + try { + is = StreamUtil.openInputStreamFromContentResolver(image); + return ExifHelper.getOrientation(is); + } catch (FileNotFoundException e) { + return 0; + } finally { + StreamUtil.close(is); + } + } + + /** + * Returns a thumbnail for a bitmap in a message. + * @param message the message to get the thumbnail for + * @param size the size to resize the original image to + * @param cacheOnly whether only cached images should be returned or not + * @return the resized thumbail + * @throws FileNotFoundException if the original image does not exist anymore or an IOException occurs. + */ + public static Bitmap getThumbnail(Message message, int size, boolean cacheOnly) + throws FileNotFoundException { + Bitmap thumbnail = ImageUtil.getBitmapFromCache(message.getUuid()); + if ((thumbnail == null) && (!cacheOnly)) { + File file = FileBackend.getFile(message); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(file, size); + Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),options); + if (fullsize == null) { + throw new FileNotFoundException(); + } + thumbnail = resize(fullsize, size); + thumbnail = rotate(thumbnail, file.getAbsolutePath()); + + ImageUtil.addBitmapToCache(message.getUuid(), thumbnail); + } + return thumbnail; + } + + /** + * Rotates an bitmap. Only the values 90°, 180° and 270° are considered to rotate the image. + * The orientation information is read using the ExifInterface. + * @param original the original bitmap + * @param srcPath the path to the original bitmap (used to read the exif information) + * @return rotated bitmap, or original bitmap if criteria are not met + * @throws IOException + */ + public static Bitmap rotate(Bitmap original, String srcPath) { + try { + ExifInterface exif = new ExifInterface(srcPath); + int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); + int rotation = 0; + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + rotation = 90; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + rotation = 180; + break; + case ExifInterface.ORIENTATION_ROTATE_270: + rotation = 270; + break; + } + if (rotation > 0) { + return rotate(original, rotation); + } + } catch (IOException e) { + Log.w("filebackend", "Error while rotating image, returning original (" + e.getMessage() + ")"); + } + return original; + } + + /** + * Rotates a bitmap with given degrees. + * @param bitmap the bitmap to be rotated + * @param degree the degrees to rotate the bitmap + * @return a newly created bitmap + */ + public static Bitmap rotate(Bitmap bitmap, int degree) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + Matrix mtx = new Matrix(); + mtx.postRotate(degree); + return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); + } + + + public static Bitmap cropCenterSquare(Uri image, int size) { + if (image == null) { + return null; + } + InputStream is = null; + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(image, size); + is = StreamUtil.openInputStreamFromContentResolver(image); + Bitmap input = BitmapFactory.decodeStream(is, null, options); + if (input == null) { + return null; + } else { + int rotation = getRotation(image); + if (rotation > 0) { + input = rotate(input, rotation); + } + return cropCenterSquare(input, size); + } + } catch (FileNotFoundException e) { + return null; + } finally { + StreamUtil.close(is); + } + } + + public static Bitmap cropCenter(Uri image, int newHeight, int newWidth) { + if (image == null) { + return null; + } + InputStream is = null; + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(image,Math.max(newHeight, newWidth)); + is = StreamUtil.openInputStreamFromContentResolver(image); + Bitmap source = BitmapFactory.decodeStream(is, null, options); + if (source == null) { + return null; + } + int sourceWidth = source.getWidth(); + int sourceHeight = source.getHeight(); + float xScale = (float) newWidth / sourceWidth; + float yScale = (float) newHeight / sourceHeight; + float scale = Math.max(xScale, yScale); + float scaledWidth = scale * sourceWidth; + float scaledHeight = scale * sourceHeight; + float left = (newWidth - scaledWidth) / 2; + float top = (newHeight - scaledHeight) / 2; + + RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight); + Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(dest); + canvas.drawBitmap(source, null, targetRect, null); + return dest; + } catch (FileNotFoundException e) { + return null; + } finally { + StreamUtil.close(is); + } + } + + public static Bitmap cropCenterSquare(Bitmap input, int size) { + int w = input.getWidth(); + int h = input.getHeight(); + + float scale = Math.max((float) size / h, (float) size / w); + + float outWidth = scale * w; + float outHeight = scale * h; + float left = (size - outWidth) / 2; + float top = (size - outHeight) / 2; + RectF target = new RectF(left, top, left + outWidth, top + outHeight); + + Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + canvas.drawBitmap(input, null, target, null); + return output; + } + + public static int calcSampleSize(Uri image, int size) throws FileNotFoundException { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(StreamUtil.openInputStreamFromContentResolver(image), null, options); + return calcSampleSize(options, size); + } + + public static int calcSampleSize(File image, int size) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(image.getAbsolutePath(), options); + return calcSampleSize(options, size); + } + + public static int calcSampleSize(BitmapFactory.Options options, int size) { + int height = options.outHeight; + int width = options.outWidth; + int inSampleSize = 1; + + if (height > size || width > size) { + int halfHeight = height / 2; + int halfWidth = width / 2; + + while ((halfHeight / inSampleSize) > size + && (halfWidth / inSampleSize) > size) { + inSampleSize *= 2; + } + } + return inSampleSize; + } + + private ImageUtil() { + // Static helper class + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/StreamUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/StreamUtil.java new file mode 100644 index 00000000..64f46314 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/StreamUtil.java @@ -0,0 +1,48 @@ +package de.thedevstack.conversationsplus.utils; + +import android.net.Uri; + +import java.io.Closeable; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import de.thedevstack.conversationsplus.ConversationsPlusApplication; + +/** + * Util to handle streams. + */ +public final class StreamUtil { + + /** + * Opens an InputStream from Uri using the ContentResolver from application. + * @see android.content.ContentResolver#openInputStream(Uri) + * @param uri the uri to open + * @return the InputStream for given uri + * @throws FileNotFoundException if the provided URI could not be opened. + */ + public static InputStream openInputStreamFromContentResolver(Uri uri) throws FileNotFoundException { + return ConversationsPlusApplication.getInstance().getContentResolver().openInputStream(uri); + } + + /** + * Closes a stream. + * IOException is silently ignored. + * @param stream the stream to close + */ + public static void close(Closeable stream) { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + } + } + } + + /** + * Avoid instantiation of util class. + */ + private StreamUtil() { + // 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 e3dcbe6c..94a0d2d7 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java @@ -20,6 +20,7 @@ import de.thedevstack.conversationsplus.entities.Transferable; import de.thedevstack.conversationsplus.entities.DownloadableFile; 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.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; @@ -90,7 +91,7 @@ public class JingleConnection implements Transferable { JingleConnection.this.mXmppConnectionService .getNotificationService().push(message); } - mXmppConnectionService.getFileBackend().updateFileParams(message); + FileBackend.updateFileParams(message); mXmppConnectionService.databaseBackend.createMessage(message); mXmppConnectionService.markMessage(message, Message.STATUS_RECEIVED); @@ -336,8 +337,7 @@ public class JingleConnection implements Transferable { this.mXmppConnectionService.getNotificationService() .push(message); } - this.file = this.mXmppConnectionService.getFileBackend() - .getFile(message, false); + this.file = FileBackend.getFile(message, false); if (message.getEncryption() == Message.ENCRYPTION_OTR) { byte[] key = conversation.getSymmetricKey(); if (key == null) { @@ -364,8 +364,7 @@ public class JingleConnection implements Transferable { Content content = new Content(this.contentCreator, this.contentName); if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { content.setTransportId(this.transportId); - this.file = this.mXmppConnectionService.getFileBackend().getFile( - message, false); + this.file = FileBackend.getFile(message, false); if (message.getEncryption() == Message.ENCRYPTION_OTR) { Conversation conversation = this.message.getConversation(); if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) { diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleInbandTransport.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleInbandTransport.java index 08b63d5b..94cce09f 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleInbandTransport.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleInbandTransport.java @@ -15,6 +15,7 @@ import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.StreamUtil; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnIqPacketReceived; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -190,7 +191,7 @@ public class JingleInbandTransport extends JingleTransport { } } catch (IOException e) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage()); - FileBackend.close(fileInputStream); + StreamUtil.close(fileInputStream); this.onFileTransmissionStatusChanged.onFileTransferAborted(); } } @@ -214,7 +215,7 @@ public class JingleInbandTransport extends JingleTransport { } } catch (IOException e) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage()); - FileBackend.close(fileOutputStream); + StreamUtil.close(fileOutputStream); this.onFileTransmissionStatusChanged.onFileTransferAborted(); } } diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java index 2e2ba4fe..72a5d7cb 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleSocks5Transport.java @@ -18,6 +18,7 @@ import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.StreamUtil; public class JingleSocks5Transport extends JingleTransport { private JingleCandidate candidate; @@ -137,7 +138,7 @@ public class JingleSocks5Transport extends JingleTransport { Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } finally { - FileBackend.close(fileInputStream); + StreamUtil.close(fileInputStream); } } }).start(); @@ -194,7 +195,7 @@ public class JingleSocks5Transport extends JingleTransport { Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } finally { - FileBackend.close(fileOutputStream); + StreamUtil.close(fileOutputStream); } } }).start(); -- cgit v1.2.3