diff --git a/androidTest/java/org/thoughtcrime/securesms/database/PartDatabaseTest.java b/androidTest/java/org/thoughtcrime/securesms/database/PartDatabaseTest.java
new file mode 100644
index 0000000000..dfba5cbccc
--- /dev/null
+++ b/androidTest/java/org/thoughtcrime/securesms/database/PartDatabaseTest.java
@@ -0,0 +1,64 @@
+package org.thoughtcrime.securesms.database;
+
+import android.net.Uri;
+import android.test.InstrumentationTestCase;
+
+import org.thoughtcrime.securesms.crypto.MasterSecret;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+import ws.com.google.android.mms.pdu.PduPart;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyFloat;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class PartDatabaseTest extends InstrumentationTestCase {
+ private static final long PART_ID = 1L;
+
+ private PartDatabase database;
+
+ @Override
+ public void setUp() {
+ database = spy(DatabaseFactory.getPartDatabase(getInstrumentation().getTargetContext()));
+ }
+
+ public void testTaskNotRunWhenThumbnailExists() throws Exception {
+ when(database.getPart(eq(PART_ID))).thenReturn(getPduPartSkeleton("x/x"));
+ doReturn(mock(InputStream.class)).when(database).getDataStream(any(MasterSecret.class), anyLong(), eq("thumbnail"));
+
+ database.getThumbnailStream(null, PART_ID);
+
+ verify(database, never()).updatePartThumbnail(any(MasterSecret.class), anyLong(), any(PduPart.class), any(InputStream.class), anyFloat());
+ }
+
+ public void testTaskRunWhenThumbnailMissing() throws Exception {
+ when(database.getPart(eq(PART_ID))).thenReturn(getPduPartSkeleton("image/png"));
+ doReturn(null).when(database).getDataStream(any(MasterSecret.class), anyLong(), eq("thumbnail"));
+ doNothing().when(database).updatePartThumbnail(any(MasterSecret.class), anyLong(), any(PduPart.class), any(InputStream.class), anyFloat());
+
+ try {
+ database.new ThumbnailFetchCallable(mock(MasterSecret.class), PART_ID).call();
+ throw new AssertionError("didn't try to generate thumbnail");
+ } catch (FileNotFoundException fnfe) {
+ // success
+ }
+ }
+
+ private PduPart getPduPartSkeleton(String contentType) {
+ PduPart part = new PduPart();
+ part.setContentType(contentType.getBytes());
+ part.setDataUri(Uri.EMPTY);
+ return part;
+ }
+}
diff --git a/build.gradle b/build.gradle
index d5e78eafca..4e9c4d16ab 100644
--- a/build.gradle
+++ b/build.gradle
@@ -16,7 +16,7 @@ apply plugin: 'witness'
repositories {
maven {
- url "https://repo1.maven.org/maven2"
+ url "https://repo1.maven.org/maven2/"
}
maven {
url "https://raw.github.com/whispersystems/maven/master/preferencefragment/releases/"
diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java
index 651d61c5e0..eecde312b4 100644
--- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java
+++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java
@@ -511,7 +511,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity {
if (avatarUri != null) {
try {
avatarBmp = BitmapUtil.getScaledCircleCroppedBitmap(GroupCreateActivity.this, masterSecret, avatarUri, AVATAR_SIZE);
- } catch (FileNotFoundException | BitmapDecodingException e) {
+ } catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
return null;
}
diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
index b41d99750c..9e6a4f9731 100644
--- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -21,6 +21,7 @@ import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
+import android.graphics.Bitmap;
import android.net.Uri;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -52,7 +53,6 @@ import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.LRUCache;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.thoughtcrime.securesms.util.Trimmer;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.libaxolotl.InvalidMessageException;
@@ -713,14 +713,14 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
if (Types.isSymmetricEncryption(contentValues.getAsLong(MESSAGE_BOX))) {
String messageText = PartParser.getMessageText(body);
- body = PartParser.getNonTextParts(body);
+ body = PartParser.getSupportedMediaParts(body);
if (!TextUtils.isEmpty(messageText)) {
contentValues.put(BODY, new MasterCipher(masterSecret).encryptBody(messageText));
}
}
- contentValues.put(PART_COUNT, PartParser.getDisplayablePartCount(body));
+ contentValues.put(PART_COUNT, PartParser.getSupportedMediaPartCount(body));
long messageId = db.insert(TABLE_NAME, null, contentValues);
diff --git a/src/org/thoughtcrime/securesms/database/PartDatabase.java b/src/org/thoughtcrime/securesms/database/PartDatabase.java
index 4f2b6753b2..624f0ccbc4 100644
--- a/src/org/thoughtcrime/securesms/database/PartDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/PartDatabase.java
@@ -22,17 +22,20 @@ import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
+import android.graphics.Bitmap;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
-import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
import org.thoughtcrime.securesms.crypto.MasterSecret;
-import org.thoughtcrime.securesms.jobs.ThumbnailGenerateJob;
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;
+import org.thoughtcrime.securesms.util.VisibleForTesting;
import java.io.ByteArrayInputStream;
import java.io.File;
@@ -42,6 +45,9 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
@@ -85,6 +91,8 @@ public class PartDatabase extends Database {
"CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + PENDING_PUSH_ATTACHMENT + ");",
};
+ private final ExecutorService thumbnailExecutor = Util.newSingleThreadedLifoExecutor();
+
public PartDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
@@ -95,12 +103,6 @@ public class PartDatabase extends Database {
return getDataStream(masterSecret, partId, DATA);
}
- public InputStream getThumbnailStream(MasterSecret masterSecret, long partId)
- throws FileNotFoundException
- {
- return getDataStream(masterSecret, partId, THUMBNAIL);
- }
-
public void updateFailedDownloadedPart(long messageId, long partId, PduPart part)
throws MmsException
{
@@ -199,12 +201,16 @@ public class PartDatabase extends Database {
void insertParts(MasterSecret masterSecret, long mmsId, PduBody body) throws MmsException {
for (int i=0;i
partData = null;
@@ -410,7 +426,13 @@ public class PartDatabase extends Database {
long partId = database.insert(TABLE_NAME, null, contentValues);
- ApplicationContext.getInstance(context).getJobManager().add(new ThumbnailGenerateJob(context, partId));
+ if (thumbnail != null) {
+ Log.w(TAG, "inserting pre-generated thumbnail");
+ ThumbnailData data = new ThumbnailData(thumbnail);
+ updatePartThumbnail(masterSecret, partId, part, data.toDataStream(), data.getAspectRatio());
+ } else if (!part.isPendingPush()) {
+ thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret, partId));
+ }
return partId;
}
@@ -432,9 +454,9 @@ public class PartDatabase extends Database {
values.put(SIZE, partData.second);
}
- database.update(TABLE_NAME, values, ID_WHERE, new String[] {partId+""});
+ database.update(TABLE_NAME, values, ID_WHERE, new String[]{partId+""});
- ApplicationContext.getInstance(context).getJobManager().add(new ThumbnailGenerateJob(context, partId));
+ thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret, partId));
notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
}
@@ -452,6 +474,36 @@ public class PartDatabase extends Database {
values.put(THUMBNAIL, thumbnailFile.first.getAbsolutePath());
values.put(ASPECT_RATIO, aspectRatio);
- database.update(TABLE_NAME, values, ID_WHERE, new String[] {partId + ""});
+ database.update(TABLE_NAME, values, ID_WHERE, new String[]{partId+""});
+ }
+
+ @VisibleForTesting class ThumbnailFetchCallable implements Callable {
+ private final MasterSecret masterSecret;
+ private final long partId;
+
+ public ThumbnailFetchCallable(MasterSecret masterSecret, long partId) {
+ this.masterSecret = masterSecret;
+ this.partId = partId;
+ }
+
+ @Override
+ public InputStream call() throws Exception {
+ final InputStream stream = getDataStream(masterSecret, partId, THUMBNAIL);
+ if (stream != null) {
+ 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);
+ }
+ return getDataStream(masterSecret, partId, THUMBNAIL);
+ }
}
}
diff --git a/src/org/thoughtcrime/securesms/jobs/ThumbnailGenerateJob.java b/src/org/thoughtcrime/securesms/jobs/ThumbnailGenerateJob.java
deleted file mode 100644
index 0224d5871c..0000000000
--- a/src/org/thoughtcrime/securesms/jobs/ThumbnailGenerateJob.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package org.thoughtcrime.securesms.jobs;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.CompressFormat;
-import android.util.Log;
-import android.util.Pair;
-
-import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher;
-import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret;
-import org.thoughtcrime.securesms.crypto.MasterSecret;
-import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
-import org.thoughtcrime.securesms.crypto.SecurityEvent;
-import org.thoughtcrime.securesms.crypto.SmsCipher;
-import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
-import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
-import org.thoughtcrime.securesms.database.NoSuchMessageException;
-import org.thoughtcrime.securesms.database.PartDatabase;
-import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
-import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
-import org.thoughtcrime.securesms.notifications.MessageNotifier;
-import org.thoughtcrime.securesms.service.KeyCachingService;
-import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
-import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
-import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
-import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage;
-import org.thoughtcrime.securesms.sms.IncomingTextMessage;
-import org.thoughtcrime.securesms.sms.MessageSender;
-import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
-import org.thoughtcrime.securesms.util.BitmapDecodingException;
-import org.thoughtcrime.securesms.util.BitmapUtil;
-import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.thoughtcrime.securesms.util.Util;
-import org.whispersystems.jobqueue.JobParameters;
-import org.whispersystems.libaxolotl.DuplicateMessageException;
-import org.whispersystems.libaxolotl.InvalidMessageException;
-import org.whispersystems.libaxolotl.InvalidVersionException;
-import org.whispersystems.libaxolotl.LegacyMessageException;
-import org.whispersystems.libaxolotl.NoSessionException;
-import org.whispersystems.libaxolotl.StaleKeyExchangeException;
-import org.whispersystems.libaxolotl.UntrustedIdentityException;
-import org.whispersystems.libaxolotl.util.guava.Optional;
-import org.whispersystems.textsecure.api.messages.TextSecureGroup;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-
-import ws.com.google.android.mms.ContentType;
-import ws.com.google.android.mms.MmsException;
-import ws.com.google.android.mms.pdu.PduPart;
-
-public class ThumbnailGenerateJob extends MasterSecretJob {
-
- private static final String TAG = ThumbnailGenerateJob.class.getSimpleName();
-
- private final long partId;
-
- public ThumbnailGenerateJob(Context context, long partId) {
- super(context, JobParameters.newBuilder()
- .withRequirement(new MasterSecretRequirement(context))
- .create());
-
- this.partId = partId;
- }
-
- @Override
- public void onAdded() { }
-
- @Override
- public void onRun(MasterSecret masterSecret) throws MmsException {
- PartDatabase database = DatabaseFactory.getPartDatabase(context);
- PduPart part = database.getPart(partId);
-
- if (part.getThumbnailUri() != null) {
- return;
- }
-
- long startMillis = System.currentTimeMillis();
- Bitmap thumbnail = generateThumbnailForPart(masterSecret, part);
-
- if (thumbnail != null) {
- ByteArrayOutputStream thumbnailBytes = new ByteArrayOutputStream();
- thumbnail.compress(CompressFormat.JPEG, 85, thumbnailBytes);
-
- float aspectRatio = (float)thumbnail.getWidth() / (float)thumbnail.getHeight();
- Log.w(TAG, String.format("generated thumbnail for part #%d, %dx%d (%.3f:1) in %dms",
- partId,
- thumbnail.getWidth(),
- thumbnail.getHeight(),
- aspectRatio, System.currentTimeMillis() - startMillis));
- database.updatePartThumbnail(masterSecret, partId, part, new ByteArrayInputStream(thumbnailBytes.toByteArray()), aspectRatio);
- } else {
- Log.w(TAG, "thumbnail not generated");
- }
- }
-
- private Bitmap generateThumbnailForPart(MasterSecret masterSecret, PduPart part) {
- String contentType = Util.toIsoString(part.getContentType());
-
- if (ContentType.isImageType(contentType)) return generateImageThumbnail(masterSecret, part);
- else return null;
- }
-
- private Bitmap generateImageThumbnail(MasterSecret masterSecret, PduPart part) {
- try {
- int maxSize = context.getResources().getDimensionPixelSize(R.dimen.thumbnail_max_size);
- return BitmapUtil.createScaledBitmap(context, masterSecret, part.getDataUri(), maxSize, maxSize);
- } catch (FileNotFoundException | BitmapDecodingException | OutOfMemoryError e) {
- Log.w(TAG, e);
- return null;
- }
- }
-
- @Override
- public boolean onShouldRetryThrowable(Exception exception) {
- return false;
- }
-
- @Override
- public void onCanceled() { }
-}
diff --git a/src/org/thoughtcrime/securesms/mms/ImageSlide.java b/src/org/thoughtcrime/securesms/mms/ImageSlide.java
index a2ee5f88e9..66160c3135 100644
--- a/src/org/thoughtcrime/securesms/mms/ImageSlide.java
+++ b/src/org/thoughtcrime/securesms/mms/ImageSlide.java
@@ -31,11 +31,14 @@ import android.util.Log;
import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
-import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.LRUCache;
+import org.thoughtcrime.securesms.util.MediaUtil;
+import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData;
import org.thoughtcrime.securesms.util.SmilUtil;
+import org.thoughtcrime.securesms.util.Util;
import org.w3c.dom.smil.SMILDocument;
import org.w3c.dom.smil.SMILMediaElement;
import org.w3c.dom.smil.SMILRegionElement;
@@ -81,9 +84,19 @@ public class ImageSlide extends Slide {
try {
Bitmap thumbnailBitmap;
long startDecode = System.currentTimeMillis();
- Log.w(TAG, (part.getThumbnailUri() == null ? "generating" : "fetching pre-generated") + " thumbnail");
- if (part.getThumbnailUri() != null) thumbnailBitmap = BitmapFactory.decodeStream(PartAuthority.getPartStream(context, masterSecret, part.getThumbnailUri()));
- else thumbnailBitmap = BitmapUtil.createScaledBitmap(context, masterSecret, getUri(), maxWidth, maxHeight);
+
+ if (part.getDataUri() != null && part.getId() > -1) {
+ thumbnailBitmap = BitmapFactory.decodeStream(DatabaseFactory.getPartDatabase(context)
+ .getThumbnailStream(masterSecret, part.getId()));
+ } else if (part.getDataUri() != null) {
+ Log.w(TAG, "generating thumbnail for new part");
+ ThumbnailData thumbnailData = MediaUtil.generateThumbnail(context, masterSecret,
+ part.getDataUri(), Util.toIsoString(part.getContentType()));
+ thumbnailBitmap = thumbnailData.getBitmap();
+ part.setThumbnail(thumbnailBitmap);
+ } else {
+ throw new FileNotFoundException("no data location specified");
+ }
Log.w(TAG, "thumbnail decode/generate time: " + (System.currentTimeMillis() - startDecode) + "ms");
@@ -91,11 +104,8 @@ public class ImageSlide extends Slide {
thumbnailCache.put(part.getDataUri(), new SoftReference<>(thumbnail));
return thumbnail;
- } catch (FileNotFoundException e) {
- Log.w("ImageSlide", e);
- return context.getResources().getDrawable(R.drawable.ic_missing_thumbnail_picture);
- } catch (BitmapDecodingException e) {
- Log.w("ImageSlide", e);
+ } catch (IOException | BitmapDecodingException e) {
+ Log.w(TAG, e);
return context.getResources().getDrawable(R.drawable.ic_missing_thumbnail_picture);
}
}
diff --git a/src/org/thoughtcrime/securesms/mms/PartAuthority.java b/src/org/thoughtcrime/securesms/mms/PartAuthority.java
index de8c02092f..2cc6883a5e 100644
--- a/src/org/thoughtcrime/securesms/mms/PartAuthority.java
+++ b/src/org/thoughtcrime/securesms/mms/PartAuthority.java
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.database.PartDatabase;
import org.thoughtcrime.securesms.providers.PartProvider;
import java.io.FileNotFoundException;
+import java.io.IOException;
import java.io.InputStream;
public class PartAuthority {
@@ -33,7 +34,7 @@ public class PartAuthority {
}
public static InputStream getPartStream(Context context, MasterSecret masterSecret, Uri uri)
- throws FileNotFoundException
+ throws IOException
{
PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context);
int match = uriMatcher.match(uri);
diff --git a/src/org/thoughtcrime/securesms/mms/PartParser.java b/src/org/thoughtcrime/securesms/mms/PartParser.java
index 74986c437a..8d3fbc4f1f 100644
--- a/src/org/thoughtcrime/securesms/mms/PartParser.java
+++ b/src/org/thoughtcrime/securesms/mms/PartParser.java
@@ -37,11 +37,11 @@ public class PartParser {
return bodyText;
}
- public static PduBody getNonTextParts(PduBody body) {
+ public static PduBody getSupportedMediaParts(PduBody body) {
PduBody stripped = new PduBody();
for (int i=0;i