Refactor recipient access.

1) Refactor recipient class to support asynchronous loading operations.

2) Refactor recipient factory to simplify recipient access.

3) Consoliate everything into one recipient provider that is capable of
doing async lookups and intelligent caching.
This commit is contained in:
Moxie Marlinspike 2012-12-24 08:40:37 -08:00
parent f685cb550b
commit 9939830551
27 changed files with 521 additions and 284 deletions

View File

@ -95,7 +95,7 @@ public class ContactSelectionGroupsFragment extends SherlockListFragment
for (ContactData contactData : contactDataList) {
for (NumberData numberData : contactData.numbers) {
recipientList.add(new Recipient(contactData.name, numberData.number, null));
recipientList.add(new Recipient(contactData.name, numberData.number, null, null));
}
}
}

View File

@ -101,7 +101,7 @@ public class ContactSelectionListFragment extends SherlockListFragment
for (ContactData contactData : selectedContacts.values()) {
for (NumberData numberData : contactData.numbers) {
recipientList.add(new Recipient(contactData.name, numberData.number, null));
recipientList.add(new Recipient(contactData.name, numberData.number, null, null));
}
}

View File

@ -89,7 +89,7 @@ public class ContactSelectionRecentFragment extends SherlockListFragment
for (ContactData contactData : selectedContacts.values()) {
for (NumberData numberData : contactData.numbers) {
recipientList.add(new Recipient(contactData.name, numberData.number, null));
recipientList.add(new Recipient(contactData.name, numberData.number, null, null));
}
}

View File

@ -266,10 +266,10 @@ public class ConversationAdapter extends CursorAdapter {
try {
if (address == null) recipient = recipients.getPrimaryRecipient();
else recipient = RecipientFactory.getRecipientsFromString(context, address).getPrimaryRecipient();
else recipient = RecipientFactory.getRecipientsFromString(context, address, false).getPrimaryRecipient();
} catch (RecipientFormattingException e) {
Log.w("ConversationAdapter", e);
recipient = new Recipient("Unknown", "Unknown", null);
recipient = new Recipient("Unknown", "Unknown", null, null);
}
return recipient;

View File

@ -51,8 +51,8 @@ import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.protocol.Tag;
import org.thoughtcrime.securesms.recipients.ContactPhotoFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.service.SendReceiveService;
import java.io.File;
@ -283,20 +283,9 @@ public class ConversationItem extends LinearLayout {
}
}
private void setContactPhotoForUserIdentity() {
Uri selfIdentityContact = ContactIdentityManager.getInstance(context).getSelfIdentityUri();
if (selfIdentityContact!= null) {
Recipient recipient = RecipientFactory.getRecipientForUri(context, selfIdentityContact);
if (recipient != null) {
contactPhoto.setImageBitmap(recipient.getContactPhoto());
return;
}
} else {
contactPhoto.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_contact_picture));
}
Uri uri = ContactIdentityManager.getInstance(context).getSelfIdentityUri();
contactPhoto.setImageBitmap(ContactPhotoFactory.getLocalUserContactPhoto(context, uri));
contactPhoto.setVisibility(View.VISIBLE);
}

View File

@ -329,7 +329,7 @@ public class ConversationListActivity extends SherlockFragmentActivity
if (intent.getAction() != null && intent.getAction().equals("android.intent.action.SENDTO")) {
Log.w("ConversationListActivity", "Intent has sendto action...");
try {
recipients = RecipientFactory.getRecipientsFromString(this, intent.getData().getSchemeSpecificPart());
recipients = RecipientFactory.getRecipientsFromString(this, intent.getData().getSchemeSpecificPart(), false);
thread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipients);
} catch (RecipientFormattingException rfe) {
recipients = null;

View File

@ -18,6 +18,7 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import android.database.Cursor;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
@ -37,9 +38,10 @@ import java.util.Set;
*
* @author Moxie Marlinspike
*/
public class ConversationListAdapter extends CursorAdapter {
public class ConversationListAdapter extends CursorAdapter {
private final Context context;
private final LayoutInflater inflater;
private final Set<Long> batchSet = Collections.synchronizedSet(new HashSet<Long>());
private boolean batchMode = false;
@ -47,21 +49,19 @@ public class ConversationListAdapter extends CursorAdapter {
public ConversationListAdapter(Context context, Cursor cursor) {
super(context, cursor);
this.context = context;
this.inflater = LayoutInflater.from(context);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
ConversationListItem view = new ConversationListItem(context, batchSet);
bindView(view, context, cursor);
return view;
return inflater.inflate(R.layout.conversation_list_item_view, parent, false);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.ID));
String recipientId = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_IDS));
Recipients recipients = RecipientFactory.getRecipientsForIds(context, recipientId);
Recipients recipients = RecipientFactory.getRecipientsForIds(context, recipientId, true);
long date = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE));
long count = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
@ -70,7 +70,7 @@ public class ConversationListAdapter extends CursorAdapter {
ThreadRecord thread = new ThreadRecord(context, recipients, date, count, read == 1, threadId);
setBody(cursor, thread);
((ConversationListItem)view).set(thread, batchMode);
((ConversationListItem)view).set(thread, batchSet, batchMode);
}
protected void filterBody(ThreadRecord thread, String body) {

View File

@ -22,6 +22,7 @@ import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.provider.Contacts.Intents;
import android.provider.ContactsContract.QuickContact;
import android.text.Spannable;
@ -30,7 +31,6 @@ import android.text.format.DateUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
@ -52,7 +52,9 @@ import java.util.Set;
* @author Moxie Marlinspike
*/
public class ConversationListItem extends RelativeLayout {
public class ConversationListItem extends RelativeLayout
implements Recipient.RecipientModifiedListener
{
private Context context;
private Set<Long> selectedThreads;
@ -62,18 +64,26 @@ public class ConversationListItem extends RelativeLayout {
private TextView fromView;
private TextView dateView;
private CheckBox checkbox;
private long count;
private boolean read;
private ImageView contactPhotoImage;
private QuickContactBadge contactPhotoBadge;
public ConversationListItem(Context context, Set<Long> selectedThreads) {
private final Handler handler = new Handler();
public ConversationListItem(Context context) {
super(context);
this.context = context;
}
LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
li.inflate(R.layout.conversation_list_item_view, this, true);
public ConversationListItem(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
}
this.context = context;
this.selectedThreads = selectedThreads;
@Override
protected void onFinishInflate() {
this.subjectView = (TextView)findViewById(R.id.subject);
this.fromView = (TextView)findViewById(R.id.from);
this.dateView = (TextView)findViewById(R.id.date);
@ -86,14 +96,14 @@ public class ConversationListItem extends RelativeLayout {
intializeListeners();
}
public ConversationListItem(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void set(ThreadRecord thread, Set<Long> selectedThreads, boolean batchMode) {
this.selectedThreads = selectedThreads;
this.recipients = thread.getRecipients();
this.threadId = thread.getThreadId();
this.count = thread.getCount();
this.read = thread.isRead();
public void set(ThreadRecord thread, boolean batchMode) {
this.recipients = thread.getRecipients();
this.threadId = thread.getThreadId();
this.fromView.setText(formatFrom(recipients, thread.getCount(), thread.isRead()));
this.fromView.setText(formatFrom(recipients, count, read));
if (thread.isKeyExchange())
this.subjectView.setText(R.string.ConversationListItem_key_exchange_message,
@ -114,6 +124,7 @@ public class ConversationListItem extends RelativeLayout {
else checkbox.setVisibility(View.GONE);
setContactPhoto(this.recipients.getPrimaryRecipient());
this.recipients.setListener(this);
}
private void intializeListeners() {
@ -188,4 +199,15 @@ public class ConversationListItem extends RelativeLayout {
else selectedThreads.remove(threadId);
}
}
@Override
public void onModified(Recipient recipient) {
handler.post(new Runnable() {
@Override
public void run() {
ConversationListItem.this.fromView.setText(formatFrom(recipients, count, read));
setContactPhoto(ConversationListItem.this.recipients.getPrimaryRecipient());
}
});
}
}

View File

@ -81,7 +81,7 @@ public class RecipientsPanel extends RelativeLayout {
public Recipients getRecipients() throws RecipientFormattingException {
String rawText = recipientsText.getText().toString();
Recipients recipients = RecipientFactory.getRecipientsFromString(getContext(), rawText);
Recipients recipients = RecipientFactory.getRecipientsFromString(getContext(), rawText, false);
if (recipients.isEmpty())
throw new RecipientFormattingException("Recipient List Is Empty!");

View File

@ -17,15 +17,6 @@
package org.thoughtcrime.securesms.contacts;
import java.util.ArrayList;
import java.util.List;
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.recipients.RecipientsFormatter;
import android.content.Context;
import android.telephony.PhoneNumberUtils;
import android.text.Annotation;
@ -39,11 +30,20 @@ import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MotionEvent;
import android.view.inputmethod.EditorInfo;
import android.widget.MultiAutoCompleteTextView;
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.recipients.RecipientsFormatter;
import java.util.ArrayList;
import java.util.List;
/**
* Provide UI for editing the recipients of multi-media messages.
*/
@ -52,7 +52,7 @@ public class RecipientsEditor extends MultiAutoCompleteTextView {
private final RecipientsEditorTokenizer mTokenizer;
private char mLastSeparator = ',';
private Context mContext;
public RecipientsEditor(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.autoCompleteTextViewStyle);
mContext = context;
@ -131,7 +131,7 @@ public class RecipientsEditor extends MultiAutoCompleteTextView {
public Recipients constructContactsFromInput() {
Recipients r = null;
try {
r = RecipientFactory.getRecipientsFromString(mContext, mTokenizer.getRawString() );
r = RecipientFactory.getRecipientsFromString(mContext, mTokenizer.getRawString(), false);
} catch (RecipientFormattingException e) {
Log.w( "RecipientsEditor", e);
}

View File

@ -1,6 +1,6 @@
/**
/**
* Copyright (C) 2011 Whisper Systems
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@ -10,15 +10,15 @@
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingMmsDatabase;
@ -38,13 +38,14 @@ import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.MultimediaMessagePdu;
import ws.com.google.android.mms.pdu.PduParser;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
/**
* A work queue for processing a number of encryption operations.
*
*
* @author Moxie Marlinspike
*/
@ -52,12 +53,12 @@ public class DecryptingQueue {
private static List<Runnable> workQueue = new LinkedList<Runnable>();
private static Thread workerThread;
static {
workerThread = new WorkerThread(workQueue, "Async Decryption Thread");
workerThread.start();
}
}
public static void scheduleDecryption(Context context, MasterSecret masterSecret, long messageId, long threadId, MultimediaMessagePdu mms) {
MmsDecryptionItem runnable = new MmsDecryptionItem(context, masterSecret, messageId, threadId, mms);
synchronized (workQueue) {
@ -65,7 +66,7 @@ public class DecryptingQueue {
workQueue.notifyAll();
}
}
public static void scheduleDecryption(Context context, MasterSecret masterSecret, long messageId, String originator, String body) {
DecryptionWorkItem runnable = new DecryptionWorkItem(context, masterSecret, messageId, body, originator);
synchronized (workQueue) {
@ -73,16 +74,16 @@ public class DecryptingQueue {
workQueue.notifyAll();
}
}
public static void schedulePendingDecrypts(Context context, MasterSecret masterSecret) {
Cursor cursor = null;
Log.w("DecryptingQueue", "Processing pending decrypts...");
try {
cursor = DatabaseFactory.getSmsDatabase(context).getDecryptInProgressMessages();
if (cursor == null || cursor.getCount() == 0 || !cursor.moveToFirst())
return;
do {
scheduleDecryptFromCursor(context, masterSecret, cursor);
} while (cursor.moveToNext());
@ -94,12 +95,12 @@ public class DecryptingQueue {
public static void scheduleRogueMessages(Context context, MasterSecret masterSecret, Recipient recipient) {
Cursor cursor = null;
try {
cursor = DatabaseFactory.getSmsDatabase(context).getEncryptedRogueMessages(recipient);
if (cursor == null || cursor.getCount() == 0 || !cursor.moveToFirst())
return;
do {
DatabaseFactory.getSmsDatabase(context).markAsDecrypting(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
scheduleDecryptFromCursor(context, masterSecret, cursor);
@ -107,9 +108,9 @@ public class DecryptingQueue {
} finally {
if (cursor != null)
cursor.close();
}
}
}
private static void scheduleDecryptFromCursor(Context context, MasterSecret masterSecret, Cursor cursor) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.ID));
String originator = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
@ -117,14 +118,14 @@ public class DecryptingQueue {
scheduleDecryption(context, masterSecret, id, originator, body);
}
private static class MmsDecryptionItem implements Runnable {
private long messageId;
private long threadId;
private Context context;
private MasterSecret masterSecret;
private MultimediaMessagePdu pdu;
public MmsDecryptionItem(Context context, MasterSecret masterSecret, long messageId, long threadId, MultimediaMessagePdu pdu) {
this.context = context;
this.masterSecret = masterSecret;
@ -132,7 +133,7 @@ public class DecryptingQueue {
this.threadId = threadId;
this.pdu = pdu;
}
private byte[] getEncryptedData() {
for (int i=0;i<pdu.getBody().getPartsNum();i++) {
Log.w("DecryptingQueue", "Content type (" + i + "): " + new String(pdu.getBody().getPart(i).getContentType()));
@ -140,16 +141,16 @@ public class DecryptingQueue {
return pdu.getBody().getPart(i).getData();
}
}
return null;
}
public void run() {
EncryptingMmsDatabase database = DatabaseFactory.getEncryptingMmsDatabase(context, masterSecret);
try {
String messageFrom = pdu.getFrom().getString();
Recipients recipients = RecipientFactory.getRecipientsFromString(context, messageFrom);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, messageFrom, false);
Recipient recipient = recipients.getPrimaryRecipient();
byte[] ciphertextPduBytes = getEncryptedData();
@ -164,42 +165,42 @@ public class DecryptingQueue {
database.markAsNoSession(messageId, threadId);
return;
}
byte[] plaintextPduBytes;
synchronized (SessionCipher.CIPHER_LOCK) {
Log.w("DecryptingQueue", "Decrypting: " + Hex.toString(ciphertextPduBytes));
SessionCipher cipher = new SessionCipher(context, masterSecret, recipient, new TextTransport());
SessionCipher cipher = new SessionCipher(context, masterSecret, recipient, new TextTransport());
plaintextPduBytes = cipher.decryptMessage(ciphertextPduBytes);
}
MultimediaMessagePdu plaintextPdu = (MultimediaMessagePdu)new PduParser(plaintextPduBytes).parse();
Log.w("DecryptingQueue", "Successfully decrypted MMS!");
database.insertSecureDecryptedMessageReceived(plaintextPdu, threadId);
database.delete(messageId);
database.delete(messageId);
} catch (RecipientFormattingException rfe) {
Log.w("DecryptingQueue", rfe);
database.markAsDecryptFailed(messageId, threadId);
} catch (InvalidMessageException ime) {
Log.w("DecryptingQueue", ime);
database.markAsDecryptFailed(messageId, threadId);
database.markAsDecryptFailed(messageId, threadId);
} catch (MmsException mme) {
Log.w("DecryptingQueue", mme);
database.markAsDecryptFailed(messageId, threadId);
database.markAsDecryptFailed(messageId, threadId);
}
}
}
private static class DecryptionWorkItem implements Runnable {
private long messageId;
private Context context;
private MasterSecret masterSecret;
private String body;
private String originator;
public DecryptionWorkItem(Context context, MasterSecret masterSecret, long messageId, String body, String originator) {
this.context = context;
this.messageId = messageId;
@ -207,7 +208,7 @@ public class DecryptingQueue {
this.body = body;
this.originator = originator;
}
private void handleRemoteAsymmetricEncrypt() {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
String plaintextBody;
@ -215,17 +216,17 @@ public class DecryptingQueue {
synchronized (SessionCipher.CIPHER_LOCK) {
try {
Log.w("DecryptingQueue", "Parsing recipient for originator: " + originator);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false);
Recipient recipient = recipients.getPrimaryRecipient();
Log.w("DecryptingQueue", "Parsed Recipient: " + recipient.getNumber());
if (!KeyUtil.isSessionFor(context, recipient)) {
Log.w("DecryptingQueue", "No such recipient session...");
database.markAsNoSession(messageId);
return;
}
SessionCipher cipher = new SessionCipher(context, masterSecret, recipient, new SmsTransportDetails());
SessionCipher cipher = new SessionCipher(context, masterSecret, recipient, new SmsTransportDetails());
plaintextBody = new String(cipher.decryptMessage(body.getBytes()));
} catch (InvalidMessageException e) {
Log.w("DecryptionQueue", e);
@ -240,11 +241,11 @@ public class DecryptingQueue {
database.updateSecureMessageBody(masterSecret, messageId, plaintextBody);
}
private void handleLocalAsymmetricEncrypt() {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
String plaintextBody;
try {
AsymmetricMasterCipher asymmetricMasterCipher = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret));
String encryptedBody = body.substring(Prefix.ASYMMETRIC_LOCAL_ENCRYPT.length());
@ -258,15 +259,15 @@ public class DecryptingQueue {
database.markAsDecryptFailed(messageId);
return;
}
database.updateMessageBody(masterSecret, messageId, plaintextBody);
}
public void run() {
public void run() {
if (body.startsWith(Prefix.ASYMMETRIC_ENCRYPT)) handleRemoteAsymmetricEncrypt();
else if (body.startsWith(Prefix.ASYMMETRIC_LOCAL_ENCRYPT)) handleLocalAsymmetricEncrypt();
}
}
}

View File

@ -22,6 +22,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import java.util.Collections;
import java.util.HashMap;
@ -44,8 +45,9 @@ public class CanonicalAddressDatabase {
private static CanonicalAddressDatabase instance;
private final DatabaseHelper databaseHelper;
private final HashMap<String,Long> addressCache = new HashMap<String,Long>();
private final Map<String,String> idCache = Collections.synchronizedMap(new HashMap<String,String>());
private final Map<String,Long> addressCache = Collections.synchronizedMap(new HashMap<String,Long>());
private final Map<String,String> idCache = Collections.synchronizedMap(new HashMap<String,String>());
public static CanonicalAddressDatabase getInstance(Context context) {
synchronized (lock) {
@ -58,18 +60,45 @@ public class CanonicalAddressDatabase {
private CanonicalAddressDatabase(Context context) {
databaseHelper = new DatabaseHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
fillCache();
}
private void fillCache() {
Cursor cursor = null;
try {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
cursor = db.query(TABLE, null, null, null, null, null, null);
while (cursor != null && cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID_COLUMN));
String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS_COLUMN));
if (address == null || address.trim().length() == 0)
address = "Anonymous";
idCache.put(id+"", address);
addressCache.put(address, id);
}
} finally {
if (cursor != null)
cursor.close();
}
}
public String getAddressFromId(String id) {
if (id == null || id.trim().equals("")) return "Anonymous";
String cachedAddress = idCache.get(id);
if (cachedAddress != null)
return cachedAddress;
Cursor cursor = null;
try {
Log.w("CanonicalAddressDatabase", "Hitting DB on query [ID].");
SQLiteDatabase db = databaseHelper.getReadableDatabase();
cursor = db.query(TABLE, null, ID_COLUMN + " = ?", new String[] {id+""}, null, null, null);
@ -127,7 +156,7 @@ public class CanonicalAddressDatabase {
private long getCanonicalAddressFromDatabase(String address) {
Cursor cursor = null;
try {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String[] selectionArguments = new String[] {address};
cursor = db.query(TABLE, ID_PROJECTION, SELECTION, selectionArguments, null, null, null);

View File

@ -140,7 +140,7 @@ public class MmsDatabase extends Database {
try {
EncodedStringValue encodedString = headers.getEncodedStringValue(PduHeaders.FROM);
String fromString = new String(encodedString.getTextString(), CharacterSets.MIMENAME_ISO_8859_1);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, fromString);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, fromString, false);
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);

View File

@ -84,7 +84,7 @@ public class SmsDatabase extends Database {
private long insertMessageReceived(SmsMessage message, String body, long type) {
List<Recipient> recipientList = new ArrayList<Recipient>(1);
recipientList.add(new Recipient(null, message.getDisplayOriginatingAddress(), null));
recipientList.add(new Recipient(null, message.getDisplayOriginatingAddress(), null, null));
Recipients recipients = new Recipients(recipientList);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);

View File

@ -128,7 +128,7 @@ public class SmsMigrator {
try {
if (sb.length() == 0) return null;
else return RecipientFactory.getRecipientsFromString(context, sb.toString());
else return RecipientFactory.getRecipientsFromString(context, sb.toString(), true);
} catch (RecipientFormattingException rfe) {
Log.w("SmsMigrator", rfe);
return null;

View File

@ -0,0 +1,69 @@
package org.thoughtcrime.securesms.recipients;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import org.thoughtcrime.securesms.R;
import java.io.InputStream;
public class ContactPhotoFactory {
private static final Object defaultPhotoLock = new Object();
private static final Object localUserLock = new Object();
private static Bitmap defaultContactPhoto;
private static Bitmap localUserContactPhoto;
private static final String[] CONTENT_URI_PROJECTION = new String[] {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.LOOKUP_KEY
};
public static Bitmap getDefaultContactPhoto(Context context) {
synchronized (defaultPhotoLock) {
if (defaultContactPhoto == null)
defaultContactPhoto = BitmapFactory.decodeResource(context.getResources(),
R.drawable.ic_contact_picture);
}
return defaultContactPhoto;
}
public static Bitmap getLocalUserContactPhoto(Context context, Uri uri) {
synchronized (localUserLock) {
if (localUserContactPhoto == null) {
Cursor cursor = context.getContentResolver().query(uri, CONTENT_URI_PROJECTION,
null, null, null);
if (cursor != null && cursor.moveToFirst()) {
localUserContactPhoto = getContactPhoto(context, Uri.withAppendedPath(Contacts.CONTENT_URI,
cursor.getLong(0) + ""));
} else {
localUserContactPhoto = getDefaultContactPhoto(context);
}
}
}
return localUserContactPhoto;
}
public static void clearCache() {
synchronized (localUserLock) {
localUserContactPhoto = null;
}
}
private static Bitmap getContactPhoto(Context context, Uri uri) {
InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), uri);
if (inputStream == null) return ContactPhotoFactory.getDefaultContactPhoto(context);
else return BitmapFactory.decodeStream(inputStream);
}
}

View File

@ -21,6 +21,10 @@ import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import org.thoughtcrime.securesms.recipients.RecipientProvider.RecipientDetails;
import java.util.concurrent.atomic.AtomicReference;
public class Recipient implements Parcelable {
public static final Parcelable.Creator<Recipient> CREATOR = new Parcelable.Creator<Recipient>() {
@ -33,35 +37,69 @@ public class Recipient implements Parcelable {
}
};
private final String name;
private final AtomicReference<String> name = new AtomicReference<String>(null);
private final AtomicReference<Bitmap> contactPhoto = new AtomicReference<Bitmap>(null);
private final AtomicReference<Uri> contactUri = new AtomicReference<Uri>(null);
private final String number;
private Uri contactUri;
private Bitmap contactPhoto;
private RecipientModifiedListener listener;
private boolean asynchronousUpdateComplete = false;
// public Recipient(String name, String number, Uri contactUri, Bitmap contactPhoto) {
// this(name, number, contactPhoto);
// this.contactUri = contactUri;
// }
// public Recipient(String number, Bitmap contactPhoto,
// ListenableFutureTask<RecipientDetails> future)
// {
// this.number = number;
// this.contactUri = null;
// this.contactPhoto.set(contactPhoto);
//
// future.setListener(new FutureTaskListener<RecipientDetails>() {
// @Override
// public void onSuccess(RecipientDetails result) {
// if (result != null) {
// Recipient.this.name.set(result.name);
// Recipient.this.contactPhoto.set(result.avatar);
//
// synchronized(this) {
// if (listener == null) asynchronousUpdateComplete = true;
// else listener.onModified(Recipient.this);
// }
// }
// }
//
// @Override
// public void onFailure(Throwable error) {
// Log.w("Recipient", error);
// }
// });
// }
public Recipient(String name, String number, Uri contactUri, Bitmap contactPhoto) {
this(name, number, contactPhoto);
this.contactUri = contactUri;
}
public Recipient(String name, String number, Bitmap contactPhoto) {
this.name = name;
this.number = number;
this.contactPhoto = contactPhoto;
this.number = number;
this.contactUri.set(contactUri);
this.name.set(name);
this.contactPhoto.set(contactPhoto);
}
public Recipient(Parcel in) {
this.name = in.readString();
this.number = in.readString();
this.contactUri = in.readParcelable(null);
this.contactPhoto = in.readParcelable(null);
this.number = in.readString();
this.name.set(in.readString());
this.contactUri.set((Uri)in.readParcelable(null));
this.contactPhoto.set((Bitmap)in.readParcelable(null));
}
public Uri getContactUri() {
return this.contactUri;
return this.contactUri.get();
}
public String getName() {
return name;
return name.get();
}
public String getNumber() {
@ -72,20 +110,44 @@ public class Recipient implements Parcelable {
return 0;
}
public void updateAsynchronousContent(RecipientDetails result) {
if (result != null) {
Recipient.this.name.set(result.name);
Recipient.this.contactUri.set(result.contactUri);
Recipient.this.contactPhoto.set(result.avatar);
synchronized(this) {
if (listener == null) asynchronousUpdateComplete = true;
else listener.onModified(Recipient.this);
}
}
}
public synchronized void setListener(RecipientModifiedListener listener) {
this.listener = listener;
if (asynchronousUpdateComplete) {
if (listener != null)
listener.onModified(this);
asynchronousUpdateComplete = false;
}
}
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeString(number);
dest.writeParcelable(contactUri, 0);
dest.writeParcelable(contactPhoto, 0);
dest.writeString(name.get());
dest.writeParcelable(contactUri.get(), 0);
dest.writeParcelable(contactPhoto.get(), 0);
}
public String toShortString() {
return (name == null ? number : name);
return (name.get() == null ? number : name.get());
}
public Bitmap getContactPhoto() {
return contactPhoto;
return contactPhoto.get();
}
public static interface RecipientModifiedListener {
public void onModified(Recipient recipient);
}
}

View File

@ -17,44 +17,19 @@
package org.thoughtcrime.securesms.recipients;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.util.NumberUtil;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
public class RecipientFactory {
private static final Map<String,Recipient> recipientCache = Collections.synchronizedMap(new LRUHashMap<String,Recipient>());
private static final Map<String,Recipient> recipientIdCache = Collections.synchronizedMap(new LRUHashMap<String,Recipient>());
private static final Map<Uri,Recipient> recipientUriCache = Collections.synchronizedMap(new HashMap<Uri,Recipient>());
private static final RecipientProvider provider = new RecipientProvider();
public static RecipientProvider getRecipientProvider() {
return provider;
}
public static Recipient getRecipientForUri(Context context, Uri uri) {
Recipient recipient = recipientUriCache.get(uri);
if (recipient == null)
recipient = getRecipientFromProvider(context, uri);
return recipient;
}
public static Recipients getRecipientsForIds(Context context, String recipientIds) {
public static Recipients getRecipientsForIds(Context context, String recipientIds, boolean asynchronous) {
if (recipientIds == null || recipientIds.trim().length() == 0)
return new Recipients(new LinkedList<Recipient>());
@ -63,14 +38,7 @@ public class RecipientFactory {
while (tokenizer.hasMoreTokens()) {
String recipientId = tokenizer.nextToken();
Recipient recipient = recipientIdCache.get(recipientId);
if (recipient == null)
recipient = getRecipientFromProviderId(context, recipientId);
if (recipient == null)
recipient = getNullIdRecipient(context, recipientId);
Recipient recipient = getRecipientFromProviderId(context, recipientId, asynchronous);
results.add(recipient);
}
@ -78,24 +46,18 @@ public class RecipientFactory {
return new Recipients(results);
}
private static Recipient getRecipientForNumber(Context context, String number) {
Recipient recipient = recipientCache.get(number);
if (recipient == null)
recipient = getRecipientFromProvider(context, number);
if (recipient == null)
recipient = getNullRecipient(context, number);
return recipient;
private static Recipient getRecipientForNumber(Context context, String number, boolean asynchronous) {
return provider.getRecipient(context, number, asynchronous);
}
public static Recipients getRecipientsFromString(Context context, String rawText) throws RecipientFormattingException {
public static Recipients getRecipientsFromString(Context context, String rawText, boolean asynchronous)
throws RecipientFormattingException
{
List<Recipient> results = new LinkedList<Recipient>();
StringTokenizer tokenizer = new StringTokenizer(rawText, ",");
while (tokenizer.hasMoreTokens()) {
Recipient recipient = parseRecipient(context, tokenizer.nextToken());
Recipient recipient = parseRecipient(context, tokenizer.nextToken(), asynchronous);
if( recipient != null )
results.add(recipient);
}
@ -103,45 +65,9 @@ public class RecipientFactory {
return new Recipients(results);
}
private static Recipient getNullIdRecipient(Context context, String recipientId) {
String address = DatabaseFactory.getAddressDatabase(context).getAddressFromId(recipientId);
Recipient recipient = getNullRecipient(context, address);
recipientIdCache.put(recipientId, recipient);
return recipient;
}
private static Recipient getNullRecipient(Context context, String number) {
Recipient nullRecipient = new Recipient(null, number, BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_contact_picture));
recipientCache.put(number, nullRecipient);
return nullRecipient;
}
private static Recipient getRecipientFromProviderId(Context context, String recipientId) {
Log.w("RecipientFactory", "Hitting recipient provider [ID].");
String address = DatabaseFactory.getAddressDatabase(context).getAddressFromId(recipientId);
Recipient recipient = getRecipientFromProvider(context, address);
recipientIdCache.put(recipientId, recipient);
return recipient;
}
private static Recipient getRecipientFromProvider(Context context, Uri uri) {
Recipient recipient = provider.getRecipient(context, uri);
if (recipient != null)
recipientUriCache.put(uri, recipient);
return recipient;
}
private static Recipient getRecipientFromProvider(Context context, String number) {
Recipient recipient = provider.getRecipient(context, number);
if (recipient != null)
recipientCache.put(number, recipient);
return recipient;
private static Recipient getRecipientFromProviderId(Context context, String recipientId, boolean asynchronous) {
String number = DatabaseFactory.getAddressDatabase(context).getAddressFromId(recipientId);
return getRecipientForNumber(context, number, asynchronous);
}
private static boolean hasBracketedNumber(String recipient) {
@ -151,7 +77,9 @@ public class RecipientFactory {
(recipient.indexOf('>', openBracketIndex) != -1);
}
private static String parseBracketedNumber(String recipient) throws RecipientFormattingException {
private static String parseBracketedNumber(String recipient)
throws RecipientFormattingException
{
int begin = recipient.indexOf('<');
int end = recipient.indexOf('>', begin);
String value = recipient.substring(begin + 1, end);
@ -162,32 +90,26 @@ public class RecipientFactory {
throw new RecipientFormattingException("Bracketed value: " + value + " is not valid.");
}
private static Recipient parseRecipient(Context context, String recipient) throws RecipientFormattingException {
private static Recipient parseRecipient(Context context, String recipient, boolean asynchronous)
throws RecipientFormattingException
{
recipient = recipient.trim();
if( recipient.length() == 0 )
return null;
if (hasBracketedNumber(recipient))
return getRecipientForNumber(context, parseBracketedNumber(recipient));
return getRecipientForNumber(context, parseBracketedNumber(recipient), asynchronous);
if (NumberUtil.isValidSmsOrEmail(recipient))
return getRecipientForNumber(context, recipient);
return getRecipientForNumber(context, recipient, asynchronous);
throw new RecipientFormattingException("Recipient: " + recipient + " is badly formatted.");
}
public static void clearCache() {
recipientCache.clear();
recipientIdCache.clear();
recipientUriCache.clear();
ContactPhotoFactory.clearCache();
provider.clearCache();
}
private static class LRUHashMap<K,V> extends LinkedHashMap<K,V> {
private static final int MAX_SIZE = 1000;
@Override
protected boolean removeEldestEntry (Map.Entry<K,V> eldest) {
return size() > MAX_SIZE;
}
}
}

View File

@ -21,17 +21,22 @@ import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.PhoneLookup;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.LRUCache;
import java.io.InputStream;
import java.util.Collections;
import java.util.Map;
public class RecipientProvider {
private static Bitmap defaultContactPhoto;
private static final Map<String,Recipient> recipientCache = Collections.synchronizedMap(new LRUCache<String,Recipient>(1000));
// private static final ExecutorService asyncRecipientResolver = Executors.newSingleThreadExecutor();
private static final String[] CALLER_ID_PROJECTION = new String[] {
PhoneLookup.DISPLAY_NAME,
@ -39,41 +44,107 @@ public class RecipientProvider {
PhoneLookup._ID,
};
private static final String[] CONTENT_URI_PROJECTION = new String[] {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.LOOKUP_KEY
};
public Recipient getRecipient(Context context, String number, boolean asynchronous) {
Recipient cachedRecipient = recipientCache.get(number);
public Recipient getRecipient(Context context, Uri uri) {
Cursor cursor = context.getContentResolver().query(uri, CONTENT_URI_PROJECTION, null, null, null);
try {
if (cursor.moveToFirst()) {
long rowId = cursor.getLong(0);
Uri contactUri = Contacts.getLookupUri(rowId, cursor.getString(2));
Bitmap contactPhoto = getContactPhoto(context, Uri.withAppendedPath(Contacts.CONTENT_URI,
rowId+""));
String displayName = cursor.getString(1);
cursor.close();
cursor = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, new String[] {ContactsContract.CommonDataKinds.Phone.NUMBER}, ContactsContract.CommonDataKinds.Phone.CONTACT_ID +" = ?", new String[] {rowId+""}, null);
if (cursor.moveToFirst())
return new Recipient(displayName, cursor.getString(0), contactUri, contactPhoto);
else
return new Recipient(displayName, null, contactUri, contactPhoto);
}
} finally {
cursor.close();
}
return null;
if (cachedRecipient != null) return cachedRecipient;
else if (asynchronous) return getAsynchronousRecipient(context, number);
else return getSynchronousRecipient(context, number);
}
public Recipient getRecipient(Context context, String number) {
private Recipient getSynchronousRecipient(Context context, String number) {
Log.w("RecipientProvider", "Cache miss [SYNC]!");
RecipientDetails details = getRecipientDetails(context, number);
Recipient recipient;
if (details != null) {
recipient = new Recipient(details.name, number, details.contactUri, details.avatar);
} else {
recipient = new Recipient(null, number, null, ContactPhotoFactory.getDefaultContactPhoto(context));
}
recipientCache.put(number, recipient);
return recipient;
}
private Recipient getAsynchronousRecipient(final Context context, final String number) {
Log.w("RecipientProvider", "Cache miss [ASYNC]!");
Recipient recipient = new Recipient(null, number, null, ContactPhotoFactory.getDefaultContactPhoto(context));
recipientCache.put(number, recipient);
new AsyncTask<Recipient, Void, RecipientDetails>() {
private Recipient recipient;
@Override
protected RecipientDetails doInBackground(Recipient... recipient) {
this.recipient = recipient[0];
return getRecipientDetails(context, number);
}
@Override
protected void onPostExecute(RecipientDetails result) {
recipient.updateAsynchronousContent(result);
}
}.execute(recipient);
return recipient;
// ListenableFutureTask<RecipientDetails> future =
// new ListenableFutureTask<RecipientDetails>(new Callable<RecipientDetails>() {
// @Override
// public RecipientDetails call() throws Exception {
// return getRecipientDetails(context, number);
//// RecipientDetails recipientDetails = getRecipientDetails();
////
//// if (recipientDeta)
////
//// Recipient cachedRecipient = recipientCache.get(number);
////
//// if (cachedRecipient != null) {
//// return new RecipientDetails(cachedRecipient.getName(), cachedRecipient.getContactPhoto());
//// }
////
//// Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
//// Cursor cursor = context.getContentResolver().query(uri, CALLER_ID_PROJECTION,
//// null, null, null);
////
//// try {
//// if (cursor != null && cursor.moveToFirst()) {
//// Uri contactUri = Contacts.getLookupUri(cursor.getLong(2), cursor.getString(1));
//// Bitmap contactPhoto = getContactPhoto(context, Uri.withAppendedPath(Contacts.CONTENT_URI,
//// cursor.getLong(2)+""));
////
//// recipientCache.put(number, new Recipient(cursor.getString(0), number, contactPhoto));
//// return new RecipientDetails(cursor.getString(0), contactPhoto);
//// } else {
//// recipientCache.put(number, new Recipient(null, number, ContactPhotoFactory.getDefaultContactPhoto(context)));
//// }
//// } finally {
//// if (cursor != null)
//// cursor.close();
//// }
////
//// return null;
// }
// }, null);
//
// asyncRecipientResolver.submit(future);
// Recipient recipient = new Recipient(number, ContactPhotoFactory.getDefaultContactPhoto(context), future);
// recipientCache.put(number, recipient);
//
// return recipient;
//// return new Recipient(null, number, ContactPhotoFactory.getDefaultContactPhoto(context));
}
public void clearCache() {
recipientCache.clear();
}
private RecipientDetails getRecipientDetails(Context context, String number) {
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
Cursor cursor = context.getContentResolver().query(uri, CALLER_ID_PROJECTION, null, null, null);
Cursor cursor = context.getContentResolver().query(uri, CALLER_ID_PROJECTION,
null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
@ -81,8 +152,7 @@ public class RecipientProvider {
Bitmap contactPhoto = getContactPhoto(context, Uri.withAppendedPath(Contacts.CONTENT_URI,
cursor.getLong(2)+""));
Recipient recipient = new Recipient(cursor.getString(0), number, contactUri, contactPhoto);
return recipient;
return new RecipientDetails(cursor.getString(0), contactUri, contactPhoto);
}
} finally {
if (cursor != null)
@ -92,22 +162,25 @@ public class RecipientProvider {
return null;
}
public Bitmap getDefaultContactPhoto(Context context) {
synchronized (this) {
if (defaultContactPhoto == null)
defaultContactPhoto = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_contact_picture);
}
return defaultContactPhoto;
}
private Bitmap getContactPhoto(Context context, Uri uri) {
InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), uri);
if (inputStream == null)
return getDefaultContactPhoto(context);
return ContactPhotoFactory.getDefaultContactPhoto(context);
else
return BitmapFactory.decodeStream(inputStream);
}
public static class RecipientDetails {
public final String name;
public final Bitmap avatar;
public final Uri contactUri;
public RecipientDetails(String name, Uri contactUri, Bitmap avatar) {
this.name = name;
this.avatar = avatar;
this.contactUri = contactUri;
}
}
}

View File

@ -21,6 +21,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import org.thoughtcrime.securesms.crypto.KeyUtil;
import org.thoughtcrime.securesms.recipients.Recipient.RecipientModifiedListener;
import org.thoughtcrime.securesms.util.NumberUtil;
import java.util.ArrayList;
@ -68,6 +69,12 @@ public class Recipients implements Parcelable {
return this;
}
public void setListener(RecipientModifiedListener listener) {
for (Recipient recipient : recipients) {
recipient.setListener(listener);
}
}
public boolean isEmailRecipient() {
for (Recipient recipient : recipients) {
if (NumberUtil.isValidEmail(recipient.getNumber()))

View File

@ -94,13 +94,13 @@ public class MessageNotifier {
private static Recipients getSmsRecipient(Context context, Cursor c) throws RecipientFormattingException {
String address = c.getString(c.getColumnIndexOrThrow(SmsDatabase.ADDRESS));
return RecipientFactory.getRecipientsFromString(context, address);
return RecipientFactory.getRecipientsFromString(context, address, false);
}
private static Recipients getMmsRecipient(Context context, Cursor c) throws RecipientFormattingException {
long messageId = c.getLong(c.getColumnIndexOrThrow(MmsDatabase.ID));
String address = DatabaseFactory.getMmsDatabase(context).getMessageRecipient(messageId);
return RecipientFactory.getRecipientsFromString(context, address);
return RecipientFactory.getRecipientsFromString(context, address, false);
}
private static Recipients getMostRecentRecipients(Context context, Cursor c) {

View File

@ -108,7 +108,7 @@ public class MmsSender extends MmscProcessor {
private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipient, byte[] pduBytes) {
synchronized (SessionCipher.CIPHER_LOCK) {
SessionCipher cipher = new SessionCipher(context, masterSecret, new Recipient(null, recipient, null), new TextTransport());
SessionCipher cipher = new SessionCipher(context, masterSecret, new Recipient(null, recipient, null, null), new TextTransport());
return cipher.encryptMessage(pduBytes);
}
}

View File

@ -102,7 +102,7 @@ public class SmsReceiver {
private void storeKeyExchangeMessage(MasterSecret masterSecret, SmsMessage message, String messageBody) {
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(ApplicationPreferencesActivity.AUTO_KEY_EXCHANGE_PREF, true)) {
try {
Recipient recipient = new Recipient(null, message.getDisplayOriginatingAddress(), null);
Recipient recipient = new Recipient(null, message.getDisplayOriginatingAddress(), null, null);
KeyExchangeMessage keyExchangeMessage = new KeyExchangeMessage(messageBody);
KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipient);

View File

@ -175,7 +175,7 @@ public class SmsSender {
private String getAsymmetricEncrypt(MasterSecret masterSecret, String body, String address) {
synchronized (SessionCipher.CIPHER_LOCK) {
SessionCipher cipher = new SessionCipher(context, masterSecret, new Recipient(null, address, null), new SmsTransportDetails());
SessionCipher cipher = new SessionCipher(context, masterSecret, new Recipient(null, address, null, null), new SmsTransportDetails());
return new String(cipher.encryptMessage(body.getBytes()));
}
}

View File

@ -0,0 +1,6 @@
package org.thoughtcrime.securesms.util;
public interface FutureTaskListener<V> {
public void onSuccess(V result);
public void onFailure(Throwable error);
}

View File

@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.util;
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCache<K,V> extends LinkedHashMap<K,V> {
private final int maxSize;
public LRUCache(int maxSize) {
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry (Map.Entry<K,V> eldest) {
return size() > maxSize;
}
}

View File

@ -0,0 +1,39 @@
package org.thoughtcrime.securesms.util;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ListenableFutureTask<V> extends FutureTask<V> {
private FutureTaskListener<V> listener;
public ListenableFutureTask(Callable<V> callable, FutureTaskListener<V> listener) {
super(callable);
this.listener = listener;
}
public synchronized void setListener(FutureTaskListener<V> listener) {
this.listener = listener;
if (this.isDone()) {
callback();
}
}
@Override
protected synchronized void done() {
callback();
}
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);
}
}
}
}