Make MMS more asynchronous and consistent with new SMS types.

1) We now delay MMS notifications until a payload is received,
   or there's an error downloading the payload.  This makes
   group messages more consistent.

2) All "text" parts of an MMS are combined into a second text
   record, which is stored in the MMS row directly rather than
   as a distinct part.  This allows for immediate text loading,
   which means there's no chance a ConversationItem will resize.

   To do this, we need to include MMS in the big DB migration
   that's already staged for this application update.  It's also
   an "application-level" migration, because we need the MasterSecret
   to do it.

3) On conversation display, all image-based parts now have their
   thumbnails loaded asynchronously.  This allows for smooth-scrolling.
   The thumbnails are also scaled more accurately.
This commit is contained in:
Moxie Marlinspike 2013-04-26 11:23:43 -07:00
parent dd0aecc811
commit 7c47ea5cec
29 changed files with 747 additions and 288 deletions

View File

@ -48,11 +48,10 @@
<ImageView <ImageView
android:id="@+id/image_view" android:id="@+id/image_view"
android:layout_width="wrap_content" android:layout_width="230dip"
android:layout_height="wrap_content" android:layout_height="174dip"
android:layout_gravity="center" android:layout_gravity="center"
android:maxWidth="178dip" android:scaleType="centerInside"
android:maxHeight="178dip"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:background="@android:drawable/picture_frame" android:background="@android:drawable/picture_frame"
android:visibility="gone" /> android:visibility="gone" />

View File

@ -69,15 +69,14 @@
android:paddingBottom="7dip"> android:paddingBottom="7dip">
<ImageView <ImageView
android:id="@+id/image_view" android:id="@+id/image_view"
android:layout_width="wrap_content" android:layout_width="230dip"
android:layout_height="wrap_content" android:layout_height="174dip"
android:layout_gravity="center" android:layout_gravity="center"
android:maxWidth="178dip" android:scaleType="centerInside"
android:maxHeight="178dip" android:adjustViewBounds="true"
android:adjustViewBounds="true" android:background="@android:drawable/picture_frame"
android:background="@android:drawable/picture_frame" android:visibility="gone" />
android:visibility="gone" />
<ImageButton <ImageButton
android:id="@+id/play_slideshow_button" android:id="@+id/play_slideshow_button"

View File

@ -22,6 +22,7 @@ import android.os.Handler;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.CursorAdapter; import android.widget.CursorAdapter;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
@ -42,7 +43,7 @@ import java.util.LinkedHashMap;
* @author Moxie Marlinspike * @author Moxie Marlinspike
* *
*/ */
public class ConversationAdapter extends CursorAdapter { public class ConversationAdapter extends CursorAdapter implements AbsListView.RecyclerListener {
private static final int MAX_CACHE_SIZE = 40; private static final int MAX_CACHE_SIZE = 40;
@ -129,6 +130,11 @@ public class ConversationAdapter extends CursorAdapter {
this.getCursor().close(); this.getCursor().close();
} }
@Override
public void onMovedToScrapHeap(View view) {
((ConversationItem)view).unbind();
}
private LinkedHashMap<String,MessageRecord> initializeCache() { private LinkedHashMap<String,MessageRecord> initializeCache() {
return new LinkedHashMap<String,MessageRecord>() { return new LinkedHashMap<String,MessageRecord>() {
@Override @Override

View File

@ -169,6 +169,7 @@ public class ConversationFragment extends SherlockListFragment
if (this.recipients != null && this.threadId != -1) { if (this.recipients != null && this.threadId != -1) {
this.setListAdapter(new ConversationAdapter(recipients, threadId, getActivity(), this.setListAdapter(new ConversationAdapter(recipients, threadId, getActivity(),
masterSecret, new FailedIconClickHandler())); masterSecret, new FailedIconClickHandler()));
getListView().setRecyclerListener((ConversationAdapter)getListAdapter());
getLoaderManager().initLoader(0, null, this); getLoaderManager().initLoader(0, null, this);
} }
} }

View File

@ -21,6 +21,8 @@ import android.app.ProgressDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.media.MediaScannerConnection; import android.media.MediaScannerConnection;
import android.net.Uri; import android.net.Uri;
import android.os.Environment; 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.MmsDatabase;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord; 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.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.SendReceiveService; 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.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Iterator;
import java.util.List;
/** /**
* A view that displays an individual conversation item within a conversation * A view that displays an individual conversation item within a conversation
@ -85,10 +86,12 @@ public class ConversationItem extends LinearLayout {
private ImageView mmsThumbnail; private ImageView mmsThumbnail;
private Button mmsDownloadButton; private Button mmsDownloadButton;
private TextView mmsDownloadingLabel; private TextView mmsDownloadingLabel;
private ListenableFutureTask<SlideDeck> slideDeck;
private final FailedIconClickListener failedIconClickListener = new FailedIconClickListener(); private final FailedIconClickListener failedIconClickListener = new FailedIconClickListener();
private final MmsDownloadClickListener mmsDownloadClickListener = new MmsDownloadClickListener(); private final MmsDownloadClickListener mmsDownloadClickListener = new MmsDownloadClickListener();
private final ClickListener clickListener = new ClickListener(); private final ClickListener clickListener = new ClickListener();
private final Handler handler = new Handler();
private final Context context; private final Context context;
public ConversationItem(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() { public MessageRecord getMessageRecord() {
return messageRecord; return messageRecord;
} }
@ -231,26 +239,40 @@ public class ConversationItem extends LinearLayout {
} }
private void setMediaMmsAttributes(MediaMmsMessageRecord messageRecord) { private void setMediaMmsAttributes(MediaMmsMessageRecord messageRecord) {
SlideDeck slideDeck = messageRecord.getSlideDeck(); if (messageRecord.getPartCount() > 0) {
mmsThumbnail.setVisibility(View.VISIBLE);
if (slideDeck != null) { mmsThumbnail.setImageDrawable(new ColorDrawable(Color.TRANSPARENT));
List<Slide> slides = slideDeck.getSlides();
Iterator<Slide> 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;
}
}
} }
mmsThumbnail.setVisibility(View.GONE); slideDeck = messageRecord.getSlideDeck();
slideDeck.setListener(new FutureTaskListener<SlideDeck>() {
@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 /// Helper Methods

View File

@ -21,6 +21,7 @@ import java.util.TreeSet;
public class DatabaseUpgradeActivity extends Activity { public class DatabaseUpgradeActivity extends Activity {
public static final int NO_MORE_KEY_EXCHANGE_PREFIX_VERSION = 46; 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"; private static final String LAST_VERSION_CODE = "last_version_code";

View File

@ -24,10 +24,17 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.DatabaseUpgradeActivity; import org.thoughtcrime.securesms.DatabaseUpgradeActivity;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.util.InvalidMessageException; 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 { public class DatabaseFactory {
@ -36,7 +43,8 @@ public class DatabaseFactory {
private static final int INTRODUCED_DATE_SENT_VERSION = 4; private static final int INTRODUCED_DATE_SENT_VERSION = 4;
private static final int INTRODUCED_DRAFTS_VERSION = 5; private static final int INTRODUCED_DRAFTS_VERSION = 5;
private static final int INTRODUCED_NEW_TYPES_VERSION = 6; 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 String DATABASE_NAME = "messages.db";
private static final Object lock = new Object(); private static final Object lock = new Object();
@ -148,23 +156,31 @@ public class DatabaseFactory {
MasterCipher masterCipher = new MasterCipher(masterSecret); MasterCipher masterCipher = new MasterCipher(masterSecret);
int count = 0; int count = 0;
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
Cursor cursor = db.query("sms", Cursor smsCursor = db.query("sms",
new String[] {"_id", "type", "body"}, new String[] {"_id", "type", "body"},
"type & " + 0x80000000 + " != 0", "type & " + 0x80000000 + " != 0",
null, null, null, null); null, null, null, null);
if (cursor != null) Cursor threadCursor = db.query("thread",
count = cursor.getCount(); 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(); db.beginTransaction();
while (cursor != null && cursor.moveToNext()) { while (smsCursor != null && smsCursor.moveToNext()) {
listener.setProgress(cursor.getPosition(), count); listener.setProgress(smsCursor.getPosition(), count);
try { try {
String body = masterCipher.decryptBody(cursor.getString(cursor.getColumnIndexOrThrow("body"))); String body = masterCipher.decryptBody(smsCursor.getString(smsCursor.getColumnIndexOrThrow("body")));
long type = cursor.getLong(cursor.getColumnIndexOrThrow("type")); long type = smsCursor.getLong(smsCursor.getColumnIndexOrThrow("type"));
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); long id = smsCursor.getLong(smsCursor.getColumnIndexOrThrow("_id"));
if (body.startsWith(KEY_EXCHANGE)) { if (body.startsWith(KEY_EXCHANGE)) {
body = body.substring(KEY_EXCHANGE.length()); 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.setTransactionSuccessful();
db.endTransaction(); db.endTransaction();
} }
@ -328,17 +452,17 @@ public class DatabaseFactory {
// MMS Updates // 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[] {(20L | 0x80000000L)+"", 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[] {(23L | 0x80000000L)+"", 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[] {(21L | 0x80000000L)+"", 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[] {(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[] {(21L | 0x80000000L | 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[] {(23L | 0x80000000L | 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 | 0x20000000L | 0x800000L) +"", 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 | 0x80000000L | 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 | 0x08000000L | 0x800000L) +"", 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[] {(20L | 0x10000000L | 0x800000L) +"", 10+""});
// Thread Updates // Thread Updates
@ -367,6 +491,11 @@ public class DatabaseFactory {
db.setTransactionSuccessful(); db.setTransactionSuccessful();
db.endTransaction(); 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) private void updateSmsBodyAndType(SQLiteDatabase db, Cursor cursor, String prefix, long typeMask)

View File

@ -23,24 +23,31 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory; import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord; import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients; 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.Trimmer;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; 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.InvalidHeaderValueException;
import ws.com.google.android.mms.MmsException; 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 RESPONSE_TEXT = "resp_txt";
private static final String DELIVERY_TIME = "d_tm"; private static final String DELIVERY_TIME = "d_tm";
private static final String DELIVERY_REPORT = "d_rpt"; 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, " + 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, " + THREAD_ID + " INTEGER, " + DATE_SENT + " INTEGER, " + DATE_RECEIVED + " INTEGER, " + MESSAGE_BOX + " INTEGER, " +
READ + " INTEGER DEFAULT 0, " + MESSAGE_ID + " TEXT, " + SUBJECT + " TEXT, " + 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, " + EXPIRY + " INTEGER, " + MESSAGE_CLASS + " TEXT, " + MESSAGE_TYPE + " INTEGER, " +
MMS_VERSION + " INTEGER, " + MESSAGE_SIZE + " INTEGER, " + PRIORITY + " INTEGER, " + MMS_VERSION + " INTEGER, " + MESSAGE_SIZE + " INTEGER, " + PRIORITY + " INTEGER, " +
READ_REPORT + " INTEGER, " + REPORT_ALLOWED + " INTEGER, " + RESPONSE_STATUS + " 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, CONTENT_LOCATION, EXPIRY, MESSAGE_CLASS, MESSAGE_TYPE, MMS_VERSION,
MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS, MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS,
RETRIEVE_TEXT, RETRIEVE_TEXT_CS, READ_STATUS, CONTENT_CLASS, RESPONSE_TEXT, 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) { public MmsDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper); super(context, databaseHelper);
} }
@ -288,7 +299,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
return new NotificationInd(headers); return new NotificationInd(headers);
} }
public MultimediaMessagePdu getMediaMessage(long messageId) private MultimediaMessagePdu getMediaMessage(long messageId)
throws MmsException throws MmsException
{ {
PduHeaders headers = getHeadersForId(messageId); PduHeaders headers = getHeadersForId(messageId);
@ -340,8 +351,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
} }
} }
private long insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved, private Pair<Long, Long> insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved,
String contentLocation, long threadId, long mailbox) String contentLocation, long threadId, long mailbox)
throws MmsException throws MmsException
{ {
PduHeaders headers = retrieved.getPduHeaders(); PduHeaders headers = retrieved.getPduHeaders();
@ -364,34 +375,42 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
if (!contentValues.containsKey(DATE_SENT)) if (!contentValues.containsKey(DATE_SENT))
contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); 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<Long, Long>(threadId, messageId);
} }
public long insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved, public Pair<Long, Long> insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved,
String contentLocation, long threadId) String contentLocation, long threadId)
throws MmsException throws MmsException
{ {
return insertMessageInbox(masterSecret, retrieved, contentLocation, threadId, return insertMessageInbox(masterSecret, retrieved, contentLocation, threadId,
Types.BASE_INBOX_TYPE | Types.ENCRYPTION_SYMMETRIC_BIT); Types.BASE_INBOX_TYPE | Types.ENCRYPTION_SYMMETRIC_BIT);
} }
public long insertSecureMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved, public Pair<Long, Long> insertSecureMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved,
String contentLocation, long threadId) String contentLocation, long threadId)
throws MmsException throws MmsException
{ {
return insertMessageInbox(masterSecret, retrieved, contentLocation, threadId, return insertMessageInbox(masterSecret, retrieved, contentLocation, threadId,
Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_REMOTE_BIT); Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_REMOTE_BIT);
} }
public long insertSecureDecryptedMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved, public Pair<Long, Long> insertSecureDecryptedMessageInbox(MasterSecret masterSecret,
long threadId) RetrieveConf retrieved,
long threadId)
throws MmsException throws MmsException
{ {
return insertMessageInbox(masterSecret, retrieved, "", threadId, return insertMessageInbox(masterSecret, retrieved, "", threadId,
Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_SYMMETRIC_BIT); Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_SYMMETRIC_BIT);
} }
public long insertMessageInbox(NotificationInd notification) { public Pair<Long, Long> insertMessageInbox(NotificationInd notification) {
try { try {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
PduHeaders headers = notification.getPduHeaders(); PduHeaders headers = notification.getPduHeaders();
@ -412,23 +431,30 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
long messageId = db.insert(TABLE_NAME, null, contentValues); long messageId = db.insert(TABLE_NAME, null, contentValues);
addressDatabase.insertAddressesForId(messageId, headers); addressDatabase.insertAddressesForId(messageId, headers);
notifyConversationListeners(threadId); // notifyConversationListeners(threadId);
DatabaseFactory.getThreadDatabase(context).update(threadId); // DatabaseFactory.getThreadDatabase(context).update(threadId);
DatabaseFactory.getThreadDatabase(context).setUnread(threadId); // DatabaseFactory.getThreadDatabase(context).setUnread(threadId);
Trimmer.trimThread(context, threadId); // Trimmer.trimThread(context, threadId);
return messageId; return new Pair<Long, Long>(messageId, threadId);
} catch (RecipientFormattingException rfe) { } catch (RecipientFormattingException rfe) {
Log.w("MmsDatabase", rfe); Log.w("MmsDatabase", rfe);
return -1; return new Pair<Long, Long>(-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, public long insertMessageOutbox(MasterSecret masterSecret, SendReq sendRequest,
long threadId, boolean isSecure) long threadId, boolean isSecure)
throws MmsException throws MmsException
{ {
long type = Types.BASE_OUTBOX_TYPE; long type = Types.BASE_OUTBOX_TYPE | Types.ENCRYPTION_SYMMETRIC_BIT;
PduHeaders headers = sendRequest.getPduHeaders(); PduHeaders headers = sendRequest.getPduHeaders();
ContentValues contentValues = getContentValuesFromHeader(headers); ContentValues contentValues = getContentValuesFromHeader(headers);
@ -453,10 +479,22 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
throws MmsException throws MmsException
{ {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, null, contentValues);
PduBody body = message.getBody();
PartDatabase partsDatabase = getPartDatabase(masterSecret); PartDatabase partsDatabase = getPartDatabase(masterSecret);
MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context); 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()); addressDatabase.insertAddressesForId(messageId, message.getPduHeaders());
partsDatabase.insertParts(messageId, body); partsDatabase.insertParts(messageId, body);
@ -480,7 +518,6 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
notifyConversationListeners(threadId); notifyConversationListeners(threadId);
} }
public void deleteThread(long threadId) { public void deleteThread(long threadId) {
Set<Long> singleThreadSet = new HashSet<Long>(); Set<Long> singleThreadSet = new HashSet<Long>();
singleThreadSet.add(threadId); singleThreadSet.add(threadId);
@ -692,12 +729,16 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
public class Reader { public class Reader {
private final Cursor cursor; private final Cursor cursor;
private final MasterSecret masterSecret; private final MasterSecret masterSecret;
private final MasterCipher masterCipher;
public Reader(MasterSecret masterSecret, Cursor cursor) { public Reader(MasterSecret masterSecret, Cursor cursor) {
this.cursor = cursor; this.cursor = cursor;
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
if (masterSecret != null) masterCipher = new MasterCipher(masterSecret);
else masterCipher = null;
} }
public MessageRecord getNext() { 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 dateReceived = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_RECEIVED));
long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)); long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX));
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID)); 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); Recipient recipient = getMessageRecipient(id);
SlideDeck slideDeck; ListenableFutureTask<SlideDeck> slideDeck = getSlideDeck(masterSecret, id);
try {
MultimediaMessagePdu pdu = getMediaMessage(id);
slideDeck = getSlideDeck(masterSecret, pdu);
} catch (MmsException me) {
Log.w("ConversationAdapter", me);
slideDeck = null;
}
return new MediaMmsMessageRecord(context, id, new Recipients(recipient), recipient, return new MediaMmsMessageRecord(context, id, new Recipients(recipient), recipient,
dateSent, dateReceived, threadId, dateSent, dateReceived, threadId, body,
slideDeck, box); slideDeck, partCount, box);
} }
protected SlideDeck getSlideDeck(MasterSecret masterSecret, MultimediaMessagePdu pdu) { private String getBody(Cursor cursor) {
if (masterSecret == null) try {
return null; 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<SlideDeck> getSlideDeck(final MasterSecret masterSecret,
final long id)
{
Callable<SlideDeck> task = new Callable<SlideDeck>() {
@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<SlideDeck> future = new ListenableFutureTask<SlideDeck>(task, null);
slideResolver.execute(future);
return future;
} }
private NotificationMmsMessageRecord getNotificationMmsMessageRecord(Cursor cursor) { private NotificationMmsMessageRecord getNotificationMmsMessageRecord(Cursor cursor) {

View File

@ -7,6 +7,7 @@ public interface MmsSmsColumns {
public static final String NORMALIZED_DATE_RECEIVED = "date_received"; public static final String NORMALIZED_DATE_RECEIVED = "date_received";
public static final String THREAD_ID = "thread_id"; public static final String THREAD_ID = "thread_id";
public static final String READ = "read"; public static final String READ = "read";
public static final String BODY = "body";
public static class Types { public static class Types {
protected static final long TOTAL_MASK = 0xFFFFFFFF; protected static final long TOTAL_MASK = 0xFFFFFFFF;

View File

@ -94,7 +94,7 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX,
SmsDatabase.STATUS, TRANSPORT}; SmsDatabase.STATUS, MmsDatabase.PART_COUNT, TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC"; String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
@ -108,11 +108,12 @@ public class MmsSmsDatabase extends Database {
public Cursor getConversationSnippet(long threadId) { public Cursor getConversationSnippet(long threadId) {
String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE, String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE,
MmsSmsColumns.THREAD_ID,
SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, SmsDatabase.ADDRESS, SmsDatabase.SUBJECT,
MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX,
TRANSPORT}; SmsDatabase.STATUS, MmsDatabase.PART_COUNT, TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
@ -126,7 +127,7 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.NORMALIZED_DATE_SENT, MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX,
TRANSPORT}; MmsDatabase.PART_COUNT, TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC"; String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
String selection = MmsSmsColumns.READ + " = 0"; String selection = MmsSmsColumns.READ + " = 0";
@ -145,13 +146,13 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.DATE_RECEIVED + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsDatabase.DATE_RECEIVED + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, 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, String[] smsProjection = {SmsDatabase.DATE_SENT + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, SmsDatabase.DATE_RECEIVED + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, 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(); SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
@ -171,10 +172,12 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsDatabase.DATE_RECEIVED); mmsColumnsPresent.add(MmsDatabase.DATE_RECEIVED);
mmsColumnsPresent.add(MmsSmsColumns.READ); mmsColumnsPresent.add(MmsSmsColumns.READ);
mmsColumnsPresent.add(MmsSmsColumns.THREAD_ID); mmsColumnsPresent.add(MmsSmsColumns.THREAD_ID);
mmsColumnsPresent.add(MmsSmsColumns.BODY);
mmsColumnsPresent.add(MmsDatabase.PART_COUNT);
Set<String> smsColumnsPresent = new HashSet<String>(); Set<String> smsColumnsPresent = new HashSet<String>();
smsColumnsPresent.add(MmsSmsColumns.ID); smsColumnsPresent.add(MmsSmsColumns.ID);
smsColumnsPresent.add(SmsDatabase.BODY); smsColumnsPresent.add(MmsSmsColumns.BODY);
smsColumnsPresent.add(SmsDatabase.TYPE); smsColumnsPresent.add(SmsDatabase.TYPE);
smsColumnsPresent.add(SmsDatabase.ADDRESS); smsColumnsPresent.add(SmsDatabase.ADDRESS);
smsColumnsPresent.add(SmsDatabase.SUBJECT); smsColumnsPresent.add(SmsDatabase.SUBJECT);
@ -242,5 +245,9 @@ public class MmsSmsDatabase extends Database {
return smsReader.getCurrent(); return smsReader.getCurrent();
} }
} }
public void close() {
cursor.close();
}
} }
} }

View File

@ -25,12 +25,7 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log; import android.util.Log;
import org.thoughtcrime.securesms.providers.PartProvider; import org.thoughtcrime.securesms.providers.PartProvider;
import org.thoughtcrime.securesms.util.Util;
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 java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
@ -39,7 +34,11 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; 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 { public class PartDatabase extends Database {
@ -83,32 +82,32 @@ public class PartDatabase extends Database {
int contentTypeColumn = cursor.getColumnIndexOrThrow(CONTENT_TYPE); int contentTypeColumn = cursor.getColumnIndexOrThrow(CONTENT_TYPE);
if (!cursor.isNull(contentTypeColumn)) if (!cursor.isNull(contentTypeColumn))
part.setContentType(getBytes(cursor.getString(contentTypeColumn))); part.setContentType(Util.toIsoBytes(cursor.getString(contentTypeColumn)));
int nameColumn = cursor.getColumnIndexOrThrow(NAME); int nameColumn = cursor.getColumnIndexOrThrow(NAME);
if (!cursor.isNull(nameColumn)) if (!cursor.isNull(nameColumn))
part.setName(getBytes(cursor.getString(nameColumn))); part.setName(Util.toIsoBytes(cursor.getString(nameColumn)));
int fileNameColumn = cursor.getColumnIndexOrThrow(FILENAME); int fileNameColumn = cursor.getColumnIndexOrThrow(FILENAME);
if (!cursor.isNull(fileNameColumn)) if (!cursor.isNull(fileNameColumn))
part.setFilename(getBytes(cursor.getString(fileNameColumn))); part.setFilename(Util.toIsoBytes(cursor.getString(fileNameColumn)));
int contentDispositionColumn = cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION); int contentDispositionColumn = cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION);
if (!cursor.isNull(contentDispositionColumn)) if (!cursor.isNull(contentDispositionColumn))
part.setContentDisposition(getBytes(cursor.getString(contentDispositionColumn))); part.setContentDisposition(Util.toIsoBytes(cursor.getString(contentDispositionColumn)));
int contentIdColumn = cursor.getColumnIndexOrThrow(CONTENT_ID); int contentIdColumn = cursor.getColumnIndexOrThrow(CONTENT_ID);
if (!cursor.isNull(contentIdColumn)) if (!cursor.isNull(contentIdColumn))
part.setContentId(getBytes(cursor.getString(contentIdColumn))); part.setContentId(Util.toIsoBytes(cursor.getString(contentIdColumn)));
int contentLocationColumn = cursor.getColumnIndexOrThrow(CONTENT_LOCATION); int contentLocationColumn = cursor.getColumnIndexOrThrow(CONTENT_LOCATION);
if (!cursor.isNull(contentLocationColumn)) if (!cursor.isNull(contentLocationColumn))
part.setContentLocation(getBytes(cursor.getString(contentLocationColumn))); part.setContentLocation(Util.toIsoBytes(cursor.getString(contentLocationColumn)));
int encryptedColumn = cursor.getColumnIndexOrThrow(ENCRYPTED); int encryptedColumn = cursor.getColumnIndexOrThrow(ENCRYPTED);
@ -125,9 +124,9 @@ public class PartDatabase extends Database {
} }
if (part.getContentType() != null) { 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); contentValues.put(SEQUENCE, -1);
} else { } else {
throw new MmsException("There is no content type for this part."); throw new MmsException("There is no content type for this part.");
@ -142,15 +141,15 @@ public class PartDatabase extends Database {
} }
if (part.getContentDisposition() != null) { if (part.getContentDisposition() != null) {
contentValues.put(CONTENT_DISPOSITION, toIsoString(part.getContentDisposition())); contentValues.put(CONTENT_DISPOSITION, Util.toIsoString(part.getContentDisposition()));
} }
if (part.getContentId() != null) { if (part.getContentId() != null) {
contentValues.put(CONTENT_ID, toIsoString(part.getContentId())); contentValues.put(CONTENT_ID, Util.toIsoString(part.getContentId()));
} }
if (part.getContentLocation() != null) { 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); 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<body.getPartsNum();i++) { for (int i=0;i<body.getPartsNum();i++) {
long partId = insertPart(body.getPart(i), mmsId); long partId = insertPart(body.getPart(i), mmsId);
Log.w("PartDatabase", "Inserted part at ID: " + partId); Log.w("PartDatabase", "Inserted part at ID: " + partId);
@ -340,23 +339,4 @@ public class PartDatabase extends Database {
parts[i].delete(); parts[i].delete();
} }
} }
private byte[] getBytes(String data) {
try {
return data.getBytes(CharacterSets.MIMENAME_ISO_8859_1);
} catch (UnsupportedEncodingException e) {
Log.e("PduHeadersBuilder", "ISO_8859_1 must be supported!", e);
return new byte[0];
}
}
private 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 "";
}
}
} }

View File

@ -60,7 +60,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
public static final String TYPE = "type"; public static final String TYPE = "type";
public static final String REPLY_PATH_PRESENT = "reply_path_present"; public static final String REPLY_PATH_PRESENT = "reply_path_present";
public static final String SUBJECT = "subject"; public static final String SUBJECT = "subject";
public static final String BODY = "body"; //public static final String BODY = "body";
public static final String SERVICE_CENTER = "service_center"; public static final String SERVICE_CENTER = "service_center";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " integer PRIMARY KEY, " + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " integer PRIMARY KEY, " +

View File

@ -25,11 +25,13 @@ import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.InvalidMessageException; import org.thoughtcrime.securesms.util.InvalidMessageException;
import org.thoughtcrime.securesms.util.Util;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
@ -122,7 +124,7 @@ public class ThreadDatabase extends Database {
contentValues.put(SNIPPET_TYPE, type); contentValues.put(SNIPPET_TYPE, type);
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId+""}); db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""});
notifyConversationListListeners(); notifyConversationListListeners();
} }
@ -363,21 +365,20 @@ public class ThreadDatabase extends Database {
return; return;
} }
Cursor cursor = null; MmsSmsDatabase.Reader reader = null;
try { try {
cursor = mmsSmsDatabase.getConversationSnippet(threadId); reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId));
if (cursor != null && cursor.moveToFirst()) { MessageRecord record = null;
updateThread(threadId, count,
cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY)), if (reader != null && (record = reader.getNext()) != null) {
cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED)), updateThread(threadId, count, record.getBody(), record.getDateReceived(), record.getType());
cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE)));
} else { } else {
deleteThread(threadId); deleteThread(threadId);
} }
} finally { } finally {
if (cursor != null) if (reader != null)
cursor.close(); reader.close();
} }
notifyConversationListListeners(); notifyConversationListListeners();
@ -401,10 +402,12 @@ public class ThreadDatabase extends Database {
private final Cursor cursor; private final Cursor cursor;
private final MasterSecret masterSecret; private final MasterSecret masterSecret;
private final MasterCipher masterCipher;
public Reader(Cursor cursor, MasterSecret masterSecret) { public Reader(Cursor cursor, MasterSecret masterSecret) {
this.cursor = cursor; this.cursor = cursor;
this.masterSecret = masterSecret; this.masterSecret = masterSecret;
this.masterCipher = new MasterCipher(masterSecret);
} }
public ThreadRecord getNext() { public ThreadRecord getNext() {
@ -438,8 +441,7 @@ public class ThreadDatabase extends Database {
return ciphertextBody; return ciphertextBody;
try { try {
if (MmsSmsColumns.Types.isSymmetricEncryption(type)) { if (!Util.isEmpty(ciphertextBody) && MmsSmsColumns.Types.isSymmetricEncryption(type)) {
MasterCipher masterCipher = new MasterCipher(masterSecret);
return masterCipher.decryptBody(ciphertextBody); return masterCipher.decryptBody(ciphertextBody);
} else { } else {
return ciphertextBody; return ciphertextBody;

View File

@ -54,7 +54,7 @@ public abstract class DisplayRecord {
} }
public String getBody() { public String getBody() {
return body; return body == null ? "" : body;
} }
public abstract SpannableString getDisplayBody(); public abstract SpannableString getDisplayBody();

View File

@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
/** /**
* Represents the message record model for MMS messages that contain * Represents the message record model for MMS messages that contain
@ -37,24 +38,30 @@ import org.thoughtcrime.securesms.recipients.Recipients;
public class MediaMmsMessageRecord extends MessageRecord { public class MediaMmsMessageRecord extends MessageRecord {
private final Context context; private final Context context;
private final SlideDeck slideDeck; private final int partCount;
private final ListenableFutureTask<SlideDeck> slideDeck;
public MediaMmsMessageRecord(Context context, long id, Recipients recipients, public MediaMmsMessageRecord(Context context, long id, Recipients recipients,
Recipient individualRecipient, long dateSent, long dateReceived, Recipient individualRecipient, long dateSent, long dateReceived,
long threadId, SlideDeck slideDeck, long mailbox) long threadId, String body, ListenableFutureTask<SlideDeck> slideDeck,
int partCount, long mailbox)
{ {
super(context, id, getBodyFromSlidesIfAvailable(slideDeck), recipients, super(context, id, body, recipients, individualRecipient, dateSent, dateReceived,
individualRecipient, dateSent, dateReceived,
threadId, DELIVERY_STATUS_NONE, mailbox); threadId, DELIVERY_STATUS_NONE, mailbox);
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.partCount = partCount;
this.slideDeck = slideDeck; this.slideDeck = slideDeck;
} }
public SlideDeck getSlideDeck() { public ListenableFutureTask<SlideDeck> getSlideDeck() {
return slideDeck; return slideDeck;
} }
public int getPartCount() {
return partCount;
}
@Override @Override
public boolean isMms() { public boolean isMms() {
return true; return true;
@ -73,16 +80,16 @@ public class MediaMmsMessageRecord extends MessageRecord {
return super.getDisplayBody(); return super.getDisplayBody();
} }
private static String getBodyFromSlidesIfAvailable(SlideDeck slideDeck) { // private static String getBodyFromSlidesIfAvailable(SlideDeck slideDeck) {
if (slideDeck == null) // if (slideDeck == null)
return ""; // return "";
//
for (Slide slide : slideDeck.getSlides()) { // for (Slide slide : slideDeck.getSlides()) {
if (slide.hasText()) // if (slide.hasText())
return slide.getText(); // return slide.getText();
} // }
//
return ""; // return "";
} // }
} }

View File

@ -107,6 +107,10 @@ public abstract class MessageRecord extends DisplayRecord {
return individualRecipient; return individualRecipient;
} }
public long getType() {
return type;
}
protected SpannableString emphasisAdded(String sequence) { protected SpannableString emphasisAdded(String sequence) {
SpannableString spannable = new SpannableString(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); spannable.setSpan(new ForegroundColorSpan(context.getResources().getColor(android.R.color.darker_gray)), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

View File

@ -54,21 +54,21 @@ public class AttachmentManager {
public void setImage(Uri image) throws IOException { public void setImage(Uri image) throws IOException {
ImageSlide slide = new ImageSlide(context, image); ImageSlide slide = new ImageSlide(context, image);
slideDeck.addSlide(slide); slideDeck.addSlide(slide);
thumbnail.setImageBitmap(slide.getThumbnail()); thumbnail.setImageBitmap(slide.getThumbnail(345, 261));
attachmentView.setVisibility(View.VISIBLE); attachmentView.setVisibility(View.VISIBLE);
} }
public void setVideo(Uri video) throws IOException, MediaTooLargeException { public void setVideo(Uri video) throws IOException, MediaTooLargeException {
VideoSlide slide = new VideoSlide(context, video); VideoSlide slide = new VideoSlide(context, video);
slideDeck.addSlide(slide); slideDeck.addSlide(slide);
thumbnail.setImageBitmap(slide.getThumbnail()); thumbnail.setImageBitmap(slide.getThumbnail(thumbnail.getWidth(), thumbnail.getHeight()));
attachmentView.setVisibility(View.VISIBLE); attachmentView.setVisibility(View.VISIBLE);
} }
public void setAudio(Uri audio)throws IOException, MediaTooLargeException { public void setAudio(Uri audio)throws IOException, MediaTooLargeException {
AudioSlide slide = new AudioSlide(context, audio); AudioSlide slide = new AudioSlide(context, audio);
slideDeck.addSlide(slide); slideDeck.addSlide(slide);
thumbnail.setImageBitmap(slide.getThumbnail()); thumbnail.setImageBitmap(slide.getThumbnail(thumbnail.getWidth(), thumbnail.getHeight()));
attachmentView.setVisibility(View.VISIBLE); attachmentView.setVisibility(View.VISIBLE);
} }

View File

@ -27,6 +27,7 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.net.Uri; import android.net.Uri;
import android.provider.MediaStore.Audio; import android.provider.MediaStore.Audio;
import android.widget.ImageView;
public class AudioSlide extends Slide { public class AudioSlide extends Slide {
@ -49,10 +50,10 @@ public class AudioSlide extends Slide {
} }
@Override @Override
public Bitmap getThumbnail() { public Bitmap getThumbnail(int maxWidth, int maxHeight) {
return BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_menu_add_sound); return BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_menu_add_sound);
} }
public static PduPart constructPartFromUri(Context context, Uri uri) throws IOException, MediaTooLargeException { public static PduPart constructPartFromUri(Context context, Uri uri) throws IOException, MediaTooLargeException {
PduPart part = new PduPart(); PduPart part = new PduPart();

View File

@ -16,25 +16,33 @@
*/ */
package org.thoughtcrime.securesms.mms; 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.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.LinkedHashMap; 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.ContentType;
import ws.com.google.android.mms.pdu.PduPart; 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 { public class ImageSlide extends Slide {
@ -56,67 +64,98 @@ public class ImageSlide extends Slide {
} }
@Override @Override
public Bitmap getThumbnail() { public Bitmap getThumbnail(int maxWidth, int maxHeight) {
if (thumbnailCache.containsKey(part.getDataUri())) { Bitmap thumbnail = getCachedThumbnail();
Log.w("ImageSlide", "Cached thumbnail...");
Bitmap bitmap = thumbnailCache.get(part.getDataUri()).get(); if (thumbnail != null)
if (bitmap != null) return bitmap; return thumbnail;
else thumbnailCache.remove(part.getDataUri());
}
try { try {
BitmapFactory.Options options = getImageDimensions(getPartDataInputStream()); InputStream measureStream = getPartDataInputStream();
int imageWidth = options.outWidth; InputStream dataStream = getPartDataInputStream();
int imageHeight = options.outHeight;
thumbnail = BitmapUtil.createScaledBitmap(measureStream, dataStream, maxWidth, maxHeight);
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);
thumbnailCache.put(part.getDataUri(), new SoftReference<Bitmap>(thumbnail)); thumbnailCache.put(part.getDataUri(), new SoftReference<Bitmap>(thumbnail));
return thumbnail; return thumbnail;
} catch (FileNotFoundException fnfe) { } catch (FileNotFoundException e) {
Log.w("ImageSlide", fnfe); Log.w("ImageSlide", e);
return BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_missing_thumbnail_picture); 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 @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<ImageView> weakImageView = new WeakReference<ImageView>(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<Bitmap> 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; return true;
} }
private static PduPart constructPartFromUri(Context context, Uri uri) throws IOException { private static PduPart constructPartFromUri(Context context, Uri uri) throws IOException {
PduPart part = new PduPart(); PduPart part = new PduPart();
byte[] data = BitmapUtil.createScaledBytes(context, uri, 640, 480, (300 * 1024) - 5000);
BitmapFactory.Options options = getImageDimensions(context, uri);
long size = getMediaSize(context, uri); part.setData(data);
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..");
part.setDataUri(uri); part.setDataUri(uri);
part.setContentType(ContentType.IMAGE_JPEG.getBytes()); part.setContentType(ContentType.IMAGE_JPEG.getBytes());
part.setContentId((System.currentTimeMillis()+"").getBytes()); part.setContentId((System.currentTimeMillis()+"").getBytes());
@ -124,25 +163,4 @@ public class ImageSlide extends Slide {
return part; 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();
}
} }

View File

@ -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<body.getPartsNum();i++) {
if (ContentType.isTextType(Util.toIsoString(body.getPart(i).getContentType()))) {
String partText;
try {
partText = new String(body.getPart(i).getData(),
CharacterSets.getMimeName(body.getPart(i).getCharset()));
} catch (UnsupportedEncodingException e) {
Log.w("PartParser", e);
partText = "Unsupported Encoding!";
}
bodyText = (bodyText == null) ? partText : bodyText + " " + partText;
}
}
return bodyText;
}
public static PduBody getNonTextParts(PduBody body) {
PduBody stripped = new PduBody();
for (int i=0;i<body.getPartsNum();i++) {
if (!ContentType.isTextType(Util.toIsoString(body.getPart(i).getContentType()))) {
stripped.addPart(body.getPart(i));
}
}
return stripped;
}
}

View File

@ -29,6 +29,8 @@ import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import android.widget.ImageView;
import ws.com.google.android.mms.pdu.PduPart; import ws.com.google.android.mms.pdu.PduPart;
public abstract class Slide { public abstract class Slide {
@ -88,10 +90,14 @@ public abstract class Slide {
return part.getDataUri(); return part.getDataUri();
} }
public Bitmap getThumbnail() { public Bitmap getThumbnail(int maxWidth, int maxHeight) {
throw new AssertionError("getThumbnail() called on non-thumbnail producing slide!"); throw new AssertionError("getThumbnail() called on non-thumbnail producing slide!");
} }
public void setThumbnailOn(ImageView imageView) {
imageView.setImageBitmap(getThumbnail(imageView.getWidth(), imageView.getHeight()));
}
public boolean hasImage() { public boolean hasImage() {
return false; return false;
} }

View File

@ -21,7 +21,6 @@ import android.content.Context;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;

View File

@ -29,6 +29,7 @@ import android.graphics.BitmapFactory;
import android.net.Uri; import android.net.Uri;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.util.Log; import android.util.Log;
import android.widget.ImageView;
public class VideoSlide extends Slide { public class VideoSlide extends Slide {
@ -41,10 +42,10 @@ public class VideoSlide extends Slide {
} }
@Override @Override
public Bitmap getThumbnail() { public Bitmap getThumbnail(int width, int height) {
return BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher_video_player); return BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher_video_player);
} }
@Override @Override
public boolean hasImage() { public boolean hasImage() {
return true; return true;

View File

@ -262,6 +262,7 @@ public class MessageNotifier {
notificationState.addNotification(new NotificationItem(recipient, recipients, threadId, body, image)); notificationState.addNotification(new NotificationItem(recipient, recipients, threadId, body, image));
} }
reader.close();
return notificationState; return notificationState;
} }

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.DecryptingQueue; import org.thoughtcrime.securesms.crypto.DecryptingQueue;
@ -27,6 +28,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.mms.MmsDownloadHelper; import org.thoughtcrime.securesms.mms.MmsDownloadHelper;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.protocol.WirePrefix;
import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.MmsException;
@ -34,6 +36,7 @@ import ws.com.google.android.mms.pdu.RetrieveConf;
import java.io.IOException; import java.io.IOException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
public class MmsDownloader extends MmscProcessor { public class MmsDownloader extends MmscProcessor {
@ -51,6 +54,7 @@ public class MmsDownloader extends MmscProcessor {
DownloadItem item = new DownloadItem(masterSecret, !isCdma, false, DownloadItem item = new DownloadItem(masterSecret, !isCdma, false,
intent.getLongExtra("message_id", -1), intent.getLongExtra("message_id", -1),
intent.getLongExtra("thread_id", -1), intent.getLongExtra("thread_id", -1),
intent.getBooleanExtra("automatic", false),
intent.getStringExtra("content_location"), intent.getStringExtra("content_location"),
intent.getByteArrayExtra("transaction_id")); intent.getByteArrayExtra("transaction_id"));
@ -63,8 +67,8 @@ public class MmsDownloader extends MmscProcessor {
private void handleDownloadMmsAction(DownloadItem item) { private void handleDownloadMmsAction(DownloadItem item) {
if (!isConnectivityPossible()) { if (!isConnectivityPossible()) {
Log.w("MmsDownloader", "No MMS connectivity available!"); Log.w("MmsDownloader", "No MMS connectivity available!");
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY); handleDownloadError(item, MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY,
toastHandler.makeToast(context.getString(R.string.MmsDownloader_no_connectivity_available_for_mms_download_try_again_later)); context.getString(R.string.MmsDownloader_no_connectivity_available_for_mms_download_try_again_later));
return; return;
} }
@ -109,33 +113,38 @@ public class MmsDownloader extends MmscProcessor {
Log.w("MmsDownloadeR", "Falling back to radio mode and proxy..."); Log.w("MmsDownloadeR", "Falling back to radio mode and proxy...");
scheduleDownloadWithRadioModeAndProxy(item); scheduleDownloadWithRadioModeAndProxy(item);
} else { } else {
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE); handleDownloadError(item, MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE,
toastHandler.makeToast(context.getString(R.string.MmsDownloader_error_connecting_to_mms_provider)); context.getString(R.string.MmsDownloader_error_connecting_to_mms_provider));
} }
} catch (MmsException e) { } catch (MmsException e) {
Log.w("MmsDownloader", e); Log.w("MmsDownloader", e);
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Status.DOWNLOAD_HARD_FAILURE); handleDownloadError(item, MmsDatabase.Status.DOWNLOAD_HARD_FAILURE,
toastHandler.makeToast(context.getString(R.string.MmsDownloader_error_storing_mms)); context.getString(R.string.MmsDownloader_error_storing_mms));
} }
} }
private void storeRetrievedMms(MmsDatabase mmsDatabase, DownloadItem item, RetrieveConf retrieved) private void storeRetrievedMms(MmsDatabase mmsDatabase, DownloadItem item, RetrieveConf retrieved)
throws MmsException throws MmsException
{ {
Pair<Long, Long> messageAndThreadId;
if (retrieved.getSubject() != null && WirePrefix.isEncryptedMmsSubject(retrieved.getSubject().getString())) { if (retrieved.getSubject() != null && WirePrefix.isEncryptedMmsSubject(retrieved.getSubject().getString())) {
long messageId = mmsDatabase.insertSecureMessageInbox(item.getMasterSecret(), retrieved, messageAndThreadId = mmsDatabase.insertSecureMessageInbox(item.getMasterSecret(), retrieved,
item.getContentLocation(), item.getContentLocation(),
item.getThreadId()); item.getThreadId());
if (item.getMasterSecret() != null) if (item.getMasterSecret() != null)
DecryptingQueue.scheduleDecryption(context, item.getMasterSecret(), messageId, item.getThreadId(), retrieved); DecryptingQueue.scheduleDecryption(context, item.getMasterSecret(), messageAndThreadId.first,
messageAndThreadId.second, retrieved);
} else { } else {
mmsDatabase.insertMessageInbox(item.getMasterSecret(), retrieved, item.getContentLocation(), messageAndThreadId = mmsDatabase.insertMessageInbox(item.getMasterSecret(), retrieved,
item.getThreadId()); item.getContentLocation(),
item.getThreadId());
} }
mmsDatabase.delete(item.getMessageId()); mmsDatabase.delete(item.getMessageId());
MessageNotifier.updateNotification(context, item.getMasterSecret(), messageAndThreadId.second);
} }
protected void handleConnectivityChange() { protected void handleConnectivityChange() {
@ -153,18 +162,38 @@ public class MmsDownloader extends MmscProcessor {
} else if (!isConnected() && !isConnectivityPossible()) { } else if (!isConnected() && !isConnectivityPossible()) {
pendingMessages.clear(); pendingMessages.clear();
handleDownloadError(downloadItems, MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY,
for (DownloadItem item : downloadItems) { context.getString(R.string.MmsDownloader_no_connectivity_available_for_mms_download_try_again_later));
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));
finishConnectivity(); finishConnectivity();
} }
} }
private void handleDownloadError(List<DownloadItem> 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) { private void scheduleDownloadWithRadioMode(DownloadItem item) {
item.mmsRadioMode = true; item.mmsRadioMode = true;
@ -186,9 +215,11 @@ public class MmsDownloader extends MmscProcessor {
private long messageId; private long messageId;
private byte[] transactionId; private byte[] transactionId;
private String contentLocation; private String contentLocation;
private boolean automatic;
public DownloadItem(MasterSecret masterSecret, boolean mmsRadioMode, boolean proxyIfPossible, 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.masterSecret = masterSecret;
this.mmsRadioMode = mmsRadioMode; this.mmsRadioMode = mmsRadioMode;
@ -197,6 +228,7 @@ public class MmsDownloader extends MmscProcessor {
this.messageId = messageId; this.messageId = messageId;
this.contentLocation = contentLocation; this.contentLocation = contentLocation;
this.transactionId = transactionId; this.transactionId = transactionId;
this.automatic = automatic;
} }
public long getThreadId() { public long getThreadId() {
@ -226,6 +258,10 @@ public class MmsDownloader extends MmscProcessor {
public boolean useMmsRadioMode() { public boolean useMmsRadioMode() {
return mmsRadioMode; return mmsRadioMode;
} }
public boolean isAutomatic() {
return automatic;
}
} }
@Override @Override

View File

@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -44,6 +45,7 @@ public class MmsReceiver {
intent.putExtra("message_id", messageId); intent.putExtra("message_id", messageId);
intent.putExtra("transaction_id", pdu.getTransactionId()); intent.putExtra("transaction_id", pdu.getTransactionId());
intent.putExtra("thread_id", threadId); intent.putExtra("thread_id", threadId);
intent.putExtra("automatic", true);
context.startService(intent); context.startService(intent);
} }
@ -54,12 +56,12 @@ public class MmsReceiver {
GenericPdu pdu = parser.parse(); GenericPdu pdu = parser.parse();
if (pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) { if (pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
long messageId = database.insertMessageInbox((NotificationInd)pdu); Pair<Long, Long> messageAndThreadId = database.insertMessageInbox((NotificationInd)pdu);
long threadId = database.getThreadIdForMessage(messageId); // long threadId = database.getThreadIdForMessage(messageId);
MessageNotifier.updateNotification(context, masterSecret, threadId); // MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
scheduleDownload((NotificationInd)pdu, messageId, threadId); scheduleDownload((NotificationInd)pdu, messageAndThreadId.first, messageAndThreadId.second);
Log.w("MmsReceiverService", "Inserted received notification..."); Log.w("MmsReceiverService", "Inserted received notification...");
} }

View File

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

View File

@ -1,20 +1,30 @@
package org.thoughtcrime.securesms.util; package org.thoughtcrime.securesms.util;
import java.lang.ref.WeakReference;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask; import java.util.concurrent.FutureTask;
public class ListenableFutureTask<V> extends FutureTask<V> { public class ListenableFutureTask<V> extends FutureTask<V> {
// private WeakReference<FutureTaskListener<V>> listener;
private FutureTaskListener<V> listener; private FutureTaskListener<V> listener;
public ListenableFutureTask(Callable<V> callable, FutureTaskListener<V> listener) { public ListenableFutureTask(Callable<V> callable, FutureTaskListener<V> listener) {
super(callable); super(callable);
this.listener = listener; this.listener = listener;
// if (listener == null) {
// this.listener = null;
// } else {
// this.listener = new WeakReference<FutureTaskListener<V>>(listener);
// }
} }
public synchronized void setListener(FutureTaskListener<V> listener) { public synchronized void setListener(FutureTaskListener<V> listener) {
// if (listener != null) this.listener = new WeakReference<FutureTaskListener<V>>(listener);
// else this.listener = null;
this.listener = listener; this.listener = listener;
if (this.isDone()) { if (this.isDone()) {
callback(); callback();
} }
@ -27,12 +37,16 @@ public class ListenableFutureTask<V> extends FutureTask<V> {
private void callback() { private void callback() {
if (this.listener != null) { if (this.listener != null) {
try { FutureTaskListener<V> nestedListener = this.listener;
this.listener.onSuccess(get()); // FutureTaskListener<V> nestedListener = this.listener.get();
} catch (ExecutionException ee) { if (nestedListener != null) {
this.listener.onFailure(ee); try {
} catch (InterruptedException e) { nestedListener.onSuccess(get());
throw new AssertionError(e); } catch (ExecutionException ee) {
nestedListener.onFailure(ee);
} catch (InterruptedException e) {
throw new AssertionError(e);
}
} }
} }
} }

View File

@ -22,17 +22,20 @@ import android.graphics.Typeface;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.util.Log;
import android.widget.EditText; import android.widget.EditText;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue; import ws.com.google.android.mms.pdu.EncodedStringValue;
public class Util { public class Util {
@ -129,6 +132,25 @@ public class Util {
return spanned; 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) { public static void showAlertDialog(Context context, String title, String message) {
AlertDialog.Builder dialog = new AlertDialog.Builder(context); AlertDialog.Builder dialog = new AlertDialog.Builder(context);
dialog.setTitle(title); dialog.setTitle(title);