diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml index cbb0d39e57..9b74ce2c04 100644 --- a/res/layout/conversation_item_received.xml +++ b/res/layout/conversation_item_received.xml @@ -48,11 +48,10 @@ diff --git a/res/layout/conversation_item_sent.xml b/res/layout/conversation_item_sent.xml index 3c53989fb3..be4defe5db 100644 --- a/res/layout/conversation_item_sent.xml +++ b/res/layout/conversation_item_sent.xml @@ -69,15 +69,14 @@ android:paddingBottom="7dip"> + android:id="@+id/image_view" + android:layout_width="230dip" + android:layout_height="174dip" + android:layout_gravity="center" + android:scaleType="centerInside" + android:adjustViewBounds="true" + android:background="@android:drawable/picture_frame" + android:visibility="gone" /> initializeCache() { return new LinkedHashMap() { @Override diff --git a/src/org/thoughtcrime/securesms/ConversationFragment.java b/src/org/thoughtcrime/securesms/ConversationFragment.java index e5003f8228..e458df0e9d 100644 --- a/src/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationFragment.java @@ -169,6 +169,7 @@ public class ConversationFragment extends SherlockListFragment if (this.recipients != null && this.threadId != -1) { this.setListAdapter(new ConversationAdapter(recipients, threadId, getActivity(), masterSecret, new FailedIconClickHandler())); + getListView().setRecyclerListener((ConversationAdapter)getListAdapter()); getLoaderManager().initLoader(0, null, this); } } diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index b659fd6322..32399856a0 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -21,6 +21,8 @@ import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Environment; @@ -44,20 +46,19 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.database.model.MessageRecord.GroupData; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.SendReceiveService; +import org.thoughtcrime.securesms.util.FutureTaskListener; +import org.thoughtcrime.securesms.util.ListenableFutureTask; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Iterator; -import java.util.List; /** * A view that displays an individual conversation item within a conversation @@ -85,10 +86,12 @@ public class ConversationItem extends LinearLayout { private ImageView mmsThumbnail; private Button mmsDownloadButton; private TextView mmsDownloadingLabel; + private ListenableFutureTask slideDeck; private final FailedIconClickListener failedIconClickListener = new FailedIconClickListener(); private final MmsDownloadClickListener mmsDownloadClickListener = new MmsDownloadClickListener(); private final ClickListener clickListener = new ClickListener(); + private final Handler handler = new Handler(); private final Context context; public ConversationItem(Context context) { @@ -141,6 +144,11 @@ public class ConversationItem extends LinearLayout { } } + public void unbind() { + if (slideDeck != null) + slideDeck.setListener(null); + } + public MessageRecord getMessageRecord() { return messageRecord; } @@ -231,26 +239,40 @@ public class ConversationItem extends LinearLayout { } private void setMediaMmsAttributes(MediaMmsMessageRecord messageRecord) { - SlideDeck slideDeck = messageRecord.getSlideDeck(); - - if (slideDeck != null) { - List slides = slideDeck.getSlides(); - - Iterator iterator = slides.iterator(); - - while (iterator.hasNext()) { - Slide slide = iterator.next(); - if (slide.hasImage()) { - mmsThumbnail.setImageBitmap(slide.getThumbnail()); - mmsThumbnail.setOnClickListener(new ThumbnailClickListener(slide)); - mmsThumbnail.setOnLongClickListener(new ThumbnailSaveListener(slide)); - mmsThumbnail.setVisibility(View.VISIBLE); - return; - } - } + if (messageRecord.getPartCount() > 0) { + mmsThumbnail.setVisibility(View.VISIBLE); + mmsThumbnail.setImageDrawable(new ColorDrawable(Color.TRANSPARENT)); } - mmsThumbnail.setVisibility(View.GONE); + slideDeck = messageRecord.getSlideDeck(); + slideDeck.setListener(new FutureTaskListener() { + @Override + public void onSuccess(final SlideDeck result) { + if (result == null) + return; + + handler.post(new Runnable() { + @Override + public void run() { + for (Slide slide : result.getSlides()) { + if (slide.hasImage()) { + slide.setThumbnailOn(mmsThumbnail); +// mmsThumbnail.setImageBitmap(slide.getThumbnail()); + mmsThumbnail.setOnClickListener(new ThumbnailClickListener(slide)); + mmsThumbnail.setOnLongClickListener(new ThumbnailSaveListener(slide)); + mmsThumbnail.setVisibility(View.VISIBLE); + return; + } + } + + mmsThumbnail.setVisibility(View.GONE); + } + }); + } + + @Override + public void onFailure(Throwable error) {} + }); } /// Helper Methods diff --git a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java index 7f0b185722..397b0f17a9 100644 --- a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java +++ b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java @@ -21,6 +21,7 @@ import java.util.TreeSet; public class DatabaseUpgradeActivity extends Activity { public static final int NO_MORE_KEY_EXCHANGE_PREFIX_VERSION = 46; + public static final int MMS_BODY_VERSION = 46; private static final String LAST_VERSION_CODE = "last_version_code"; diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 052b123ebf..65a2f392d5 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -24,10 +24,17 @@ import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; import org.thoughtcrime.securesms.DatabaseUpgradeActivity; +import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.util.InvalidMessageException; +import org.thoughtcrime.securesms.util.Util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import ws.com.google.android.mms.ContentType; public class DatabaseFactory { @@ -36,7 +43,8 @@ public class DatabaseFactory { private static final int INTRODUCED_DATE_SENT_VERSION = 4; private static final int INTRODUCED_DRAFTS_VERSION = 5; private static final int INTRODUCED_NEW_TYPES_VERSION = 6; - private static final int DATABASE_VERSION = 6; + private static final int INTRODUCED_MMS_BODY_VERSION = 7; + private static final int DATABASE_VERSION = 7; private static final String DATABASE_NAME = "messages.db"; private static final Object lock = new Object(); @@ -148,23 +156,31 @@ public class DatabaseFactory { MasterCipher masterCipher = new MasterCipher(masterSecret); int count = 0; SQLiteDatabase db = databaseHelper.getWritableDatabase(); - Cursor cursor = db.query("sms", + Cursor smsCursor = db.query("sms", new String[] {"_id", "type", "body"}, "type & " + 0x80000000 + " != 0", null, null, null, null); - if (cursor != null) - count = cursor.getCount(); + Cursor threadCursor = db.query("thread", + new String[] {"_id", "snippet_type", "snippet"}, + "snippet_type & " + 0x80000000 + " != 0", + null, null, null, null); + + if (smsCursor != null) + count = smsCursor.getCount(); + + if (threadCursor != null) + count += threadCursor.getCount(); db.beginTransaction(); - while (cursor != null && cursor.moveToNext()) { - listener.setProgress(cursor.getPosition(), count); + while (smsCursor != null && smsCursor.moveToNext()) { + listener.setProgress(smsCursor.getPosition(), count); try { - String body = masterCipher.decryptBody(cursor.getString(cursor.getColumnIndexOrThrow("body"))); - long type = cursor.getLong(cursor.getColumnIndexOrThrow("type")); - long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); + String body = masterCipher.decryptBody(smsCursor.getString(smsCursor.getColumnIndexOrThrow("body"))); + long type = smsCursor.getLong(smsCursor.getColumnIndexOrThrow("type")); + long id = smsCursor.getLong(smsCursor.getColumnIndexOrThrow("_id")); if (body.startsWith(KEY_EXCHANGE)) { body = body.substring(KEY_EXCHANGE.length()); @@ -193,6 +209,114 @@ public class DatabaseFactory { } } + while (threadCursor != null && threadCursor.moveToNext()) { + listener.setProgress(smsCursor.getCount() + threadCursor.getPosition(), count); + + try { + String snippet = masterCipher.decryptBody(threadCursor.getString(threadCursor.getColumnIndexOrThrow("snippet"))); + long snippetType = threadCursor.getLong(threadCursor.getColumnIndexOrThrow("snippet_type")); + long id = threadCursor.getLong(threadCursor.getColumnIndexOrThrow("_id")); + + if (snippet.startsWith(KEY_EXCHANGE)) { + snippet = snippet.substring(KEY_EXCHANGE.length()); + snippet = masterCipher.encryptBody(snippet); + snippetType |= 0x8000; + + db.execSQL("UPDATE thread SET snippet = ?, snippet_type = ? WHERE _id = ?", + new String[] {snippet, snippetType+"", id+""}); + } else if (snippet.startsWith(PROCESSED_KEY_EXCHANGE)) { + snippet = snippet.substring(PROCESSED_KEY_EXCHANGE.length()); + snippet = masterCipher.encryptBody(snippet); + snippetType |= 0x2000; + + db.execSQL("UPDATE thread SET snippet = ?, snippet_type = ? WHERE _id = ?", + new String[] {snippet, snippetType+"", id+""}); + } else if (snippet.startsWith(STALE_KEY_EXCHANGE)) { + snippet = snippet.substring(STALE_KEY_EXCHANGE.length()); + snippet = masterCipher.encryptBody(snippet); + snippetType |= 0x4000; + + db.execSQL("UPDATE thread SET snippet = ?, snippet_type = ? WHERE _id = ?", + new String[] {snippet, snippetType+"", id+""}); + } + } catch (InvalidMessageException e) { + Log.w("DatabaseFactory", e); + } + } + + db.setTransactionSuccessful(); + db.endTransaction(); + + smsCursor.close(); + threadCursor.close(); + } + + if (fromVersion < DatabaseUpgradeActivity.MMS_BODY_VERSION) { + Log.w("DatabaseFactory", "Update MMS bodies..."); + MasterCipher masterCipher = new MasterCipher(masterSecret); + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + + db.beginTransaction(); + + Cursor mmsCursor = db.query("mms", new String[] {"_id"}, + "msg_box & " + 0x80000000L + " != 0", + null, null, null, null); + + Log.w("DatabaseFactory", "Got MMS rows: " + (mmsCursor == null ? "null" : mmsCursor.getCount())); + + while (mmsCursor != null && mmsCursor.moveToNext()) { + listener.setProgress(mmsCursor.getPosition(), mmsCursor.getCount()); + + long mmsId = mmsCursor.getLong(mmsCursor.getColumnIndexOrThrow("_id")); + String body = null; + int partCount = 0; + Cursor partCursor = db.query("part", new String[] {"_id", "ct", "_data", "encrypted"}, + "mid = ?", new String[] {mmsId+""}, null, null, null); + + while (partCursor != null && partCursor.moveToNext()) { + String contentType = partCursor.getString(partCursor.getColumnIndexOrThrow("ct")); + + if (ContentType.isTextType(contentType)) { + try { + long partId = partCursor.getLong(partCursor.getColumnIndexOrThrow("_id")); + String dataLocation = partCursor.getString(partCursor.getColumnIndexOrThrow("_data")); + boolean encrypted = partCursor.getInt(partCursor.getColumnIndexOrThrow("encrypted")) == 1; + File dataFile = new File(dataLocation); + + FileInputStream fin; + + if (encrypted) fin = new DecryptingPartInputStream(dataFile, masterSecret); + else fin = new FileInputStream(dataFile); + + body = (body == null) ? Util.readFully(fin) : body + " " + Util.readFully(fin); + + Log.w("DatabaseFactory", "Read body: " + body); + + dataFile.delete(); + db.delete("part", "_id = ?", new String[] {partId+""}); + } catch (IOException e) { + Log.w("DatabaseFactory", e); + } + } else if (ContentType.isAudioType(contentType) || + ContentType.isImageType(contentType) || + ContentType.isVideoType(contentType)) + { + partCount++; + } + } + + if (!Util.isEmpty(body)) { + body = masterCipher.encryptBody(body); + db.execSQL("UPDATE mms SET body = ?, part_count = ? WHERE _id = ?", + new String[] {body, partCount+"", mmsId+""}); + } else { + db.execSQL("UPDATE mms SET part_count = ? WHERE _id = ?", + new String[] {partCount+"", mmsId+""}); + } + + Log.w("DatabaseFactory", "Updated body: " + body + " and part_count: " + partCount); + } + db.setTransactionSuccessful(); db.endTransaction(); } @@ -328,17 +452,17 @@ public class DatabaseFactory { // MMS Updates - db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {20L+"", 1+""}); - db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {23L+"", 2+""}); - db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {21L+"", 4+""}); - db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {24L+"", 12+""}); + db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x80000000L)+"", 1+""}); + db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(23L | 0x80000000L)+"", 2+""}); + db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(21L | 0x80000000L)+"", 4+""}); + db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(24L | 0x80000000L)+"", 12+""}); - db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(21L | 0x800000L) +"", 5+""}); - db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(23L | 0x800000L) +"", 6+""}); - db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x800000L | 0x20000000L) +"", 7+""}); - db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x800000L) +"", 8+""}); - db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x800000L | 0x08000000L) +"", 9+""}); - db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x800000L | 0x10000000L) +"", 10+""}); + db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(21L | 0x80000000L | 0x800000L) +"", 5+""}); + db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(23L | 0x80000000L | 0x800000L) +"", 6+""}); + db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x20000000L | 0x800000L) +"", 7+""}); + db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x80000000L | 0x800000L) +"", 8+""}); + db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x08000000L | 0x800000L) +"", 9+""}); + db.execSQL("UPDATE mms SET msg_box = ? WHERE msg_box = ?", new String[] {(20L | 0x10000000L | 0x800000L) +"", 10+""}); // Thread Updates @@ -367,6 +491,11 @@ public class DatabaseFactory { db.setTransactionSuccessful(); db.endTransaction(); } + + if (oldVersion < INTRODUCED_MMS_BODY_VERSION) { + db.execSQL("ALTER TABLE mms ADD COLUMN body TEXT"); + db.execSQL("ALTER TABLE mms ADD COLUMN part_count INTEGER"); + } } private void updateSmsBodyAndType(SQLiteDatabase db, Cursor cursor, String prefix, long typeMask) diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index e5da57317b..2ba11798e7 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -23,24 +23,31 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.util.Log; +import android.util.Pair; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.contacts.ContactPhotoFactory; +import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; +import org.thoughtcrime.securesms.mms.PartParser; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.InvalidMessageException; +import org.thoughtcrime.securesms.util.ListenableFutureTask; import org.thoughtcrime.securesms.util.Trimmer; import org.thoughtcrime.securesms.util.Util; import java.io.UnsupportedEncodingException; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; import ws.com.google.android.mms.InvalidHeaderValueException; import ws.com.google.android.mms.MmsException; @@ -88,11 +95,13 @@ public class MmsDatabase extends Database implements MmsSmsColumns { private static final String RESPONSE_TEXT = "resp_txt"; private static final String DELIVERY_TIME = "d_tm"; private static final String DELIVERY_REPORT = "d_rpt"; + static final String PART_COUNT = "part_count"; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " + THREAD_ID + " INTEGER, " + DATE_SENT + " INTEGER, " + DATE_RECEIVED + " INTEGER, " + MESSAGE_BOX + " INTEGER, " + READ + " INTEGER DEFAULT 0, " + MESSAGE_ID + " TEXT, " + SUBJECT + " TEXT, " + - SUBJECT_CHARSET + " INTEGER, " + CONTENT_TYPE + " TEXT, " + CONTENT_LOCATION + " TEXT, " + + SUBJECT_CHARSET + " INTEGER, " + BODY + " TEXT, " + PART_COUNT + " INTEGER, " + + CONTENT_TYPE + " TEXT, " + CONTENT_LOCATION + " TEXT, " + EXPIRY + " INTEGER, " + MESSAGE_CLASS + " TEXT, " + MESSAGE_TYPE + " INTEGER, " + MMS_VERSION + " INTEGER, " + MESSAGE_SIZE + " INTEGER, " + PRIORITY + " INTEGER, " + READ_REPORT + " INTEGER, " + REPORT_ALLOWED + " INTEGER, " + RESPONSE_STATUS + " INTEGER, " + @@ -115,9 +124,11 @@ public class MmsDatabase extends Database implements MmsSmsColumns { CONTENT_LOCATION, EXPIRY, MESSAGE_CLASS, MESSAGE_TYPE, MMS_VERSION, MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS, RETRIEVE_TEXT, RETRIEVE_TEXT_CS, READ_STATUS, CONTENT_CLASS, RESPONSE_TEXT, - DELIVERY_TIME, DELIVERY_REPORT + DELIVERY_TIME, DELIVERY_REPORT, BODY, PART_COUNT }; + public static final ExecutorService slideResolver = Util.newSingleThreadedLifoExecutor(); + public MmsDatabase(Context context, SQLiteOpenHelper databaseHelper) { super(context, databaseHelper); } @@ -288,7 +299,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns { return new NotificationInd(headers); } - public MultimediaMessagePdu getMediaMessage(long messageId) + private MultimediaMessagePdu getMediaMessage(long messageId) throws MmsException { PduHeaders headers = getHeadersForId(messageId); @@ -340,8 +351,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns { } } - private long insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved, - String contentLocation, long threadId, long mailbox) + private Pair insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved, + String contentLocation, long threadId, long mailbox) throws MmsException { PduHeaders headers = retrieved.getPduHeaders(); @@ -364,34 +375,42 @@ public class MmsDatabase extends Database implements MmsSmsColumns { if (!contentValues.containsKey(DATE_SENT)) contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); - return insertMediaMessage(masterSecret, retrieved, contentValues); + long messageId = insertMediaMessage(masterSecret, retrieved, contentValues); + + notifyConversationListeners(threadId); + DatabaseFactory.getThreadDatabase(context).update(threadId); + DatabaseFactory.getThreadDatabase(context).setUnread(threadId); + Trimmer.trimThread(context, threadId); + + return new Pair(threadId, messageId); } - public long insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved, - String contentLocation, long threadId) + public Pair insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved, + String contentLocation, long threadId) throws MmsException { return insertMessageInbox(masterSecret, retrieved, contentLocation, threadId, Types.BASE_INBOX_TYPE | Types.ENCRYPTION_SYMMETRIC_BIT); } - public long insertSecureMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved, - String contentLocation, long threadId) + public Pair insertSecureMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved, + String contentLocation, long threadId) throws MmsException { return insertMessageInbox(masterSecret, retrieved, contentLocation, threadId, Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_REMOTE_BIT); } - public long insertSecureDecryptedMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved, - long threadId) + public Pair insertSecureDecryptedMessageInbox(MasterSecret masterSecret, + RetrieveConf retrieved, + long threadId) throws MmsException { return insertMessageInbox(masterSecret, retrieved, "", threadId, Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_SYMMETRIC_BIT); } - public long insertMessageInbox(NotificationInd notification) { + public Pair insertMessageInbox(NotificationInd notification) { try { SQLiteDatabase db = databaseHelper.getWritableDatabase(); PduHeaders headers = notification.getPduHeaders(); @@ -412,23 +431,30 @@ public class MmsDatabase extends Database implements MmsSmsColumns { long messageId = db.insert(TABLE_NAME, null, contentValues); addressDatabase.insertAddressesForId(messageId, headers); - notifyConversationListeners(threadId); - DatabaseFactory.getThreadDatabase(context).update(threadId); - DatabaseFactory.getThreadDatabase(context).setUnread(threadId); - Trimmer.trimThread(context, threadId); +// notifyConversationListeners(threadId); +// DatabaseFactory.getThreadDatabase(context).update(threadId); +// DatabaseFactory.getThreadDatabase(context).setUnread(threadId); +// Trimmer.trimThread(context, threadId); - return messageId; + return new Pair(messageId, threadId); } catch (RecipientFormattingException rfe) { Log.w("MmsDatabase", rfe); - return -1; + return new Pair(-1L, -1L); } } + public void markIncomingNotificationReceived(long threadId) { + notifyConversationListeners(threadId); + DatabaseFactory.getThreadDatabase(context).update(threadId); + DatabaseFactory.getThreadDatabase(context).setUnread(threadId); + Trimmer.trimThread(context, threadId); + } + public long insertMessageOutbox(MasterSecret masterSecret, SendReq sendRequest, long threadId, boolean isSecure) throws MmsException { - long type = Types.BASE_OUTBOX_TYPE; + long type = Types.BASE_OUTBOX_TYPE | Types.ENCRYPTION_SYMMETRIC_BIT; PduHeaders headers = sendRequest.getPduHeaders(); ContentValues contentValues = getContentValuesFromHeader(headers); @@ -453,10 +479,22 @@ public class MmsDatabase extends Database implements MmsSmsColumns { throws MmsException { SQLiteDatabase db = databaseHelper.getWritableDatabase(); - long messageId = db.insert(TABLE_NAME, null, contentValues); - PduBody body = message.getBody(); PartDatabase partsDatabase = getPartDatabase(masterSecret); MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context); + PduBody body = message.getBody(); + + if (Types.isSymmetricEncryption(contentValues.getAsLong(MESSAGE_BOX))) { + String messageText = PartParser.getMessageText(body); + body = PartParser.getNonTextParts(body); + + if (!Util.isEmpty(messageText)) { + contentValues.put(BODY, new MasterCipher(masterSecret).encryptBody(messageText)); + } + } + + contentValues.put(PART_COUNT, body.getPartsNum()); + + long messageId = db.insert(TABLE_NAME, null, contentValues); addressDatabase.insertAddressesForId(messageId, message.getPduHeaders()); partsDatabase.insertParts(messageId, body); @@ -480,7 +518,6 @@ public class MmsDatabase extends Database implements MmsSmsColumns { notifyConversationListeners(threadId); } - public void deleteThread(long threadId) { Set singleThreadSet = new HashSet(); singleThreadSet.add(threadId); @@ -692,12 +729,16 @@ public class MmsDatabase extends Database implements MmsSmsColumns { public class Reader { - private final Cursor cursor; + private final Cursor cursor; private final MasterSecret masterSecret; + private final MasterCipher masterCipher; public Reader(MasterSecret masterSecret, Cursor cursor) { this.cursor = cursor; this.masterSecret = masterSecret; + + if (masterSecret != null) masterCipher = new MasterCipher(masterSecret); + else masterCipher = null; } public MessageRecord getNext() { @@ -723,28 +764,57 @@ public class MmsDatabase extends Database implements MmsSmsColumns { long dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_RECEIVED)); long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)); long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID)); + String body = getBody(cursor); + int partCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.PART_COUNT)); Recipient recipient = getMessageRecipient(id); - SlideDeck slideDeck; - - try { - MultimediaMessagePdu pdu = getMediaMessage(id); - slideDeck = getSlideDeck(masterSecret, pdu); - } catch (MmsException me) { - Log.w("ConversationAdapter", me); - slideDeck = null; - } + ListenableFutureTask slideDeck = getSlideDeck(masterSecret, id); return new MediaMmsMessageRecord(context, id, new Recipients(recipient), recipient, - dateSent, dateReceived, threadId, - slideDeck, box); + dateSent, dateReceived, threadId, body, + slideDeck, partCount, box); } - protected SlideDeck getSlideDeck(MasterSecret masterSecret, MultimediaMessagePdu pdu) { - if (masterSecret == null) - return null; + private String getBody(Cursor cursor) { + try { + String body = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.BODY)); + long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)); - return new SlideDeck(context, masterSecret, pdu.getBody()); + if (body != null && masterCipher != null && Types.isSymmetricEncryption(box)) { + return masterCipher.decryptBody(body); + } + + return body; + } catch (InvalidMessageException e) { + Log.w("MmsDatabase", e); + return "Error decrypting message."; + } + } + + private ListenableFutureTask getSlideDeck(final MasterSecret masterSecret, + final long id) + { + Callable task = new Callable() { + @Override + public SlideDeck call() throws Exception { + try { + if (masterSecret == null) + return null; + + MultimediaMessagePdu pdu = getMediaMessage(id); + + return new SlideDeck(context, masterSecret, pdu.getBody()); + } catch (MmsException me) { + Log.w("MmsDatabase", me); + return null; + } + } + }; + + ListenableFutureTask future = new ListenableFutureTask(task, null); + slideResolver.execute(future); + + return future; } private NotificationMmsMessageRecord getNotificationMmsMessageRecord(Cursor cursor) { diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 66e25246d4..94d7214a20 100644 --- a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -7,6 +7,7 @@ public interface MmsSmsColumns { public static final String NORMALIZED_DATE_RECEIVED = "date_received"; public static final String THREAD_ID = "thread_id"; public static final String READ = "read"; + public static final String BODY = "body"; public static class Types { protected static final long TOTAL_MASK = 0xFFFFFFFF; diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index dcea692577..92ec5d6332 100644 --- a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -94,7 +94,7 @@ public class MmsSmsDatabase extends Database { MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, - SmsDatabase.STATUS, TRANSPORT}; + SmsDatabase.STATUS, MmsDatabase.PART_COUNT, TRANSPORT}; String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC"; @@ -108,11 +108,12 @@ public class MmsSmsDatabase extends Database { public Cursor getConversationSnippet(long threadId) { String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE, + MmsSmsColumns.THREAD_ID, SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, - TRANSPORT}; + SmsDatabase.STATUS, MmsDatabase.PART_COUNT, TRANSPORT}; String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; @@ -126,7 +127,7 @@ public class MmsSmsDatabase extends Database { MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, - TRANSPORT}; + MmsDatabase.PART_COUNT, TRANSPORT}; String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC"; String selection = MmsSmsColumns.READ + " = 0"; @@ -145,13 +146,13 @@ public class MmsSmsDatabase extends Database { MmsDatabase.DATE_RECEIVED + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, - MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, TRANSPORT}; + MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT, TRANSPORT}; String[] smsProjection = {SmsDatabase.DATE_SENT + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, SmsDatabase.DATE_RECEIVED + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, - MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, TRANSPORT}; + MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT, TRANSPORT}; SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder(); @@ -171,10 +172,12 @@ public class MmsSmsDatabase extends Database { mmsColumnsPresent.add(MmsDatabase.DATE_RECEIVED); mmsColumnsPresent.add(MmsSmsColumns.READ); mmsColumnsPresent.add(MmsSmsColumns.THREAD_ID); + mmsColumnsPresent.add(MmsSmsColumns.BODY); + mmsColumnsPresent.add(MmsDatabase.PART_COUNT); Set smsColumnsPresent = new HashSet(); smsColumnsPresent.add(MmsSmsColumns.ID); - smsColumnsPresent.add(SmsDatabase.BODY); + smsColumnsPresent.add(MmsSmsColumns.BODY); smsColumnsPresent.add(SmsDatabase.TYPE); smsColumnsPresent.add(SmsDatabase.ADDRESS); smsColumnsPresent.add(SmsDatabase.SUBJECT); @@ -242,5 +245,9 @@ public class MmsSmsDatabase extends Database { return smsReader.getCurrent(); } } + + public void close() { + cursor.close(); + } } } diff --git a/src/org/thoughtcrime/securesms/database/PartDatabase.java b/src/org/thoughtcrime/securesms/database/PartDatabase.java index 048758aeff..9f981722ec 100644 --- a/src/org/thoughtcrime/securesms/database/PartDatabase.java +++ b/src/org/thoughtcrime/securesms/database/PartDatabase.java @@ -25,12 +25,7 @@ import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; import org.thoughtcrime.securesms.providers.PartProvider; - -import ws.com.google.android.mms.ContentType; -import ws.com.google.android.mms.MmsException; -import ws.com.google.android.mms.pdu.CharacterSets; -import ws.com.google.android.mms.pdu.PduBody; -import ws.com.google.android.mms.pdu.PduPart; +import org.thoughtcrime.securesms.util.Util; import java.io.ByteArrayOutputStream; import java.io.File; @@ -39,7 +34,11 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; + +import ws.com.google.android.mms.ContentType; +import ws.com.google.android.mms.MmsException; +import ws.com.google.android.mms.pdu.PduBody; +import ws.com.google.android.mms.pdu.PduPart; public class PartDatabase extends Database { @@ -83,32 +82,32 @@ public class PartDatabase extends Database { int contentTypeColumn = cursor.getColumnIndexOrThrow(CONTENT_TYPE); if (!cursor.isNull(contentTypeColumn)) - part.setContentType(getBytes(cursor.getString(contentTypeColumn))); + part.setContentType(Util.toIsoBytes(cursor.getString(contentTypeColumn))); int nameColumn = cursor.getColumnIndexOrThrow(NAME); if (!cursor.isNull(nameColumn)) - part.setName(getBytes(cursor.getString(nameColumn))); + part.setName(Util.toIsoBytes(cursor.getString(nameColumn))); int fileNameColumn = cursor.getColumnIndexOrThrow(FILENAME); if (!cursor.isNull(fileNameColumn)) - part.setFilename(getBytes(cursor.getString(fileNameColumn))); + part.setFilename(Util.toIsoBytes(cursor.getString(fileNameColumn))); int contentDispositionColumn = cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION); if (!cursor.isNull(contentDispositionColumn)) - part.setContentDisposition(getBytes(cursor.getString(contentDispositionColumn))); + part.setContentDisposition(Util.toIsoBytes(cursor.getString(contentDispositionColumn))); int contentIdColumn = cursor.getColumnIndexOrThrow(CONTENT_ID); if (!cursor.isNull(contentIdColumn)) - part.setContentId(getBytes(cursor.getString(contentIdColumn))); + part.setContentId(Util.toIsoBytes(cursor.getString(contentIdColumn))); int contentLocationColumn = cursor.getColumnIndexOrThrow(CONTENT_LOCATION); if (!cursor.isNull(contentLocationColumn)) - part.setContentLocation(getBytes(cursor.getString(contentLocationColumn))); + part.setContentLocation(Util.toIsoBytes(cursor.getString(contentLocationColumn))); int encryptedColumn = cursor.getColumnIndexOrThrow(ENCRYPTED); @@ -125,9 +124,9 @@ public class PartDatabase extends Database { } if (part.getContentType() != null) { - contentValues.put(CONTENT_TYPE, toIsoString(part.getContentType())); + contentValues.put(CONTENT_TYPE, Util.toIsoString(part.getContentType())); - if (toIsoString(part.getContentType()).equals(ContentType.APP_SMIL)) + if (Util.toIsoString(part.getContentType()).equals(ContentType.APP_SMIL)) contentValues.put(SEQUENCE, -1); } else { throw new MmsException("There is no content type for this part."); @@ -142,15 +141,15 @@ public class PartDatabase extends Database { } if (part.getContentDisposition() != null) { - contentValues.put(CONTENT_DISPOSITION, toIsoString(part.getContentDisposition())); + contentValues.put(CONTENT_DISPOSITION, Util.toIsoString(part.getContentDisposition())); } if (part.getContentId() != null) { - contentValues.put(CONTENT_ID, toIsoString(part.getContentId())); + contentValues.put(CONTENT_ID, Util.toIsoString(part.getContentId())); } if (part.getContentLocation() != null) { - contentValues.put(CONTENT_LOCATION, toIsoString(part.getContentLocation())); + contentValues.put(CONTENT_LOCATION, Util.toIsoString(part.getContentLocation())); } contentValues.put(ENCRYPTED, part.getEncrypted() ? 1 : 0); @@ -267,7 +266,7 @@ public class PartDatabase extends Database { } } - public void insertParts(long mmsId, PduBody body) throws MmsException { + void insertParts(long mmsId, PduBody body) throws MmsException { for (int i=0;i slideDeck; public MediaMmsMessageRecord(Context context, long id, Recipients recipients, Recipient individualRecipient, long dateSent, long dateReceived, - long threadId, SlideDeck slideDeck, long mailbox) + long threadId, String body, ListenableFutureTask slideDeck, + int partCount, long mailbox) { - super(context, id, getBodyFromSlidesIfAvailable(slideDeck), recipients, - individualRecipient, dateSent, dateReceived, + super(context, id, body, recipients, individualRecipient, dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, mailbox); this.context = context.getApplicationContext(); + this.partCount = partCount; this.slideDeck = slideDeck; } - public SlideDeck getSlideDeck() { + public ListenableFutureTask getSlideDeck() { return slideDeck; } + public int getPartCount() { + return partCount; + } + @Override public boolean isMms() { return true; @@ -73,16 +80,16 @@ public class MediaMmsMessageRecord extends MessageRecord { return super.getDisplayBody(); } - private static String getBodyFromSlidesIfAvailable(SlideDeck slideDeck) { - if (slideDeck == null) - return ""; - - for (Slide slide : slideDeck.getSlides()) { - if (slide.hasText()) - return slide.getText(); - } - - return ""; - } +// private static String getBodyFromSlidesIfAvailable(SlideDeck slideDeck) { +// if (slideDeck == null) +// return ""; +// +// for (Slide slide : slideDeck.getSlides()) { +// if (slide.hasText()) +// return slide.getText(); +// } +// +// return ""; +// } } diff --git a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java index 3970518a9b..87f2f8dbb2 100644 --- a/src/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -107,6 +107,10 @@ public abstract class MessageRecord extends DisplayRecord { return individualRecipient; } + public long getType() { + return type; + } + protected SpannableString emphasisAdded(String sequence) { SpannableString spannable = new SpannableString(sequence); spannable.setSpan(new ForegroundColorSpan(context.getResources().getColor(android.R.color.darker_gray)), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index a5ff172464..8d9ff54c95 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -54,21 +54,21 @@ public class AttachmentManager { public void setImage(Uri image) throws IOException { ImageSlide slide = new ImageSlide(context, image); slideDeck.addSlide(slide); - thumbnail.setImageBitmap(slide.getThumbnail()); + thumbnail.setImageBitmap(slide.getThumbnail(345, 261)); attachmentView.setVisibility(View.VISIBLE); } public void setVideo(Uri video) throws IOException, MediaTooLargeException { VideoSlide slide = new VideoSlide(context, video); slideDeck.addSlide(slide); - thumbnail.setImageBitmap(slide.getThumbnail()); + thumbnail.setImageBitmap(slide.getThumbnail(thumbnail.getWidth(), thumbnail.getHeight())); attachmentView.setVisibility(View.VISIBLE); } public void setAudio(Uri audio)throws IOException, MediaTooLargeException { AudioSlide slide = new AudioSlide(context, audio); slideDeck.addSlide(slide); - thumbnail.setImageBitmap(slide.getThumbnail()); + thumbnail.setImageBitmap(slide.getThumbnail(thumbnail.getWidth(), thumbnail.getHeight())); attachmentView.setVisibility(View.VISIBLE); } diff --git a/src/org/thoughtcrime/securesms/mms/AudioSlide.java b/src/org/thoughtcrime/securesms/mms/AudioSlide.java index 7f576627bc..aa62e527db 100644 --- a/src/org/thoughtcrime/securesms/mms/AudioSlide.java +++ b/src/org/thoughtcrime/securesms/mms/AudioSlide.java @@ -27,6 +27,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.provider.MediaStore.Audio; +import android.widget.ImageView; public class AudioSlide extends Slide { @@ -49,10 +50,10 @@ public class AudioSlide extends Slide { } @Override - public Bitmap getThumbnail() { + public Bitmap getThumbnail(int maxWidth, int maxHeight) { return BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_menu_add_sound); } - + public static PduPart constructPartFromUri(Context context, Uri uri) throws IOException, MediaTooLargeException { PduPart part = new PduPart(); diff --git a/src/org/thoughtcrime/securesms/mms/ImageSlide.java b/src/org/thoughtcrime/securesms/mms/ImageSlide.java index 23424ddd0c..620706497c 100644 --- a/src/org/thoughtcrime/securesms/mms/ImageSlide.java +++ b/src/org/thoughtcrime/securesms/mms/ImageSlide.java @@ -16,25 +16,33 @@ */ package org.thoughtcrime.securesms.mms; -import java.io.ByteArrayOutputStream; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.net.Uri; +import android.os.Handler; +import android.util.Log; +import android.widget.ImageView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.util.BitmapUtil; + import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; import java.util.LinkedHashMap; -import java.util.Map.Entry; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.crypto.MasterSecret; import ws.com.google.android.mms.ContentType; import ws.com.google.android.mms.pdu.PduPart; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Bitmap.CompressFormat; -import android.net.Uri; -import android.util.Log; public class ImageSlide extends Slide { @@ -56,67 +64,98 @@ public class ImageSlide extends Slide { } @Override - public Bitmap getThumbnail() { - if (thumbnailCache.containsKey(part.getDataUri())) { - Log.w("ImageSlide", "Cached thumbnail..."); - Bitmap bitmap = thumbnailCache.get(part.getDataUri()).get(); - if (bitmap != null) return bitmap; - else thumbnailCache.remove(part.getDataUri()); - } + public Bitmap getThumbnail(int maxWidth, int maxHeight) { + Bitmap thumbnail = getCachedThumbnail(); + + if (thumbnail != null) + return thumbnail; try { - BitmapFactory.Options options = getImageDimensions(getPartDataInputStream()); - int imageWidth = options.outWidth; - int imageHeight = options.outHeight; - - int scaler = 1; - while ((imageWidth / scaler > 480) || (imageHeight / scaler > 480)) - scaler *= 2; - - options.inSampleSize = scaler; - options.inJustDecodeBounds = false; - - Bitmap thumbnail = BitmapFactory.decodeStream(getPartDataInputStream(), null, options); + InputStream measureStream = getPartDataInputStream(); + InputStream dataStream = getPartDataInputStream(); + + thumbnail = BitmapUtil.createScaledBitmap(measureStream, dataStream, maxWidth, maxHeight); thumbnailCache.put(part.getDataUri(), new SoftReference(thumbnail)); return thumbnail; - } catch (FileNotFoundException fnfe) { - Log.w("ImageSlide", fnfe); + } catch (FileNotFoundException e) { + Log.w("ImageSlide", e); return BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_missing_thumbnail_picture); } } - - private static BitmapFactory.Options getImageDimensions(InputStream inputStream) throws FileNotFoundException { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(inputStream, null, options); - return options; - } - - private static BitmapFactory.Options getImageDimensions(Context context, Uri uri) throws FileNotFoundException { - InputStream in = context.getContentResolver().openInputStream(uri); - return getImageDimensions(in); - } - @Override - public boolean hasImage() { + public void setThumbnailOn(ImageView imageView) { + Bitmap thumbnail = getCachedThumbnail(); + + if (thumbnail != null) { + Log.w("ImageSlide", "Setting cached thumbnail..."); + setThumbnailOn(imageView, thumbnail, true); + return; + } + + final ColorDrawable temporaryDrawable = new ColorDrawable(Color.TRANSPARENT); + final WeakReference weakImageView = new WeakReference(imageView); + final Handler handler = new Handler(); + final int maxWidth = imageView.getWidth(); + final int maxHeight = imageView.getHeight(); + + imageView.setImageDrawable(temporaryDrawable); + + MmsDatabase.slideResolver.execute(new Runnable() { + @Override + public void run() { + final Bitmap bitmap = getThumbnail(maxWidth, maxHeight); + final ImageView destination = weakImageView.get(); + if (destination != null && destination.getDrawable() == temporaryDrawable) { + handler.post(new Runnable() { + @Override + public void run() { + setThumbnailOn(destination, bitmap, false); + } + }); + } + } + }); + } + + private void setThumbnailOn(ImageView imageView, Bitmap thumbnail, boolean fromMemory) { + if (fromMemory) { + imageView.setImageBitmap(thumbnail); + } else { + BitmapDrawable result = new BitmapDrawable(context.getResources(), thumbnail); + TransitionDrawable fadingResult = new TransitionDrawable(new Drawable[]{new ColorDrawable(Color.TRANSPARENT), result}); + imageView.setImageDrawable(fadingResult); + fadingResult.startTransition(300); + } + } + + private Bitmap getCachedThumbnail() { + synchronized (thumbnailCache) { + SoftReference bitmapReference = thumbnailCache.get(part.getDataUri()); + Log.w("ImageSlide", "Got soft reference: " + bitmapReference); + + if (bitmapReference != null) { + Bitmap bitmap = bitmapReference.get(); + Log.w("ImageSlide", "Got cached bitmap: " + bitmap); + if (bitmap != null) return bitmap; + else thumbnailCache.remove(part.getDataUri()); + } + } + + return null; + } + + @Override + public boolean hasImage() { return true; } private static PduPart constructPartFromUri(Context context, Uri uri) throws IOException { PduPart part = new PduPart(); - - BitmapFactory.Options options = getImageDimensions(context, uri); - long size = getMediaSize(context, uri); - - if (options.outWidth > 640 || options.outHeight > 480 || size > (1024*1024)) { - byte[] data = scaleImage(context, uri, options, size, 640, 480, 1024*1024); - part.setData(data); - Log.w("ImageSlide", "Setting actual part data..."); - } - - Log.w("ImageSlide", "Setting part data URI.."); + byte[] data = BitmapUtil.createScaledBytes(context, uri, 640, 480, (300 * 1024) - 5000); + + part.setData(data); part.setDataUri(uri); part.setContentType(ContentType.IMAGE_JPEG.getBytes()); part.setContentId((System.currentTimeMillis()+"").getBytes()); @@ -124,25 +163,4 @@ public class ImageSlide extends Slide { return part; } - - private static byte[] scaleImage(Context context, Uri uri, BitmapFactory.Options options, long size, int maxWidth, int maxHeight, int maxSize) throws FileNotFoundException { - int scaler = 1; - while ((options.outWidth / scaler > maxWidth) || (options.outHeight / scaler > maxHeight)) - scaler *= 2; - - options.inSampleSize = scaler; - options.inJustDecodeBounds = false; - - Bitmap bitmap = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri), null, options); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - int quality = 80; - - do { - bitmap.compress(CompressFormat.JPEG, quality, baos); - if (baos.size() > maxSize) - quality = quality * maxSize / baos.size(); - } while (baos.size() > maxSize); - - return baos.toByteArray(); - } } diff --git a/src/org/thoughtcrime/securesms/mms/PartParser.java b/src/org/thoughtcrime/securesms/mms/PartParser.java new file mode 100644 index 0000000000..fad3bea7a1 --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/PartParser.java @@ -0,0 +1,47 @@ +package org.thoughtcrime.securesms.mms; + +import android.util.Log; + +import org.thoughtcrime.securesms.util.Util; + +import java.io.UnsupportedEncodingException; + +import ws.com.google.android.mms.ContentType; +import ws.com.google.android.mms.pdu.CharacterSets; +import ws.com.google.android.mms.pdu.PduBody; + +public class PartParser { + public static String getMessageText(PduBody body) { + String bodyText = null; + + for (int i=0;i messageAndThreadId; + if (retrieved.getSubject() != null && WirePrefix.isEncryptedMmsSubject(retrieved.getSubject().getString())) { - long messageId = mmsDatabase.insertSecureMessageInbox(item.getMasterSecret(), retrieved, - item.getContentLocation(), - item.getThreadId()); + messageAndThreadId = mmsDatabase.insertSecureMessageInbox(item.getMasterSecret(), retrieved, + item.getContentLocation(), + item.getThreadId()); if (item.getMasterSecret() != null) - DecryptingQueue.scheduleDecryption(context, item.getMasterSecret(), messageId, item.getThreadId(), retrieved); + DecryptingQueue.scheduleDecryption(context, item.getMasterSecret(), messageAndThreadId.first, + messageAndThreadId.second, retrieved); } else { - mmsDatabase.insertMessageInbox(item.getMasterSecret(), retrieved, item.getContentLocation(), - item.getThreadId()); + messageAndThreadId = mmsDatabase.insertMessageInbox(item.getMasterSecret(), retrieved, + item.getContentLocation(), + item.getThreadId()); } mmsDatabase.delete(item.getMessageId()); + MessageNotifier.updateNotification(context, item.getMasterSecret(), messageAndThreadId.second); } protected void handleConnectivityChange() { @@ -153,18 +162,38 @@ public class MmsDownloader extends MmscProcessor { } else if (!isConnected() && !isConnectivityPossible()) { pendingMessages.clear(); - - for (DownloadItem item : downloadItems) { - DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY); - } - - toastHandler.makeToast(context - .getString(R.string.MmsDownloader_no_connectivity_available_for_mms_download_try_again_later)); - + handleDownloadError(downloadItems, MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY, + context.getString(R.string.MmsDownloader_no_connectivity_available_for_mms_download_try_again_later)); finishConnectivity(); } } + private void handleDownloadError(List items, int downloadStatus, String error) { + MmsDatabase db = DatabaseFactory.getMmsDatabase(context); + + for (DownloadItem item : items) { + db.markDownloadState(item.getMessageId(), downloadStatus); + + if (item.isAutomatic()) { + db.markIncomingNotificationReceived(item.getThreadId()); + MessageNotifier.updateNotification(context, item.getMasterSecret(), item.getThreadId()); + } + } + + toastHandler.makeToast(error); + } + + private void handleDownloadError(DownloadItem item, int downloadStatus, String error) { + MmsDatabase db = DatabaseFactory.getMmsDatabase(context); + db.markDownloadState(item.getMessageId(), downloadStatus); + + if (item.isAutomatic()) { + db.markIncomingNotificationReceived(item.getThreadId()); + MessageNotifier.updateNotification(context, item.getMasterSecret(), item.getThreadId()); + } + + toastHandler.makeToast(error); + } private void scheduleDownloadWithRadioMode(DownloadItem item) { item.mmsRadioMode = true; @@ -186,9 +215,11 @@ public class MmsDownloader extends MmscProcessor { private long messageId; private byte[] transactionId; private String contentLocation; + private boolean automatic; public DownloadItem(MasterSecret masterSecret, boolean mmsRadioMode, boolean proxyIfPossible, - long messageId, long threadId, String contentLocation, byte[] transactionId) + long messageId, long threadId, boolean automatic, String contentLocation, + byte[] transactionId) { this.masterSecret = masterSecret; this.mmsRadioMode = mmsRadioMode; @@ -197,6 +228,7 @@ public class MmsDownloader extends MmscProcessor { this.messageId = messageId; this.contentLocation = contentLocation; this.transactionId = transactionId; + this.automatic = automatic; } public long getThreadId() { @@ -226,6 +258,10 @@ public class MmsDownloader extends MmscProcessor { public boolean useMmsRadioMode() { return mmsRadioMode; } + + public boolean isAutomatic() { + return automatic; + } } @Override diff --git a/src/org/thoughtcrime/securesms/service/MmsReceiver.java b/src/org/thoughtcrime/securesms/service/MmsReceiver.java index 367c0a2dd3..907e88ccc8 100644 --- a/src/org/thoughtcrime/securesms/service/MmsReceiver.java +++ b/src/org/thoughtcrime/securesms/service/MmsReceiver.java @@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.service; import android.content.Context; import android.content.Intent; import android.util.Log; +import android.util.Pair; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -44,6 +45,7 @@ public class MmsReceiver { intent.putExtra("message_id", messageId); intent.putExtra("transaction_id", pdu.getTransactionId()); intent.putExtra("thread_id", threadId); + intent.putExtra("automatic", true); context.startService(intent); } @@ -54,12 +56,12 @@ public class MmsReceiver { GenericPdu pdu = parser.parse(); if (pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) { - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - long messageId = database.insertMessageInbox((NotificationInd)pdu); - long threadId = database.getThreadIdForMessage(messageId); + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + Pair messageAndThreadId = database.insertMessageInbox((NotificationInd)pdu); +// long threadId = database.getThreadIdForMessage(messageId); - MessageNotifier.updateNotification(context, masterSecret, threadId); - scheduleDownload((NotificationInd)pdu, messageId, threadId); +// MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second); + scheduleDownload((NotificationInd)pdu, messageAndThreadId.first, messageAndThreadId.second); Log.w("MmsReceiverService", "Inserted received notification..."); } diff --git a/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/src/org/thoughtcrime/securesms/util/BitmapUtil.java new file mode 100644 index 0000000000..a007dbe1ad --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -0,0 +1,84 @@ +package org.thoughtcrime.securesms.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class BitmapUtil { + + private static final int MAX_COMPRESSION_QUALITY = 95; + private static final int MIN_COMPRESSION_QUALITY = 50; + private static final int MAX_COMPRESSION_ATTEMPTS = 4; + + public static byte[] createScaledBytes(Context context, Uri uri, int maxWidth, + int maxHeight, int maxSize) + throws IOException + { + InputStream measure = context.getContentResolver().openInputStream(uri); + InputStream data = context.getContentResolver().openInputStream(uri); + Bitmap bitmap = createScaledBitmap(measure, data, maxWidth, maxHeight); + int quality = MAX_COMPRESSION_QUALITY; + int attempts = 0; + + 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); + + bitmap.recycle(); + + if (baos.size() <= maxSize) return baos.toByteArray(); + else throw new IOException("Unable to scale image below: " + baos.size()); + } + + public static Bitmap createScaledBitmap(InputStream measure, InputStream data, + int maxWidth, int maxHeight) + { + BitmapFactory.Options options = getImageDimensions(measure); + int imageWidth = options.outWidth; + int imageHeight = options.outHeight; + + int scaler = 1; + + while ((imageWidth / scaler > maxWidth) && (imageHeight / scaler > maxHeight)) + scaler *= 2; + + if (scaler > 1) + scaler /= 2; + + options.inSampleSize = scaler; + options.inJustDecodeBounds = false; + + Bitmap roughThumbnail = BitmapFactory.decodeStream(data, null, options); + + if (imageWidth > maxWidth || imageHeight > maxHeight) { + Log.w("BitmapUtil", "Scaling to max width and height: " + maxWidth + "," + maxHeight); + Bitmap scaledThumbnail = Bitmap.createScaledBitmap(roughThumbnail, maxWidth, maxHeight, true); + roughThumbnail.recycle(); + return scaledThumbnail; + } else { + return roughThumbnail; + } + } + + private static BitmapFactory.Options getImageDimensions(InputStream inputStream) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(inputStream, null, options); + + return options; + } + + +} diff --git a/src/org/thoughtcrime/securesms/util/ListenableFutureTask.java b/src/org/thoughtcrime/securesms/util/ListenableFutureTask.java index 362421b126..89d2512f2b 100644 --- a/src/org/thoughtcrime/securesms/util/ListenableFutureTask.java +++ b/src/org/thoughtcrime/securesms/util/ListenableFutureTask.java @@ -1,20 +1,30 @@ package org.thoughtcrime.securesms.util; +import java.lang.ref.WeakReference; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class ListenableFutureTask extends FutureTask { +// private WeakReference> listener; private FutureTaskListener listener; public ListenableFutureTask(Callable callable, FutureTaskListener listener) { super(callable); this.listener = listener; +// if (listener == null) { +// this.listener = null; +// } else { +// this.listener = new WeakReference>(listener); +// } } public synchronized void setListener(FutureTaskListener listener) { +// if (listener != null) this.listener = new WeakReference>(listener); +// else this.listener = null; this.listener = listener; + if (this.isDone()) { callback(); } @@ -27,12 +37,16 @@ public class ListenableFutureTask extends FutureTask { private void callback() { if (this.listener != null) { - try { - this.listener.onSuccess(get()); - } catch (ExecutionException ee) { - this.listener.onFailure(ee); - } catch (InterruptedException e) { - throw new AssertionError(e); + FutureTaskListener nestedListener = this.listener; +// FutureTaskListener nestedListener = this.listener.get(); + if (nestedListener != null) { + try { + nestedListener.onSuccess(get()); + } catch (ExecutionException ee) { + nestedListener.onFailure(ee); + } catch (InterruptedException e) { + throw new AssertionError(e); + } } } } diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java index 895be207a4..d52c647480 100644 --- a/src/org/thoughtcrime/securesms/util/Util.java +++ b/src/org/thoughtcrime/securesms/util/Util.java @@ -22,17 +22,20 @@ import android.graphics.Typeface; import android.text.Spannable; import android.text.SpannableString; import android.text.style.StyleSpan; +import android.util.Log; import android.widget.EditText; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import ws.com.google.android.mms.pdu.CharacterSets; import ws.com.google.android.mms.pdu.EncodedStringValue; public class Util { @@ -129,6 +132,25 @@ public class Util { return spanned; } + public static String toIsoString(byte[] bytes) { + try { + return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1); + } catch (UnsupportedEncodingException e) { + // Impossible to reach here! + Log.e("MmsDatabase", "ISO_8859_1 must be supported!", e); + return ""; + } + } + + public static byte[] toIsoBytes(String isoString) { + try { + return isoString.getBytes(CharacterSets.MIMENAME_ISO_8859_1); + } catch (UnsupportedEncodingException e) { + Log.w("Util", "ISO_8859_1 must be supported!", e); + return new byte[0]; + } + } + public static void showAlertDialog(Context context, String title, String message) { AlertDialog.Builder dialog = new AlertDialog.Builder(context); dialog.setTitle(title);