mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
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:
parent
dd0aecc811
commit
7c47ea5cec
@ -48,11 +48,10 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="230dip"
|
||||
android:layout_height="174dip"
|
||||
android:layout_gravity="center"
|
||||
android:maxWidth="178dip"
|
||||
android:maxHeight="178dip"
|
||||
android:scaleType="centerInside"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@android:drawable/picture_frame"
|
||||
android:visibility="gone" />
|
||||
|
@ -69,15 +69,14 @@
|
||||
android:paddingBottom="7dip">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:maxWidth="178dip"
|
||||
android:maxHeight="178dip"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@android:drawable/picture_frame"
|
||||
android:visibility="gone" />
|
||||
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" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/play_slideshow_button"
|
||||
|
@ -22,6 +22,7 @@ import android.os.Handler;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.CursorAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
@ -42,7 +43,7 @@ import java.util.LinkedHashMap;
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public class ConversationAdapter extends CursorAdapter {
|
||||
public class ConversationAdapter extends CursorAdapter implements AbsListView.RecyclerListener {
|
||||
|
||||
private static final int MAX_CACHE_SIZE = 40;
|
||||
|
||||
@ -129,6 +130,11 @@ public class ConversationAdapter extends CursorAdapter {
|
||||
this.getCursor().close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMovedToScrapHeap(View view) {
|
||||
((ConversationItem)view).unbind();
|
||||
}
|
||||
|
||||
private LinkedHashMap<String,MessageRecord> initializeCache() {
|
||||
return new LinkedHashMap<String,MessageRecord>() {
|
||||
@Override
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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> 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<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;
|
||||
}
|
||||
}
|
||||
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<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
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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<Long, Long> 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<Long, Long>(threadId, messageId);
|
||||
}
|
||||
|
||||
public long insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved,
|
||||
String contentLocation, long threadId)
|
||||
public Pair<Long, Long> 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<Long, Long> 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<Long, Long> 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<Long, Long> 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<Long, Long>(messageId, threadId);
|
||||
} catch (RecipientFormattingException 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,
|
||||
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<Long> singleThreadSet = new HashSet<Long>();
|
||||
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> 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<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) {
|
||||
|
@ -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;
|
||||
|
@ -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<String> smsColumnsPresent = new HashSet<String>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<body.getPartsNum();i++) {
|
||||
long partId = insertPart(body.getPart(i), mmsId);
|
||||
Log.w("PartDatabase", "Inserted part at ID: " + partId);
|
||||
@ -340,23 +339,4 @@ public class PartDatabase extends Database {
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
||||
public static final String TYPE = "type";
|
||||
public static final String REPLY_PATH_PRESENT = "reply_path_present";
|
||||
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 CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " integer PRIMARY KEY, " +
|
||||
|
@ -25,11 +25,13 @@ import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.InvalidMessageException;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
@ -122,7 +124,7 @@ public class ThreadDatabase extends Database {
|
||||
contentValues.put(SNIPPET_TYPE, type);
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId+""});
|
||||
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] {threadId + ""});
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
@ -363,21 +365,20 @@ public class ThreadDatabase extends Database {
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor cursor = null;
|
||||
MmsSmsDatabase.Reader reader = null;
|
||||
|
||||
try {
|
||||
cursor = mmsSmsDatabase.getConversationSnippet(threadId);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
updateThread(threadId, count,
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED)),
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE)));
|
||||
reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId));
|
||||
MessageRecord record = null;
|
||||
|
||||
if (reader != null && (record = reader.getNext()) != null) {
|
||||
updateThread(threadId, count, record.getBody(), record.getDateReceived(), record.getType());
|
||||
} else {
|
||||
deleteThread(threadId);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
if (reader != null)
|
||||
reader.close();
|
||||
}
|
||||
|
||||
notifyConversationListListeners();
|
||||
@ -401,10 +402,12 @@ public class ThreadDatabase extends Database {
|
||||
|
||||
private final Cursor cursor;
|
||||
private final MasterSecret masterSecret;
|
||||
private final MasterCipher masterCipher;
|
||||
|
||||
public Reader(Cursor cursor, MasterSecret masterSecret) {
|
||||
this.cursor = cursor;
|
||||
this.masterSecret = masterSecret;
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
}
|
||||
|
||||
public ThreadRecord getNext() {
|
||||
@ -438,8 +441,7 @@ public class ThreadDatabase extends Database {
|
||||
return ciphertextBody;
|
||||
|
||||
try {
|
||||
if (MmsSmsColumns.Types.isSymmetricEncryption(type)) {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
if (!Util.isEmpty(ciphertextBody) && MmsSmsColumns.Types.isSymmetricEncryption(type)) {
|
||||
return masterCipher.decryptBody(ciphertextBody);
|
||||
} else {
|
||||
return ciphertextBody;
|
||||
|
@ -54,7 +54,7 @@ public abstract class DisplayRecord {
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
return body == null ? "" : body;
|
||||
}
|
||||
|
||||
public abstract SpannableString getDisplayBody();
|
||||
|
@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.ListenableFutureTask;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
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,
|
||||
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,
|
||||
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<SlideDeck> 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 "";
|
||||
// }
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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<Bitmap>(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<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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
47
src/org/thoughtcrime/securesms/mms/PartParser.java
Normal file
47
src/org/thoughtcrime/securesms/mms/PartParser.java
Normal 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;
|
||||
}
|
||||
}
|
@ -29,6 +29,8 @@ import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import ws.com.google.android.mms.pdu.PduPart;
|
||||
|
||||
public abstract class Slide {
|
||||
@ -88,10 +90,14 @@ public abstract class Slide {
|
||||
return part.getDataUri();
|
||||
}
|
||||
|
||||
public Bitmap getThumbnail() {
|
||||
public Bitmap getThumbnail(int maxWidth, int maxHeight) {
|
||||
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() {
|
||||
return false;
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import android.content.Context;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -29,6 +29,7 @@ import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.widget.ImageView;
|
||||
|
||||
public class VideoSlide extends Slide {
|
||||
|
||||
@ -41,10 +42,10 @@ public class VideoSlide extends Slide {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap getThumbnail() {
|
||||
public Bitmap getThumbnail(int width, int height) {
|
||||
return BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher_video_player);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean hasImage() {
|
||||
return true;
|
||||
|
@ -262,6 +262,7 @@ public class MessageNotifier {
|
||||
notificationState.addNotification(new NotificationItem(recipient, recipients, threadId, body, image));
|
||||
}
|
||||
|
||||
reader.close();
|
||||
return notificationState;
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
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.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.mms.MmsDownloadHelper;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.protocol.WirePrefix;
|
||||
|
||||
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.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class MmsDownloader extends MmscProcessor {
|
||||
|
||||
@ -51,6 +54,7 @@ public class MmsDownloader extends MmscProcessor {
|
||||
DownloadItem item = new DownloadItem(masterSecret, !isCdma, false,
|
||||
intent.getLongExtra("message_id", -1),
|
||||
intent.getLongExtra("thread_id", -1),
|
||||
intent.getBooleanExtra("automatic", false),
|
||||
intent.getStringExtra("content_location"),
|
||||
intent.getByteArrayExtra("transaction_id"));
|
||||
|
||||
@ -63,8 +67,8 @@ public class MmsDownloader extends MmscProcessor {
|
||||
private void handleDownloadMmsAction(DownloadItem item) {
|
||||
if (!isConnectivityPossible()) {
|
||||
Log.w("MmsDownloader", "No MMS connectivity available!");
|
||||
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(item, MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY,
|
||||
context.getString(R.string.MmsDownloader_no_connectivity_available_for_mms_download_try_again_later));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -109,33 +113,38 @@ public class MmsDownloader extends MmscProcessor {
|
||||
Log.w("MmsDownloadeR", "Falling back to radio mode and proxy...");
|
||||
scheduleDownloadWithRadioModeAndProxy(item);
|
||||
} else {
|
||||
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE);
|
||||
toastHandler.makeToast(context.getString(R.string.MmsDownloader_error_connecting_to_mms_provider));
|
||||
handleDownloadError(item, MmsDatabase.Status.DOWNLOAD_SOFT_FAILURE,
|
||||
context.getString(R.string.MmsDownloader_error_connecting_to_mms_provider));
|
||||
}
|
||||
} catch (MmsException e) {
|
||||
Log.w("MmsDownloader", e);
|
||||
DatabaseFactory.getMmsDatabase(context).markDownloadState(item.getMessageId(), MmsDatabase.Status.DOWNLOAD_HARD_FAILURE);
|
||||
toastHandler.makeToast(context.getString(R.string.MmsDownloader_error_storing_mms));
|
||||
handleDownloadError(item, MmsDatabase.Status.DOWNLOAD_HARD_FAILURE,
|
||||
context.getString(R.string.MmsDownloader_error_storing_mms));
|
||||
}
|
||||
}
|
||||
|
||||
private void storeRetrievedMms(MmsDatabase mmsDatabase, DownloadItem item, RetrieveConf retrieved)
|
||||
throws MmsException
|
||||
{
|
||||
Pair<Long, Long> 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<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) {
|
||||
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
|
||||
|
@ -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<Long, Long> 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...");
|
||||
}
|
||||
|
84
src/org/thoughtcrime/securesms/util/BitmapUtil.java
Normal file
84
src/org/thoughtcrime/securesms/util/BitmapUtil.java
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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<V> extends FutureTask<V> {
|
||||
|
||||
// private WeakReference<FutureTaskListener<V>> listener;
|
||||
private FutureTaskListener<V> listener;
|
||||
|
||||
public ListenableFutureTask(Callable<V> callable, FutureTaskListener<V> listener) {
|
||||
super(callable);
|
||||
this.listener = listener;
|
||||
// if (listener == null) {
|
||||
// this.listener = null;
|
||||
// } else {
|
||||
// this.listener = new WeakReference<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;
|
||||
|
||||
if (this.isDone()) {
|
||||
callback();
|
||||
}
|
||||
@ -27,12 +37,16 @@ public class ListenableFutureTask<V> extends FutureTask<V> {
|
||||
|
||||
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<V> nestedListener = this.listener;
|
||||
// FutureTaskListener<V> nestedListener = this.listener.get();
|
||||
if (nestedListener != null) {
|
||||
try {
|
||||
nestedListener.onSuccess(get());
|
||||
} catch (ExecutionException ee) {
|
||||
nestedListener.onFailure(ee);
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user