use PartDatabase to look up thumbnails async

// FREEBIE

Closes #2286
This commit is contained in:
Jake McGinty 2014-12-30 01:36:51 -08:00 committed by Moxie Marlinspike
parent 3e6e28e688
commit 12845da91a
15 changed files with 284 additions and 188 deletions

View File

@ -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;
}
}

View File

@ -16,7 +16,7 @@ apply plugin: 'witness'
repositories { repositories {
maven { maven {
url "https://repo1.maven.org/maven2" url "https://repo1.maven.org/maven2/"
} }
maven { maven {
url "https://raw.github.com/whispersystems/maven/master/preferencefragment/releases/" url "https://raw.github.com/whispersystems/maven/master/preferencefragment/releases/"

View File

@ -511,7 +511,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity {
if (avatarUri != null) { if (avatarUri != null) {
try { try {
avatarBmp = BitmapUtil.getScaledCircleCroppedBitmap(GroupCreateActivity.this, masterSecret, avatarUri, AVATAR_SIZE); avatarBmp = BitmapUtil.getScaledCircleCroppedBitmap(GroupCreateActivity.this, masterSecret, avatarUri, AVATAR_SIZE);
} catch (FileNotFoundException | BitmapDecodingException e) { } catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e); Log.w(TAG, e);
return null; return null;
} }

View File

@ -21,6 +21,7 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.text.TextUtils; 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.LRUCache;
import org.thoughtcrime.securesms.util.ListenableFutureTask; import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Trimmer;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.jobqueue.JobManager; import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidMessageException;
@ -713,14 +713,14 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
if (Types.isSymmetricEncryption(contentValues.getAsLong(MESSAGE_BOX))) { if (Types.isSymmetricEncryption(contentValues.getAsLong(MESSAGE_BOX))) {
String messageText = PartParser.getMessageText(body); String messageText = PartParser.getMessageText(body);
body = PartParser.getNonTextParts(body); body = PartParser.getSupportedMediaParts(body);
if (!TextUtils.isEmpty(messageText)) { if (!TextUtils.isEmpty(messageText)) {
contentValues.put(BODY, new MasterCipher(masterSecret).encryptBody(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); long messageId = db.insert(TABLE_NAME, null, contentValues);

View File

@ -22,17 +22,20 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream; import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.jobs.ThumbnailGenerateJob;
import org.thoughtcrime.securesms.mms.PartAuthority; 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.Util;
import org.thoughtcrime.securesms.util.VisibleForTesting;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
@ -42,6 +45,9 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; 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.ContentType;
import ws.com.google.android.mms.MmsException; 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 + ");", "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) { public PartDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);
} }
@ -95,12 +103,6 @@ public class PartDatabase extends Database {
return getDataStream(masterSecret, partId, DATA); 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) public void updateFailedDownloadedPart(long messageId, long partId, PduPart part)
throws MmsException throws MmsException
{ {
@ -199,12 +201,16 @@ public class PartDatabase extends Database {
void insertParts(MasterSecret masterSecret, long mmsId, PduBody body) throws MmsException { void insertParts(MasterSecret masterSecret, long mmsId, PduBody body) throws MmsException {
for (int i=0;i<body.getPartsNum();i++) { for (int i=0;i<body.getPartsNum();i++) {
long partId = insertPart(masterSecret, body.getPart(i), mmsId); PduPart part = body.getPart(i);
long partId = insertPart(masterSecret, part, mmsId, part.getThumbnail());
Log.w(TAG, "Inserted part at ID: " + partId); Log.w(TAG, "Inserted part at ID: " + partId);
} }
} }
private void getPartValues(PduPart part, Cursor cursor) { private void getPartValues(PduPart part, Cursor cursor) {
part.setId(cursor.getLong(cursor.getColumnIndexOrThrow(ID)));
int charsetColumn = cursor.getColumnIndexOrThrow(CHARSET); int charsetColumn = cursor.getColumnIndexOrThrow(CHARSET);
if (!cursor.isNull(charsetColumn)) if (!cursor.isNull(charsetColumn))
@ -250,12 +256,6 @@ public class PartDatabase extends Database {
if (!cursor.isNull(pendingPushColumn)) if (!cursor.isNull(pendingPushColumn))
part.setPendingPush(cursor.getInt(pendingPushColumn) == 1); part.setPendingPush(cursor.getInt(pendingPushColumn) == 1);
int thumbnailColumn = cursor.getColumnIndexOrThrow(THUMBNAIL);
if (!cursor.isNull(thumbnailColumn))
part.setThumbnailUri(ContentUris.withAppendedId(PartAuthority.THUMB_CONTENT_URI,
cursor.getLong(cursor.getColumnIndexOrThrow(ID))));
int sizeColumn = cursor.getColumnIndexOrThrow(SIZE); int sizeColumn = cursor.getColumnIndexOrThrow(SIZE);
if (!cursor.isNull(sizeColumn)) if (!cursor.isNull(sizeColumn))
@ -320,7 +320,7 @@ public class PartDatabase extends Database {
return new EncryptingPartOutputStream(path, masterSecret); return new EncryptingPartOutputStream(path, masterSecret);
} }
private InputStream getDataStream(MasterSecret masterSecret, long partId, String dataType) @VisibleForTesting InputStream getDataStream(MasterSecret masterSecret, long partId, String dataType)
throws FileNotFoundException throws FileNotFoundException
{ {
SQLiteDatabase database = databaseHelper.getReadableDatabase(); SQLiteDatabase database = databaseHelper.getReadableDatabase();
@ -332,7 +332,7 @@ public class PartDatabase extends Database {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
if (cursor.isNull(0)) { if (cursor.isNull(0)) {
throw new FileNotFoundException("No part data for id: " + partId); return null;
} }
return getPartInputStream(masterSecret, new File(cursor.getString(0))); return getPartInputStream(masterSecret, new File(cursor.getString(0)));
@ -374,23 +374,39 @@ public class PartDatabase extends Database {
} else { } else {
throw new MmsException("Part is empty!"); throw new MmsException("Part is empty!");
} }
} catch (FileNotFoundException e) { } catch (IOException e) {
throw new MmsException(e); throw new MmsException(e);
} }
} }
public InputStream getThumbnailStream(final MasterSecret masterSecret, final long partId) throws IOException {
Log.w(TAG, "getThumbnailStream(" + partId + ")");
final InputStream dataStream = getDataStream(masterSecret, partId, THUMBNAIL);
if (dataStream != null) {
return dataStream;
}
try {
return thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret, partId)).get();
} catch (InterruptedException ie) {
throw new AssertionError("interrupted");
} catch (ExecutionException ee) {
Log.w(TAG, ee);
throw new IOException(ee);
}
}
private PduPart getPart(Cursor cursor) { private PduPart getPart(Cursor cursor) {
PduPart part = new PduPart(); PduPart part = new PduPart();
long partId = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
getPartValues(part, cursor); getPartValues(part, cursor);
part.setDataUri(ContentUris.withAppendedId(PartAuthority.PART_CONTENT_URI, partId)); part.setDataUri(ContentUris.withAppendedId(PartAuthority.PART_CONTENT_URI, part.getId()));
return part; return part;
} }
private long insertPart(MasterSecret masterSecret, PduPart part, long mmsId) throws MmsException { private long insertPart(MasterSecret masterSecret, PduPart part, long mmsId, Bitmap thumbnail) throws MmsException {
Log.w(TAG, "inserting part to mms " + mmsId); Log.w(TAG, "inserting part to mms " + mmsId);
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
Pair<File, Long> partData = null; Pair<File, Long> partData = null;
@ -410,7 +426,13 @@ public class PartDatabase extends Database {
long partId = database.insert(TABLE_NAME, null, contentValues); 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; return partId;
} }
@ -432,9 +454,9 @@ public class PartDatabase extends Database {
values.put(SIZE, partData.second); 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)); notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId));
} }
@ -452,6 +474,36 @@ public class PartDatabase extends Database {
values.put(THUMBNAIL, thumbnailFile.first.getAbsolutePath()); values.put(THUMBNAIL, thumbnailFile.first.getAbsolutePath());
values.put(ASPECT_RATIO, aspectRatio); 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<InputStream> {
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);
}
} }
} }

View File

@ -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() { }
}

View File

@ -31,11 +31,14 @@ import android.util.Log;
import android.widget.ImageView; import android.widget.ImageView;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.LRUCache; 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.SmilUtil;
import org.thoughtcrime.securesms.util.Util;
import org.w3c.dom.smil.SMILDocument; import org.w3c.dom.smil.SMILDocument;
import org.w3c.dom.smil.SMILMediaElement; import org.w3c.dom.smil.SMILMediaElement;
import org.w3c.dom.smil.SMILRegionElement; import org.w3c.dom.smil.SMILRegionElement;
@ -81,9 +84,19 @@ public class ImageSlide extends Slide {
try { try {
Bitmap thumbnailBitmap; Bitmap thumbnailBitmap;
long startDecode = System.currentTimeMillis(); 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())); if (part.getDataUri() != null && part.getId() > -1) {
else thumbnailBitmap = BitmapUtil.createScaledBitmap(context, masterSecret, getUri(), maxWidth, maxHeight); 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"); 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)); thumbnailCache.put(part.getDataUri(), new SoftReference<>(thumbnail));
return thumbnail; return thumbnail;
} catch (FileNotFoundException e) { } catch (IOException | BitmapDecodingException e) {
Log.w("ImageSlide", e); Log.w(TAG, e);
return context.getResources().getDrawable(R.drawable.ic_missing_thumbnail_picture);
} catch (BitmapDecodingException e) {
Log.w("ImageSlide", e);
return context.getResources().getDrawable(R.drawable.ic_missing_thumbnail_picture); return context.getResources().getDrawable(R.drawable.ic_missing_thumbnail_picture);
} }
} }

View File

@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.database.PartDatabase;
import org.thoughtcrime.securesms.providers.PartProvider; import org.thoughtcrime.securesms.providers.PartProvider;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
public class PartAuthority { public class PartAuthority {
@ -33,7 +34,7 @@ public class PartAuthority {
} }
public static InputStream getPartStream(Context context, MasterSecret masterSecret, Uri uri) public static InputStream getPartStream(Context context, MasterSecret masterSecret, Uri uri)
throws FileNotFoundException throws IOException
{ {
PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context); PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context);
int match = uriMatcher.match(uri); int match = uriMatcher.match(uri);

View File

@ -37,11 +37,11 @@ public class PartParser {
return bodyText; return bodyText;
} }
public static PduBody getNonTextParts(PduBody body) { public static PduBody getSupportedMediaParts(PduBody body) {
PduBody stripped = new PduBody(); PduBody stripped = new PduBody();
for (int i=0;i<body.getPartsNum();i++) { for (int i=0;i<body.getPartsNum();i++) {
if (!ContentType.isTextType(Util.toIsoString(body.getPart(i).getContentType()))) { if (isDisplayableMedia(Util.toIsoString(body.getPart(i).getContentType()))) {
stripped.addPart(body.getPart(i)); stripped.addPart(body.getPart(i));
} }
} }
@ -49,20 +49,23 @@ public class PartParser {
return stripped; return stripped;
} }
public static int getDisplayablePartCount(PduBody body) { public static int getSupportedMediaPartCount(PduBody body) {
int partCount = 0; int partCount = 0;
for (int i=0;i<body.getPartsNum();i++) { for (int i=0;i<body.getPartsNum();i++) {
String contentType = Util.toIsoString(body.getPart(i).getContentType()); String contentType = Util.toIsoString(body.getPart(i).getContentType());
if (ContentType.isImageType(contentType) || if (isDisplayableMedia(contentType)) {
ContentType.isAudioType(contentType) ||
ContentType.isVideoType(contentType))
{
partCount++; partCount++;
} }
} }
return partCount; return partCount;
} }
private static boolean isDisplayableMedia(String contentType) {
return ContentType.isImageType(contentType) ||
ContentType.isAudioType(contentType) ||
ContentType.isVideoType(contentType);
}
} }

View File

@ -80,6 +80,8 @@ public abstract class Slide {
imageView.setImageDrawable(getThumbnail(imageView.getWidth(), imageView.getHeight())); imageView.setImageDrawable(getThumbnail(imageView.getWidth(), imageView.getHeight()));
} }
public Bitmap getGeneratedThumbnail() { return null; }
public boolean hasImage() { public boolean hasImage() {
return false; return false;
} }

View File

@ -18,9 +18,9 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context; import android.content.Context;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.dom.smil.parser.SmilXmlSerializer; import org.thoughtcrime.securesms.dom.smil.parser.SmilXmlSerializer;
import org.thoughtcrime.securesms.util.SmilUtil; import org.thoughtcrime.securesms.util.SmilUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.util;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Matrix; import android.graphics.Matrix;
@ -20,6 +21,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
@ -66,7 +68,7 @@ public class BitmapUtil {
} }
public static Bitmap createScaledBitmap(Context context, MasterSecret masterSecret, Uri uri, int maxWidth, int maxHeight) public static Bitmap createScaledBitmap(Context context, MasterSecret masterSecret, Uri uri, int maxWidth, int maxHeight)
throws BitmapDecodingException, FileNotFoundException throws BitmapDecodingException, IOException
{ {
Bitmap bitmap; Bitmap bitmap;
try { try {
@ -80,7 +82,7 @@ public class BitmapUtil {
} }
private static Bitmap createScaledBitmap(Context context, MasterSecret masterSecret, Uri uri, int maxWidth, int maxHeight, boolean constrainedMemory) private static Bitmap createScaledBitmap(Context context, MasterSecret masterSecret, Uri uri, int maxWidth, int maxHeight, boolean constrainedMemory)
throws FileNotFoundException, BitmapDecodingException throws IOException, BitmapDecodingException
{ {
return createScaledBitmap(PartAuthority.getPartStream(context, masterSecret, uri), return createScaledBitmap(PartAuthority.getPartStream(context, masterSecret, uri),
PartAuthority.getPartStream(context, masterSecret, uri), PartAuthority.getPartStream(context, masterSecret, uri),
@ -221,7 +223,7 @@ public class BitmapUtil {
} }
public static Bitmap getScaledCircleCroppedBitmap(Context context, MasterSecret masterSecret, Uri uri, int destSize) public static Bitmap getScaledCircleCroppedBitmap(Context context, MasterSecret masterSecret, Uri uri, int destSize)
throws FileNotFoundException, BitmapDecodingException throws IOException, BitmapDecodingException
{ {
Bitmap bitmap = createScaledBitmap(context, masterSecret, uri, destSize, destSize); Bitmap bitmap = createScaledBitmap(context, masterSecret, uri, destSize, destSize);
return getScaledCircleCroppedBitmap(bitmap, destSize); return getScaledCircleCroppedBitmap(bitmap, destSize);
@ -249,6 +251,12 @@ public class BitmapUtil {
return output; return output;
} }
public static InputStream toCompressedJpeg(Bitmap bitmap) {
ByteArrayOutputStream thumbnailBytes = new ByteArrayOutputStream();
bitmap.compress(CompressFormat.JPEG, 85, thumbnailBytes);
return new ByteArrayInputStream(thumbnailBytes.toByteArray());
}
public static byte[] toByteArray(Bitmap bitmap) { public static byte[] toByteArray(Bitmap bitmap) {
ByteArrayOutputStream stream = new ByteArrayOutputStream(); ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);

View File

@ -0,0 +1,72 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.PartDatabase;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Callable;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.PduPart;
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
{
long startMillis = System.currentTimeMillis();
ThumbnailData data;
if (ContentType.isImageType(type)) data = new ThumbnailData(generateImageThumbnail(context, masterSecret, uri));
else data = null;
if (data != null) {
Log.w(TAG, String.format("generated thumbnail for part, %dx%d (%.3f:1) in %dms",
data.getBitmap().getWidth(), data.getBitmap().getHeight(),
data.getAspectRatio(), System.currentTimeMillis() - startMillis));
}
return data;
}
private static Bitmap generateImageThumbnail(Context context, MasterSecret masterSecret, Uri uri)
throws IOException, BitmapDecodingException, OutOfMemoryError
{
int maxSize = context.getResources().getDimensionPixelSize(R.dimen.thumbnail_max_size);
return BitmapUtil.createScaledBitmap(context, masterSecret, uri, maxSize, maxSize);
}
public static class ThumbnailData {
Bitmap bitmap;
float aspectRatio;
public ThumbnailData(Bitmap bitmap) {
this.bitmap = bitmap;
this.aspectRatio = (float) bitmap.getWidth() / (float) bitmap.getHeight();
}
public Bitmap getBitmap() {
return bitmap;
}
public float getAspectRatio() {
return aspectRatio;
}
public InputStream toDataStream() {
InputStream jpegStream = BitmapUtil.toCompressedJpeg(bitmap);
bitmap.recycle();
return jpegStream;
}
}
}

View File

@ -32,8 +32,8 @@ import android.text.TextUtils;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.widget.EditText; import android.widget.EditText;
import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
import org.whispersystems.textsecure.api.util.InvalidNumberException; import org.whispersystems.textsecure.api.util.InvalidNumberException;
import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;

View File

@ -17,6 +17,7 @@
package ws.com.google.android.mms.pdu; package ws.com.google.android.mms.pdu;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -125,10 +126,11 @@ public class PduPart {
private static final String TAG = "PduPart"; private static final String TAG = "PduPart";
private Uri thumbnailUri; private long id = -1;
private boolean isEncrypted; private boolean isEncrypted;
private boolean isPendingPush; private boolean isPendingPush;
private long dataSize; private long dataSize;
private Bitmap thumbnail;
/** /**
* Empty Constructor. * Empty Constructor.
@ -154,7 +156,7 @@ public class PduPart {
} }
public void setPendingPush(boolean isPendingPush) { public void setPendingPush(boolean isPendingPush) {
this.isPendingPush = isPendingPush; this.isPendingPush = isPendingPush;
} }
@ -162,14 +164,6 @@ public class PduPart {
return isPendingPush; return isPendingPush;
} }
public void setThumbnailUri(Uri thumbnailUri) {
this.thumbnailUri = thumbnailUri;
}
public Uri getThumbnailUri() {
return this.thumbnailUri;
}
/** /**
* Set part data. The data are stored as byte array. * Set part data. The data are stored as byte array.
* *
@ -440,5 +434,21 @@ public class PduPart {
return new String(location); return new String(location);
} }
} }
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Bitmap getThumbnail() {
return thumbnail;
}
public void setThumbnail(Bitmap thumbnail) {
this.thumbnail = thumbnail;
}
} }