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
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" />

View File

@ -70,11 +70,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" />

View File

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

View File

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

View File

@ -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,27 +239,41 @@ public class ConversationItem extends LinearLayout {
}
private void setMediaMmsAttributes(MediaMmsMessageRecord messageRecord) {
SlideDeck slideDeck = messageRecord.getSlideDeck();
if (messageRecord.getPartCount() > 0) {
mmsThumbnail.setVisibility(View.VISIBLE);
mmsThumbnail.setImageDrawable(new ColorDrawable(Color.TRANSPARENT));
}
if (slideDeck != null) {
List<Slide> slides = slideDeck.getSlides();
slideDeck = messageRecord.getSlideDeck();
slideDeck.setListener(new FutureTaskListener<SlideDeck>() {
@Override
public void onSuccess(final SlideDeck result) {
if (result == null)
return;
Iterator<Slide> iterator = slides.iterator();
while (iterator.hasNext()) {
Slide slide = iterator.next();
handler.post(new Runnable() {
@Override
public void run() {
for (Slide slide : result.getSlides()) {
if (slide.hasImage()) {
mmsThumbnail.setImageBitmap(slide.getThumbnail());
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

View File

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

View File

@ -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)

View File

@ -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,7 +351,7 @@ 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)
throws MmsException
{
@ -364,10 +375,17 @@ 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,
public Pair<Long, Long> insertMessageInbox(MasterSecret masterSecret, RetrieveConf retrieved,
String contentLocation, long threadId)
throws MmsException
{
@ -375,7 +393,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
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)
throws MmsException
{
@ -383,7 +401,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
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,
RetrieveConf retrieved,
long threadId)
throws MmsException
{
@ -391,7 +410,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
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);
return new Pair<Long, Long>(messageId, threadId);
} catch (RecipientFormattingException rfe) {
Log.w("MmsDatabase", rfe);
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);
return messageId;
} catch (RecipientFormattingException rfe) {
Log.w("MmsDatabase", rfe);
return -1;
}
}
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);
@ -694,10 +731,14 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
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) {
private String getBody(Cursor cursor) {
try {
String body = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.BODY));
long box = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX));
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) {

View File

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

View File

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

View File

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

View File

@ -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, " +

View File

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

View File

@ -54,7 +54,7 @@ public abstract class DisplayRecord {
}
public String getBody() {
return body;
return body == null ? "" : body;
}
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.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 "";
// }
}

View File

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

View File

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

View File

@ -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,7 +50,7 @@ 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);
}

View File

@ -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,47 +64,86 @@ 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;
InputStream measureStream = getPartDataInputStream();
InputStream dataStream = getPartDataInputStream();
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);
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);
@Override
public void setThumbnailOn(ImageView imageView) {
Bitmap thumbnail = getCachedThumbnail();
return options;
if (thumbnail != null) {
Log.w("ImageSlide", "Setting cached thumbnail...");
setThumbnailOn(imageView, thumbnail, true);
return;
}
private static BitmapFactory.Options getImageDimensions(Context context, Uri uri) throws FileNotFoundException {
InputStream in = context.getContentResolver().openInputStream(uri);
return getImageDimensions(in);
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
@ -106,17 +153,9 @@ public class ImageSlide extends Slide {
private static PduPart constructPartFromUri(Context context, Uri uri) throws IOException {
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);
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.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();
}
}

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

View File

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

View File

@ -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,7 +42,7 @@ 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);
}

View File

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

View File

@ -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,
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(),
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

View File

@ -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);
}
@ -55,11 +57,11 @@ public class MmsReceiver {
if (pdu.getMessageType() == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
long messageId = database.insertMessageInbox((NotificationInd)pdu);
long threadId = database.getThreadIdForMessage(messageId);
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...");
}

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;
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,13 +37,17 @@ public class ListenableFutureTask<V> extends FutureTask<V> {
private void callback() {
if (this.listener != null) {
FutureTaskListener<V> nestedListener = this.listener;
// FutureTaskListener<V> nestedListener = this.listener.get();
if (nestedListener != null) {
try {
this.listener.onSuccess(get());
nestedListener.onSuccess(get());
} catch (ExecutionException ee) {
this.listener.onFailure(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.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);