diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index 991fd6b945..38faea84ba 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -40,6 +40,9 @@ import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.animation.GlideAnimation; +import com.bumptech.glide.request.target.SimpleTarget; import com.google.protobuf.ByteString; import com.soundcloud.android.crop.Crop; @@ -52,12 +55,11 @@ import org.thoughtcrime.securesms.database.NotInDirectoryException; import org.thoughtcrime.securesms.database.TextSecureDirectory; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; +import org.thoughtcrime.securesms.mms.RoundedCorners; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.sms.MessageSender; -import org.thoughtcrime.securesms.util.BitmapDecodingException; -import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.GroupUtil; @@ -369,7 +371,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity { } @Override - public void onActivityResult(int reqCode, int resultCode, Intent data) { + public void onActivityResult(int reqCode, int resultCode, final Intent data) { super.onActivityResult(reqCode, resultCode, data); Uri outputFile = Uri.fromFile(new File(getCacheDir(), "cropped")); @@ -395,7 +397,18 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity { new Crop(data.getData()).output(outputFile).asSquare().start(this); break; case Crop.REQUEST_CROP: - new DecodeCropAndSetAsyncTask(Crop.getOutput(data)).execute(); + Glide.with(this).load(Crop.getOutput(data)).asBitmap().skipMemoryCache(true) + .centerCrop().override(AVATAR_SIZE, AVATAR_SIZE) + .into(new SimpleTarget() { + @Override public void onResourceReady(Bitmap resource, + GlideAnimation glideAnimation) + { + avatarBmp = resource; + Glide.with(GroupCreateActivity.this).load(Crop.getOutput(data)).skipMemoryCache(true) + .transform(new RoundedCorners(GroupCreateActivity.this, avatar.getWidth() / 2)) + .into(avatar); + } + }); } } @@ -489,32 +502,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity { return results; } - private class DecodeCropAndSetAsyncTask extends AsyncTask { - private final Uri avatarUri; - - DecodeCropAndSetAsyncTask(Uri uri) { - avatarUri = uri; - } - - @Override - protected Bitmap doInBackground(Void... voids) { - if (avatarUri != null) { - try { - avatarBmp = BitmapUtil.createScaledBitmap(GroupCreateActivity.this, masterSecret, avatarUri, AVATAR_SIZE, AVATAR_SIZE); - } catch (IOException | BitmapDecodingException e) { - Log.w(TAG, e); - return null; - } - } - return avatarBmp; - } - - @Override - protected void onPostExecute(Bitmap result) { - if (avatarBmp != null) avatar.setImageBitmap(avatarBmp); - } - } - private class CreateMmsGroupAsyncTask extends AsyncTask { @Override diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java index 2aebf5ef6a..000b653964 100644 --- a/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java @@ -21,16 +21,15 @@ import android.util.SparseArray; import android.widget.TextView; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.FutureTaskListener; import org.thoughtcrime.securesms.util.ListenableFutureTask; import org.thoughtcrime.securesms.util.Util; import java.io.IOException; -import java.io.InputStream; import java.lang.ref.SoftReference; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -222,7 +221,7 @@ public class EmojiProvider { try { Log.w(TAG, "loading page " + model.getSprite()); return loadPage(); - } catch (IOException ioe) { + } catch (IOException | ExecutionException ioe) { Log.w(TAG, ioe); } return null; @@ -243,23 +242,19 @@ public class EmojiProvider { return task; } - private Bitmap loadPage() throws IOException { + private Bitmap loadPage() throws IOException, ExecutionException { if (bitmapReference != null && bitmapReference.get() != null) return bitmapReference.get(); try { - final InputStream measureStream = context.getAssets().open(model.getSprite()); - final InputStream bitmapStream = context.getAssets().open(model.getSprite()); - final Bitmap bitmap = BitmapUtil.createScaledBitmap(measureStream, bitmapStream, decodeScale); + final Bitmap bitmap = BitmapUtil.createScaledBitmap(context, + "file:///android_asset/" + model.getSprite(), + decodeScale); bitmapReference = new SoftReference<>(bitmap); Log.w(TAG, "onPageLoaded(" + model.getSprite() + ")"); return bitmap; - } catch (IOException ioe) { - Log.w(TAG, ioe); - throw ioe; - } catch (BitmapDecodingException bde) { - Log.w(TAG, "page " + model + " failed."); - Log.w(TAG, bde); - throw new AssertionError("emoji sprite asset is corrupted or android decoding is broken"); + } catch (ExecutionException e) { + Log.w(TAG, e); + throw e; } } diff --git a/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java b/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java index 6cebe77f28..b17e3128dd 100644 --- a/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java +++ b/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java @@ -1,22 +1,19 @@ package org.thoughtcrime.securesms.contacts.avatars; import android.content.Context; +import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; -import android.provider.ContactsContract; import android.support.annotation.DrawableRes; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; -import android.util.Log; + +import com.bumptech.glide.Glide; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.BitmapDecodingException; -import org.thoughtcrime.securesms.util.BitmapUtil; +import org.thoughtcrime.securesms.mms.ContactPhotoUriLoader.ContactPhotoUri; -import java.io.InputStream; +import java.util.concurrent.ExecutionException; public class ContactPhotoFactory { @@ -41,17 +38,16 @@ public class ContactPhotoFactory { public static ContactPhoto getContactPhoto(Context context, Uri uri, String name) { try { - InputStream inputStream = getContactPhotoStream(context, uri); - int targetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); - - if (inputStream != null) { - return new BitmapContactPhoto(BitmapUtil.createScaledBitmap(inputStream, getContactPhotoStream(context, uri), targetSize, targetSize)); - } - } catch (BitmapDecodingException e) { - Log.w(TAG, e); + int targetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); + Bitmap bitmap = Glide.with(context) + .load(new ContactPhotoUri(uri)).asBitmap() + .centerCrop().into(targetSize, targetSize).get(); + return new BitmapContactPhoto(bitmap); + } catch (ExecutionException e) { + return getDefaultContactPhoto(name); + } catch (InterruptedException e) { + throw new AssertionError(e); } - - return getDefaultContactPhoto(name); } public static ContactPhoto getGroupContactPhoto(@Nullable byte[] avatar) { @@ -59,12 +55,4 @@ public class ContactPhotoFactory { return new BitmapContactPhoto(BitmapFactory.decodeByteArray(avatar, 0, avatar.length)); } - - private static InputStream getContactPhotoStream(Context context, Uri uri) { - if (VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH) { - return ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), uri, true); - } else { - return ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), uri); - } - } } diff --git a/src/org/thoughtcrime/securesms/database/PartDatabase.java b/src/org/thoughtcrime/securesms/database/PartDatabase.java index e8cb0a32f4..d523e694e9 100644 --- a/src/org/thoughtcrime/securesms/database/PartDatabase.java +++ b/src/org/thoughtcrime/securesms/database/PartDatabase.java @@ -32,7 +32,6 @@ import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUnion; import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData; import org.thoughtcrime.securesms.util.Util; @@ -608,16 +607,12 @@ public class PartDatabase extends Database { return stream; } - try { - PduPart part = getPart(partId); - ThumbnailData data = MediaUtil.generateThumbnail(context, masterSecret, part.getDataUri(), Util.toIsoString(part.getContentType())); - if (data == null) { - return null; - } - updatePartThumbnail(masterSecret, partId, part, data.toDataStream(), data.getAspectRatio()); - } catch (BitmapDecodingException bde) { - throw new IOException(bde); + PduPart part = getPart(partId); + ThumbnailData data = MediaUtil.generateThumbnail(context, masterSecret, part.getDataUri(), Util.toIsoString(part.getContentType())); + if (data == null) { + return null; } + updatePartThumbnail(masterSecret, partId, part, data.toDataStream(), data.getAspectRatio()); return getDataStream(masterSecret, partId, THUMBNAIL); } @@ -661,7 +656,6 @@ public class PartDatabase extends Database { if (rowId != partId.rowId) return false; return uniqueId == partId.uniqueId; - } @Override public int hashCode() { diff --git a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java index f3a5b14670..5a8b4817f9 100644 --- a/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; +import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; import org.thoughtcrime.securesms.push.TextSecurePushTrustStore; import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapUtil; @@ -24,6 +25,7 @@ import org.whispersystems.textsecure.internal.util.StaticCredentialsProvider; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.ExecutionException; public class AvatarDownloadJob extends MasterSecretJob { @@ -61,14 +63,11 @@ public class AvatarDownloadJob extends MasterSecretJob { } attachment = downloadAttachment(relay, avatarId); - - InputStream scaleInputStream = new AttachmentCipherInputStream(attachment, key); - InputStream measureInputStream = new AttachmentCipherInputStream(attachment, key); - Bitmap avatar = BitmapUtil.createScaledBitmap(measureInputStream, scaleInputStream, 500, 500); + Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key), 500, 500); database.updateAvatar(groupId, avatar); } - } catch (InvalidMessageException | BitmapDecodingException | NonSuccessfulResponseCodeException e) { + } catch (ExecutionException | NonSuccessfulResponseCodeException e) { Log.w(TAG, e); } finally { if (attachment != null) diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java b/src/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java new file mode 100644 index 0000000000..72e3749cee --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java @@ -0,0 +1,52 @@ +package org.thoughtcrime.securesms.mms; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.load.data.StreamLocalUriFetcher; + +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.whispersystems.textsecure.api.crypto.AttachmentCipherInputStream; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class AttachmentStreamLocalUriFetcher implements DataFetcher { + private static final String TAG = AttachmentStreamLocalUriFetcher.class.getSimpleName(); + private File attachment; + private byte[] key; + private InputStream is; + + public AttachmentStreamLocalUriFetcher(File attachment, byte[] key) { + this.attachment = attachment; + this.key = key; + } + + @Override public InputStream loadData(Priority priority) throws Exception { + is = new AttachmentCipherInputStream(attachment, key); + return is; + } + + @Override public void cleanup() { + try { + if (is != null) is.close(); + is = null; + } catch (IOException ioe) { + Log.w(TAG, "ioe"); + } + } + + @Override public String getId() { + return AttachmentStreamLocalUriFetcher.class.getCanonicalName() + "::" + attachment.getAbsolutePath(); + } + + @Override public void cancel() { + + } +} diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java b/src/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java new file mode 100644 index 0000000000..1c8abdb6cd --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java @@ -0,0 +1,63 @@ +package org.thoughtcrime.securesms.mms; + +import android.content.Context; +import android.net.Uri; + +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.load.model.GenericLoaderFactory; +import com.bumptech.glide.load.model.ModelLoader; +import com.bumptech.glide.load.model.ModelLoaderFactory; +import com.bumptech.glide.load.model.stream.StreamModelLoader; + +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; +import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; + +import java.io.File; +import java.io.InputStream; + +/** + * A {@link ModelLoader} for translating uri models into {@link InputStream} data. Capable of handling 'http', + * 'https', 'android.resource', 'content', and 'file' schemes. Unsupported schemes will throw an exception in + * {@link #getResourceFetcher(Uri, int, int)}. + */ +public class AttachmentStreamUriLoader implements StreamModelLoader { + private final Context context; + + /** + * THe default factory for {@link com.bumptech.glide.load.model.stream.StreamUriLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + + @Override + public StreamModelLoader build(Context context, GenericLoaderFactory factories) { + return new AttachmentStreamUriLoader(context); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public AttachmentStreamUriLoader(Context context) { + this.context = context; + } + + @Override + public DataFetcher getResourceFetcher(AttachmentModel model, int width, int height) { + return new AttachmentStreamLocalUriFetcher(model.attachment, model.key); + } + + public static class AttachmentModel { + public File attachment; + public byte[] key; + + public AttachmentModel(File attachment, byte[] key) { + this.attachment = attachment; + this.key = key; + } + } +} + diff --git a/src/org/thoughtcrime/securesms/mms/ContactPhotoLocalUriFetcher.java b/src/org/thoughtcrime/securesms/mms/ContactPhotoLocalUriFetcher.java new file mode 100644 index 0000000000..b714f25f7c --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/ContactPhotoLocalUriFetcher.java @@ -0,0 +1,32 @@ +package org.thoughtcrime.securesms.mms; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.ContactsContract; + +import com.bumptech.glide.load.data.StreamLocalUriFetcher; + +import java.io.FileNotFoundException; +import java.io.InputStream; + +public class ContactPhotoLocalUriFetcher extends StreamLocalUriFetcher { + private static final String TAG = ContactPhotoLocalUriFetcher.class.getSimpleName(); + + public ContactPhotoLocalUriFetcher(Context context, Uri uri) { + super(context, uri); + } + + @Override + protected InputStream loadResource(Uri uri, ContentResolver contentResolver) + throws FileNotFoundException + { + if (VERSION.SDK_INT >= VERSION_CODES.ICE_CREAM_SANDWICH) { + return ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, true); + } else { + return ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri); + } + } +} diff --git a/src/org/thoughtcrime/securesms/mms/ContactPhotoUriLoader.java b/src/org/thoughtcrime/securesms/mms/ContactPhotoUriLoader.java new file mode 100644 index 0000000000..0802e2fa36 --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/ContactPhotoUriLoader.java @@ -0,0 +1,51 @@ +package org.thoughtcrime.securesms.mms; + +import android.content.Context; +import android.net.Uri; + +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.load.model.GenericLoaderFactory; +import com.bumptech.glide.load.model.ModelLoaderFactory; +import com.bumptech.glide.load.model.stream.StreamModelLoader; + +import org.thoughtcrime.securesms.mms.ContactPhotoUriLoader.ContactPhotoUri; + +import java.io.InputStream; + +public class ContactPhotoUriLoader implements StreamModelLoader { + private final Context context; + + /** + * THe default factory for {@link com.bumptech.glide.load.model.stream.StreamUriLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + + @Override + public StreamModelLoader build(Context context, GenericLoaderFactory factories) { + return new ContactPhotoUriLoader(context); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public ContactPhotoUriLoader(Context context) { + this.context = context; + } + + @Override + public DataFetcher getResourceFetcher(ContactPhotoUri model, int width, int height) { + return new ContactPhotoLocalUriFetcher(context, model.uri); + } + + public static class ContactPhotoUri { + public Uri uri; + + public ContactPhotoUri(Uri uri) { + this.uri = uri; + } + } +} + diff --git a/src/org/thoughtcrime/securesms/mms/MediaConstraints.java b/src/org/thoughtcrime/securesms/mms/MediaConstraints.java index 90de4d46d1..c59ea2b2ee 100644 --- a/src/org/thoughtcrime/securesms/mms/MediaConstraints.java +++ b/src/org/thoughtcrime/securesms/mms/MediaConstraints.java @@ -1,20 +1,22 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; +import android.graphics.Bitmap.CompressFormat; import android.net.Uri; import android.util.Log; import android.util.Pair; import com.bumptech.glide.Glide; -import com.bumptech.glide.gifdecoder.GifDecoder; import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.MediaUtil; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.ExecutionException; import ws.com.google.android.mms.pdu.PduPart; @@ -64,15 +66,10 @@ public abstract class MediaConstraints { if (!canResize(part) || part.getDataUri() == null) { throw new UnsupportedOperationException("Cannot resize this content type"); } - try { - return BitmapUtil.createScaledBytes(context, masterSecret, part.getDataUri(), - getImageMaxWidth(context), - getImageMaxHeight(context), - getImageMaxSize()); - } catch (BitmapDecodingException bde) { - throw new IOException(bde); + return BitmapUtil.createScaledBytes(context, new DecryptableUri(masterSecret, part.getDataUri()), this); + } catch (ExecutionException ee) { + throw new IOException(ee); } } - } diff --git a/src/org/thoughtcrime/securesms/mms/RoundedCorners.java b/src/org/thoughtcrime/securesms/mms/RoundedCorners.java index 594876069d..0e777fb102 100644 --- a/src/org/thoughtcrime/securesms/mms/RoundedCorners.java +++ b/src/org/thoughtcrime/securesms/mms/RoundedCorners.java @@ -10,12 +10,13 @@ import android.graphics.RectF; import android.graphics.Shader.TileMode; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.Log; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; import com.bumptech.glide.load.resource.bitmap.TransformationUtils; +import org.thoughtcrime.securesms.util.ResUtil; + public class RoundedCorners extends BitmapTransformation { private final boolean crop; private final int radius; @@ -28,6 +29,10 @@ public class RoundedCorners extends BitmapTransformation { this.colorHint = colorHint; } + public RoundedCorners(@NonNull Context context, int radius) { + this(context, true, radius, ResUtil.getColor(context, android.R.attr.windowBackground)); + } + @Override protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) { diff --git a/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java b/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java index 8e27baab3c..d7f81ba33f 100644 --- a/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java +++ b/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java @@ -8,6 +8,8 @@ import com.bumptech.glide.load.engine.cache.DiskCache; import com.bumptech.glide.load.engine.cache.DiskCacheAdapter; import com.bumptech.glide.module.GlideModule; +import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; +import org.thoughtcrime.securesms.mms.ContactPhotoUriLoader.ContactPhotoUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import java.io.InputStream; @@ -21,6 +23,8 @@ public class TextSecureGlideModule implements GlideModule { @Override public void registerComponents(Context context, Glide glide) { glide.register(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory()); + glide.register(ContactPhotoUri.class, InputStream.class, new ContactPhotoUriLoader.Factory()); + glide.register(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory()); } public static class NoopDiskCacheFactory implements DiskCache.Factory { diff --git a/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/src/org/thoughtcrime/securesms/util/BitmapUtil.java index fef86d5da8..bebe9c4907 100644 --- a/src/org/thoughtcrime/securesms/util/BitmapUtil.java +++ b/src/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -3,222 +3,110 @@ package org.thoughtcrime.securesms.util; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; -import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.ImageFormat; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.YuvImage; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; import android.support.annotation.NonNull; import android.util.Log; import android.util.Pair; -import com.android.gallery3d.data.Exif; +import com.bumptech.glide.Glide; +import com.bumptech.glide.Priority; -import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.mms.PartAuthority; +import org.thoughtcrime.securesms.mms.MediaConstraints; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; public class BitmapUtil { private static final String TAG = BitmapUtil.class.getSimpleName(); - private static final int MAX_COMPRESSION_QUALITY = 95; - private static final int MIN_COMPRESSION_QUALITY = 50; + private static final int MAX_COMPRESSION_QUALITY = 80; + private static final int MIN_COMPRESSION_QUALITY = 45; private static final int MAX_COMPRESSION_ATTEMPTS = 4; - public static byte[] createScaledBytes(Context context, MasterSecret masterSecret, Uri uri, int maxWidth, int maxHeight, int maxSize) - throws IOException, BitmapDecodingException + public static byte[] createScaledBytes(Context context, T model, MediaConstraints constraints) + throws ExecutionException, IOException { - Bitmap bitmap; + int quality = MAX_COMPRESSION_QUALITY; + int attempts = 0; + byte[] bytes; + Bitmap scaledBitmap = createScaledBitmap(context, + model, + constraints.getImageMaxWidth(context), + constraints.getImageMaxHeight(context)); try { - bitmap = createScaledBitmap(context, masterSecret, uri, maxWidth, maxHeight, false); - } catch(OutOfMemoryError oome) { - Log.w(TAG, "OutOfMemoryError when scaling precisely, doing rough scale to save memory instead"); - bitmap = createScaledBitmap(context, masterSecret, uri, maxWidth, maxHeight, true); - } - int quality = MAX_COMPRESSION_QUALITY; - int attempts = 0; + do { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + scaledBitmap.compress(CompressFormat.JPEG, quality, baos); + bytes = baos.toByteArray(); - ByteArrayOutputStream baos; - - do { - baos = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos); - - quality = Math.max((quality * maxSize) / baos.size(), MIN_COMPRESSION_QUALITY); - } while (baos.size() > maxSize && attempts++ < MAX_COMPRESSION_ATTEMPTS); - - Log.w(TAG, "createScaledBytes(" + uri + ") -> quality " + Math.min(quality, MAX_COMPRESSION_QUALITY) + ", " + attempts + " attempt(s)"); - - bitmap.recycle(); - - if (baos.size() <= maxSize) return baos.toByteArray(); - else throw new IOException("Unable to scale image below: " + baos.size()); - } - - public static Bitmap createScaledBitmap(Context context, MasterSecret masterSecret, Uri uri, int maxWidth, int maxHeight) - throws BitmapDecodingException, IOException - { - Bitmap bitmap; - try { - bitmap = createScaledBitmap(context, masterSecret, uri, maxWidth, maxHeight, false); - } catch(OutOfMemoryError oome) { - Log.w(TAG, "OutOfMemoryError when scaling precisely, doing rough scale to save memory instead"); - bitmap = createScaledBitmap(context, masterSecret, uri, maxWidth, maxHeight, true); - } - - return bitmap; - } - - private static Bitmap createScaledBitmap(Context context, MasterSecret masterSecret, Uri uri, int maxWidth, int maxHeight, boolean constrainedMemory) - throws IOException, BitmapDecodingException - { - InputStream is = PartAuthority.getPartStream(context, masterSecret, uri); - if (is == null) throw new IOException("Couldn't obtain InputStream"); - return createScaledBitmap(is, - PartAuthority.getPartStream(context, masterSecret, uri), - PartAuthority.getPartStream(context, masterSecret, uri), - maxWidth, maxHeight, constrainedMemory); - } - - private static Bitmap createScaledBitmap(InputStream measure, InputStream orientationStream, InputStream data, - int maxWidth, int maxHeight, boolean constrainedMemory) - throws IOException, BitmapDecodingException - { - Bitmap bitmap = createScaledBitmap(measure, data, maxWidth, maxHeight, constrainedMemory); - return fixOrientation(bitmap, orientationStream); - } - - private static Bitmap createScaledBitmap(InputStream measure, InputStream data, int maxWidth, int maxHeight, - boolean constrainedMemory) - throws BitmapDecodingException - { - final BitmapFactory.Options options = getImageDimensions(measure); - return createScaledBitmap(data, maxWidth, maxHeight, options, constrainedMemory); - } - - public static Bitmap createScaledBitmap(InputStream measure, InputStream data, float scale) - throws BitmapDecodingException - { - final BitmapFactory.Options options = getImageDimensions(measure); - final int outWidth = (int)(options.outWidth * scale); - final int outHeight = (int)(options.outHeight * scale); - Log.w(TAG, "creating scaled bitmap with scale " + scale + " => " + outWidth + "x" + outHeight); - return createScaledBitmap(data, outWidth, outHeight, options, false); - } - - public static Bitmap createScaledBitmap(InputStream measure, InputStream data, int maxWidth, int maxHeight) - throws BitmapDecodingException - { - return createScaledBitmap(measure, data, maxWidth, maxHeight, false); - } - - private static Bitmap createScaledBitmap(InputStream data, int maxWidth, int maxHeight, - BitmapFactory.Options options, boolean constrainedMemory) - throws BitmapDecodingException - { - final int imageWidth = options.outWidth; - final int imageHeight = options.outHeight; - - options.inSampleSize = getScaleFactor(imageWidth, imageHeight, maxWidth, maxHeight, constrainedMemory); - options.inJustDecodeBounds = false; - options.inPreferredConfig = constrainedMemory ? Config.RGB_565 : Config.ARGB_8888; - - InputStream is = new BufferedInputStream(data); - Bitmap roughThumbnail = BitmapFactory.decodeStream(is, null, options); - try { - is.close(); - } catch (IOException ioe) { - Log.w(TAG, "IOException thrown when closing an images InputStream", ioe); - } - Log.w(TAG, "rough scale " + (imageWidth) + "x" + (imageHeight) + - " => " + (options.outWidth) + "x" + (options.outHeight)); - if (roughThumbnail == null) { - throw new BitmapDecodingException("Decoded stream was null."); - } - if (constrainedMemory) { - return roughThumbnail; - } - - if (options.outWidth > maxWidth || options.outHeight > maxHeight) { - final float aspectWidth, aspectHeight; - - if (imageWidth == 0 || imageHeight == 0) { - aspectWidth = maxWidth; - aspectHeight = maxHeight; - } else if (options.outWidth >= options.outHeight) { - aspectWidth = maxWidth; - aspectHeight = (aspectWidth / options.outWidth) * options.outHeight; - } else { - aspectHeight = maxHeight; - aspectWidth = (aspectHeight / options.outHeight) * options.outWidth; + Log.w(TAG, "iteration with quality " + quality + " size " + (bytes.length / 1024) + "kb"); + if (quality == MIN_COMPRESSION_QUALITY) break; + quality = Math.max((quality * constraints.getImageMaxSize()) / bytes.length, MIN_COMPRESSION_QUALITY); } - - final int fineWidth = Math.round(aspectWidth); - final int fineHeight = Math.round(aspectHeight); - - Log.w(TAG, "fine scale " + options.outWidth + "x" + options.outHeight + - " => " + fineWidth + "x" + fineHeight); - Bitmap scaledThumbnail = null; - try { - scaledThumbnail = Bitmap.createScaledBitmap(roughThumbnail, fineWidth, fineHeight, true); - } finally { - if (roughThumbnail != scaledThumbnail) roughThumbnail.recycle(); + while (bytes.length > constraints.getImageMaxSize() && attempts++ < MAX_COMPRESSION_ATTEMPTS); + if (bytes.length > constraints.getImageMaxSize()) { + throw new IOException("Unable to scale image below: " + bytes.length); } - return scaledThumbnail; - } else { - return roughThumbnail; + Log.w(TAG, "createScaledBytes(" + model.toString() + ") -> quality " + Math.min(quality, MAX_COMPRESSION_QUALITY) + ", " + attempts + " attempt(s)"); + return bytes; + } finally { + if (scaledBitmap != null) scaledBitmap.recycle(); } } - @VisibleForTesting static int getScaleFactor(int inWidth, int inHeight, - int maxWidth, int maxHeight, - boolean constrained) + public static Bitmap createScaledBitmap(Context context, T model, int maxWidth, int maxHeight) + throws ExecutionException { - int scaler = 1; - while (!constrained && ((inWidth / scaler / 2 >= maxWidth) && (inHeight / scaler / 2 >= maxHeight))) { - scaler *= 2; - } - while (constrained && ((inWidth / scaler > maxWidth) || (inHeight / scaler > maxHeight))) { - scaler *= 2; - } - return scaler; + final Pair dimensions = getDimensions(getInputStreamForModel(context, model)); + final Pair clamped = clampDimensions(dimensions.first, dimensions.second, + maxWidth, maxHeight); + return createScaledBitmapInto(context, model, clamped.first, clamped.second); } - private static Bitmap fixOrientation(Bitmap bitmap, InputStream orientationStream) throws IOException { - final int orientation = Exif.getOrientation(orientationStream); - orientationStream.close(); - if (orientation != 0) { - return rotateBitmap(bitmap, orientation); - } else { - return bitmap; + private static InputStream getInputStreamForModel(Context context, T model) + throws ExecutionException + { + try { + return Glide.buildStreamModelLoader(model, context) + .getResourceFetcher(model, -1, -1) + .loadData(Priority.NORMAL); + } catch (Exception e) { + throw new ExecutionException(e); } } - public static Bitmap rotateBitmap(Bitmap bitmap, int angle) { - if (angle == 0) return bitmap; - Matrix matrix = new Matrix(); - matrix.postRotate(angle); - Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); - if (rotated != bitmap) bitmap.recycle(); - return rotated; + private static Bitmap createScaledBitmapInto(Context context, T model, int width, int height) + throws ExecutionException + { + try { + return Glide.with(context) + .load(model) + .asBitmap() + .skipMemoryCache(true) + .into(width, height) + .get(); + } catch (InterruptedException ie) { + throw new AssertionError(ie); + } + } + + public static Bitmap createScaledBitmap(Context context, T model, float scale) + throws ExecutionException + { + Pair dimens = getDimensions(getInputStreamForModel(context, model)); + return createScaledBitmapInto(context, model, + (int)(dimens.first * scale), (int)(dimens.second * scale)); } private static BitmapFactory.Options getImageDimensions(InputStream inputStream) { @@ -251,27 +139,6 @@ public class BitmapUtil { return stream.toByteArray(); } - public static Bitmap getCircleBitmap(Bitmap bitmap) { - final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), - bitmap.getHeight(), Bitmap.Config.ARGB_8888); - final Canvas canvas = new Canvas(output); - - final int color = Color.RED; - final Paint paint = new Paint(); - final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); - final RectF rectF = new RectF(rect); - - paint.setAntiAlias(true); - canvas.drawARGB(0, 0, 0, 0); - paint.setColor(color); - canvas.drawOval(rectF, paint); - - paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); - canvas.drawBitmap(bitmap, rect, rect, paint); - - return output; - } - public static byte[] createFromNV21(@NonNull final byte[] data, final int width, final int height, @@ -333,6 +200,27 @@ public class BitmapUtil { return output; } + private static Pair clampDimensions(int inWidth, int inHeight, int maxWidth, int maxHeight) { + if (inWidth > maxWidth || inHeight > maxHeight) { + final float aspectWidth, aspectHeight; + + if (inWidth == 0 || inHeight == 0) { + aspectWidth = maxWidth; + aspectHeight = maxHeight; + } else if (inWidth >= inHeight) { + aspectWidth = maxWidth; + aspectHeight = (aspectWidth / inWidth) * inHeight; + } else { + aspectHeight = maxHeight; + aspectWidth = (aspectHeight / inHeight) * inWidth; + } + + return new Pair<>(Math.round(aspectWidth), Math.round(aspectHeight)); + } else { + return new Pair<>(inWidth, inHeight); + } + } + public static Bitmap createFromDrawable(final Drawable drawable, final int width, final int height) { final AtomicBoolean created = new AtomicBoolean(false); final Bitmap[] result = new Bitmap[1]; diff --git a/src/org/thoughtcrime/securesms/util/MediaUtil.java b/src/org/thoughtcrime/securesms/util/MediaUtil.java index 028a94f9d4..d63ad362db 100644 --- a/src/org/thoughtcrime/securesms/util/MediaUtil.java +++ b/src/org/thoughtcrime/securesms/util/MediaUtil.java @@ -7,9 +7,12 @@ import android.text.TextUtils; import android.util.Log; import android.webkit.MimeTypeMap; +import com.bumptech.glide.Glide; + import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.mms.AudioSlide; +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.GifSlide; import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.PartAuthority; @@ -19,6 +22,7 @@ import org.thoughtcrime.securesms.mms.VideoSlide; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.ExecutionException; import ws.com.google.android.mms.ContentType; import ws.com.google.android.mms.pdu.PduPart; @@ -27,7 +31,7 @@ public class MediaUtil { private static final String TAG = MediaUtil.class.getSimpleName(); public static ThumbnailData generateThumbnail(Context context, MasterSecret masterSecret, Uri uri, String type) - throws IOException, BitmapDecodingException, OutOfMemoryError + throws ExecutionException { long startMillis = System.currentTimeMillis(); ThumbnailData data; @@ -54,10 +58,10 @@ public class MediaUtil { } private static Bitmap generateImageThumbnail(Context context, MasterSecret masterSecret, Uri uri) - throws IOException, BitmapDecodingException, OutOfMemoryError + throws ExecutionException { - int maxSize = context.getResources().getDimensionPixelSize(R.dimen.thumbnail_max_size); - return BitmapUtil.createScaledBitmap(context, masterSecret, uri, maxSize, maxSize); + int maxSize = context.getResources().getDimensionPixelSize(R.dimen.media_bubble_height); + return BitmapUtil.createScaledBitmap(context, new DecryptableUri(masterSecret, uri), maxSize, maxSize); } public static Slide getSlideForPart(Context context, MasterSecret masterSecret, PduPart part, String contentType) {