mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-03 08:22:39 +00:00
Better UX handling on identity key mismatches.
1) Migrate from GSON to Jackson everywhere. 2) Add support for storing identity key conflicts on message rows. 3) Add limited support for surfacing identity key conflicts in UI.
This commit is contained in:
@@ -72,9 +72,7 @@ public class ApplicationContext extends Application implements DependencyInjecto
|
||||
return jobManager;
|
||||
}
|
||||
|
||||
|
||||
private void initializeRandomNumberFix() {
|
||||
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
|
||||
PRNGFixes.apply();
|
||||
}
|
||||
|
||||
|
||||
216
src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java
Normal file
216
src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java
Normal file
@@ -0,0 +1,216 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsAddressDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.PushDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
|
||||
import org.whispersystems.textsecure.internal.push.PushMessageProtos;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ConfirmIdentityDialog extends AlertDialog {
|
||||
|
||||
private static final String TAG = ConfirmIdentityDialog.class.getSimpleName();
|
||||
|
||||
private OnClickListener callback;
|
||||
|
||||
public ConfirmIdentityDialog(Context context,
|
||||
MasterSecret masterSecret,
|
||||
MessageRecord messageRecord,
|
||||
IdentityKeyMismatch mismatch)
|
||||
{
|
||||
super(context);
|
||||
Recipient recipient = RecipientFactory.getRecipientForId(context, mismatch.getRecipientId(), false);
|
||||
String name = recipient.toShortString();
|
||||
String introduction = String.format(context.getString(R.string.ConfirmIdentityDialog_the_signature_on_this_key_exchange_is_different), name, name);
|
||||
SpannableString spannableString = new SpannableString(introduction + " " +
|
||||
context.getString(R.string.ConfirmIdentityDialog_you_may_wish_to_verify_this_contact));
|
||||
|
||||
spannableString.setSpan(new VerifySpan(context, masterSecret, mismatch),
|
||||
introduction.length()+1, spannableString.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
setTitle(name);
|
||||
setMessage(spannableString);
|
||||
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, "Accept", new AcceptListener(masterSecret, messageRecord, mismatch));
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, "Cancel", new CancelListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
super.show();
|
||||
((TextView)this.findViewById(android.R.id.message))
|
||||
.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
public void setCallback(OnClickListener callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
private class AcceptListener implements OnClickListener {
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
private final MessageRecord messageRecord;
|
||||
private final IdentityKeyMismatch mismatch;
|
||||
|
||||
private AcceptListener(MasterSecret masterSecret, MessageRecord messageRecord, IdentityKeyMismatch mismatch) {
|
||||
this.masterSecret = masterSecret;
|
||||
this.messageRecord = messageRecord;
|
||||
this.mismatch = mismatch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
new AsyncTask<Void, Void, Void>()
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
|
||||
|
||||
identityDatabase.saveIdentity(masterSecret,
|
||||
mismatch.getRecipientId(),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
processMessageRecord(messageRecord);
|
||||
processPendingMessageRecords(messageRecord.getThreadId(), mismatch);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void processMessageRecord(MessageRecord messageRecord) {
|
||||
if (messageRecord.isOutgoing()) processOutgoingMessageRecord(messageRecord);
|
||||
else processIncomingMessageRecord(messageRecord);
|
||||
}
|
||||
|
||||
private void processPendingMessageRecords(long threadId, IdentityKeyMismatch mismatch) {
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(getContext());
|
||||
Cursor cursor = mmsSmsDatabase.getIdentityConflictMessagesForThread(threadId);
|
||||
MmsSmsDatabase.Reader reader = mmsSmsDatabase.readerFor(cursor, masterSecret);
|
||||
MessageRecord record;
|
||||
|
||||
try {
|
||||
while ((record = reader.getNext()) != null) {
|
||||
for (IdentityKeyMismatch recordMismatch : record.getIdentityKeyMismatches()) {
|
||||
if (mismatch.equals(recordMismatch)) {
|
||||
processMessageRecord(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (reader != null)
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
|
||||
MmsAddressDatabase mmsAddressDatabase = DatabaseFactory.getMmsAddressDatabase(getContext());
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getRecipientId(),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
Recipients recipients = mmsAddressDatabase.getRecipientsForId(messageRecord.getId());
|
||||
|
||||
if (recipients.isGroupRecipient()) MessageSender.resendGroupMessage(getContext(), masterSecret, messageRecord, mismatch.getRecipientId());
|
||||
else MessageSender.resend(getContext(), masterSecret, messageRecord);
|
||||
} else {
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getRecipientId(),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
MessageSender.resend(getContext(), masterSecret, messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
private void processIncomingMessageRecord(MessageRecord messageRecord) {
|
||||
try {
|
||||
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext());
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
|
||||
|
||||
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
|
||||
mismatch.getRecipientId(),
|
||||
mismatch.getIdentityKey());
|
||||
|
||||
TextSecureEnvelope envelope = new TextSecureEnvelope(PushMessageProtos.IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE,
|
||||
messageRecord.getIndividualRecipient().getNumber(),
|
||||
messageRecord.getRecipientDeviceId(), "",
|
||||
messageRecord.getDateSent(),
|
||||
Base64.decode(messageRecord.getBody().getBody()));
|
||||
|
||||
long pushId = pushDatabase.insert(envelope);
|
||||
|
||||
ApplicationContext.getInstance(getContext())
|
||||
.getJobManager()
|
||||
.add(new PushDecryptJob(getContext(), pushId, messageRecord.getId(),
|
||||
messageRecord.getIndividualRecipient().getNumber()));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}.execute();
|
||||
|
||||
if (callback != null) callback.onClick(null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private class CancelListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (callback != null) callback.onClick(null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static class VerifySpan extends ClickableSpan {
|
||||
private final Context context;
|
||||
private final MasterSecret masterSecret;
|
||||
private final IdentityKeyMismatch mismatch;
|
||||
|
||||
private VerifySpan(Context context, MasterSecret masterSecret, IdentityKeyMismatch mismatch) {
|
||||
this.context = context;
|
||||
this.masterSecret = masterSecret;
|
||||
this.mismatch = mismatch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
Intent intent = new Intent(context, VerifyIdentityActivity.class);
|
||||
intent.putExtra("recipient", mismatch.getRecipientId());
|
||||
intent.putExtra("master_secret", masterSecret);
|
||||
intent.putExtra("remote_identity", new IdentityKeyParcelable(mismatch.getIdentityKey()));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -62,7 +62,6 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re
|
||||
private final Set<MessageRecord> batchSelected = Collections.synchronizedSet(new HashSet<MessageRecord>());
|
||||
|
||||
private final SelectionClickListener selectionClickListener;
|
||||
private final Handler failedIconClickHandler;
|
||||
private final Context context;
|
||||
private final MasterSecret masterSecret;
|
||||
private final boolean groupThread;
|
||||
@@ -70,13 +69,12 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
public ConversationAdapter(Context context, MasterSecret masterSecret, SelectionClickListener selectionClickListener,
|
||||
Handler failedIconClickHandler, boolean groupThread, boolean pushDestination)
|
||||
boolean groupThread, boolean pushDestination)
|
||||
{
|
||||
super(context, null, 0);
|
||||
this.context = context;
|
||||
this.masterSecret = masterSecret;
|
||||
this.selectionClickListener = selectionClickListener;
|
||||
this.failedIconClickHandler = failedIconClickHandler;
|
||||
this.groupThread = groupThread;
|
||||
this.pushDestination = pushDestination;
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
@@ -90,7 +88,7 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re
|
||||
MessageRecord messageRecord = getMessageRecord(id, cursor, type);
|
||||
|
||||
item.set(masterSecret, messageRecord, batchSelected, selectionClickListener,
|
||||
failedIconClickHandler, groupThread, pushDestination);
|
||||
groupThread, pushDestination);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.v4.app.ListFragment;
|
||||
import android.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.Loader;
|
||||
@@ -16,7 +15,6 @@ import android.support.v4.widget.CursorAdapter;
|
||||
import android.support.v7.app.ActionBarActivity;
|
||||
import android.support.v7.view.ActionMode;
|
||||
import android.text.ClipboardManager;
|
||||
import android.text.format.DateFormat;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -30,6 +28,7 @@ import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
@@ -44,8 +43,6 @@ import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
|
||||
import java.sql.Date;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -103,7 +100,6 @@ public class ConversationFragment extends ListFragment
|
||||
private void initializeListAdapter() {
|
||||
if (this.recipients != null && this.threadId != -1) {
|
||||
this.setListAdapter(new ConversationAdapter(getActivity(), masterSecret, selectionClickListener,
|
||||
new FailedIconClickHandler(),
|
||||
(!this.recipients.isSingleRecipient()) || this.recipients.isGroupRecipient(),
|
||||
DirectoryHelper.isPushDestination(getActivity(), this.recipients)));
|
||||
getListView().setRecyclerListener((ConversationAdapter)getListAdapter());
|
||||
@@ -218,46 +214,11 @@ public class ConversationFragment extends ListFragment
|
||||
}
|
||||
|
||||
private void handleDisplayDetails(MessageRecord message) {
|
||||
long dateReceived = message.getDateReceived();
|
||||
long dateSent = message.getDateSent();
|
||||
|
||||
String transport;
|
||||
|
||||
if (message.isPending()) transport = getString(R.string.ConversationFragment_pending);
|
||||
else if (message.isPush()) transport = getString(R.string.ConversationFragment_push);
|
||||
else if (message.isMms()) transport = getString(R.string.ConversationFragment_mms);
|
||||
else transport = getString(R.string.ConversationFragment_sms);
|
||||
|
||||
String dateFormatPattern;
|
||||
|
||||
if (DateFormat.is24HourFormat(getActivity().getApplicationContext())) {
|
||||
dateFormatPattern = "EEE MMM d, yyyy '-' HH:mm:ss zzz";
|
||||
} else {
|
||||
dateFormatPattern = "EEE MMM d, yyyy '-' hh:mm:ss a zzz";
|
||||
}
|
||||
|
||||
SimpleDateFormat dateFormatter = new SimpleDateFormat(dateFormatPattern);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(R.string.ConversationFragment_message_details);
|
||||
builder.setIcon(Dialogs.resolveIcon(getActivity(), R.attr.dialog_info_icon));
|
||||
builder.setCancelable(true);
|
||||
|
||||
if (dateReceived == dateSent || message.isOutgoing()) {
|
||||
builder.setMessage(String.format(getActivity()
|
||||
.getString(R.string.ConversationFragment_transport_s_sent_received_s),
|
||||
transport,
|
||||
dateFormatter.format(new Date(dateSent))));
|
||||
} else {
|
||||
builder.setMessage(String.format(getActivity()
|
||||
.getString(R.string.ConversationFragment_sender_s_transport_s_sent_s_received_s),
|
||||
message.getIndividualRecipient().getNumber(),
|
||||
transport,
|
||||
dateFormatter.format(new Date(dateSent)),
|
||||
dateFormatter.format(new Date(dateReceived))));
|
||||
}
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok, null);
|
||||
builder.show();
|
||||
Intent intent = new Intent(getActivity(), MessageDetailsActivity.class);
|
||||
intent.putExtra(MessageDetailsActivity.MASTER_SECRET_EXTRA, masterSecret);
|
||||
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, message.getId());
|
||||
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, message.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void handleForwardMessage(MessageRecord message) {
|
||||
@@ -315,15 +276,6 @@ public class ConversationFragment extends ListFragment
|
||||
((CursorAdapter)getListAdapter()).changeCursor(null);
|
||||
}
|
||||
|
||||
private class FailedIconClickHandler extends Handler {
|
||||
@Override
|
||||
public void handleMessage(android.os.Message message) {
|
||||
if (listener != null) {
|
||||
listener.setComposeText((String)message.obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface ConversationFragmentListener {
|
||||
public void setComposeText(String text);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
@@ -119,7 +120,6 @@ public class ConversationItem extends LinearLayout {
|
||||
private FutureTaskListener<SlideDeck> slideDeckListener;
|
||||
private TypedArray backgroundDrawables;
|
||||
|
||||
private final FailedIconClickListener failedIconClickListener = new FailedIconClickListener();
|
||||
private final MmsDownloadClickListener mmsDownloadClickListener = new MmsDownloadClickListener();
|
||||
private final MmsPreferencesClickListener mmsPreferencesClickListener = new MmsPreferencesClickListener();
|
||||
private final ClickListener clickListener = new ClickListener();
|
||||
@@ -158,20 +158,18 @@ public class ConversationItem extends LinearLayout {
|
||||
this.backgroundDrawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
|
||||
|
||||
setOnClickListener(clickListener);
|
||||
if (failedImage != null) failedImage.setOnClickListener(failedIconClickListener);
|
||||
if (mmsDownloadButton != null) mmsDownloadButton.setOnClickListener(mmsDownloadClickListener);
|
||||
if (mmsThumbnail != null) mmsThumbnail.setOnLongClickListener(new MultiSelectLongClickListener());
|
||||
}
|
||||
|
||||
public void set(MasterSecret masterSecret, MessageRecord messageRecord,
|
||||
Set<MessageRecord> batchSelected, SelectionClickListener selectionClickListener,
|
||||
Handler failedIconHandler, boolean groupThread, boolean pushDestination)
|
||||
boolean groupThread, boolean pushDestination)
|
||||
{
|
||||
this.masterSecret = masterSecret;
|
||||
this.messageRecord = messageRecord;
|
||||
this.batchSelected = batchSelected;
|
||||
this.selectionClickListener = selectionClickListener;
|
||||
this.failedIconHandler = failedIconHandler;
|
||||
this.groupThread = groupThread;
|
||||
this.pushDestination = pushDestination;
|
||||
|
||||
@@ -223,10 +221,10 @@ public class ConversationItem extends LinearLayout {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
final int background;
|
||||
final int triangleBackground;
|
||||
if (messageRecord.isPending() && pushDestination && !messageRecord.isForcedSms()) {
|
||||
if ((messageRecord.isPending() || messageRecord.isFailed()) && pushDestination && !messageRecord.isForcedSms()) {
|
||||
background = SENT_PUSH_PENDING;
|
||||
triangleBackground = SENT_PUSH_PENDING_TRIANGLE;
|
||||
} else if (messageRecord.isPending() || messageRecord.isPendingSmsFallback()) {
|
||||
} else if (messageRecord.isPending() || messageRecord.isFailed() || messageRecord.isPendingSmsFallback()) {
|
||||
background = SENT_SMS_PENDING;
|
||||
triangleBackground = SENT_SMS_PENDING_TRIANGLE;
|
||||
} else if (messageRecord.isPush()) {
|
||||
@@ -279,10 +277,9 @@ public class ConversationItem extends LinearLayout {
|
||||
|
||||
private void setStatusIcons(MessageRecord messageRecord) {
|
||||
failedImage.setVisibility(messageRecord.isFailed() ? View.VISIBLE : View.GONE);
|
||||
if (messageRecord.isOutgoing()) {
|
||||
pendingIndicator.setVisibility(messageRecord.isPendingSmsFallback() ? View.VISIBLE : View.GONE);
|
||||
indicatorText.setVisibility(messageRecord.isPendingSmsFallback() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
// pendingIndicator.setVisibility(View.GONE);
|
||||
if (messageRecord.isOutgoing()) indicatorText.setVisibility(View.GONE);
|
||||
|
||||
secureImage.setVisibility(messageRecord.isSecure() ? View.VISIBLE : View.GONE);
|
||||
bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.ic_menu_login : 0, 0);
|
||||
deliveryImage.setVisibility(!messageRecord.isKeyExchange() && messageRecord.isDelivered() ? View.VISIBLE : View.GONE);
|
||||
@@ -291,25 +288,37 @@ public class ConversationItem extends LinearLayout {
|
||||
mmsDownloadButton.setVisibility(View.GONE);
|
||||
mmsDownloadingLabel.setVisibility(View.GONE);
|
||||
|
||||
if (messageRecord.isFailed()) {
|
||||
dateText.setText(R.string.ConversationItem_error_sending_message);
|
||||
} else if (messageRecord.isPendingSmsFallback() && indicatorText != null) {
|
||||
dateText.setText("");
|
||||
if (messageRecord.isPendingSecureSmsFallback()) {
|
||||
if (messageRecord.isMms()) indicatorText.setText(R.string.ConversationItem_click_to_approve_mms);
|
||||
else indicatorText.setText(R.string.ConversationItem_click_to_approve_sms);
|
||||
} else {
|
||||
indicatorText.setText(R.string.ConversationItem_click_to_approve_unencrypted);
|
||||
}
|
||||
} else if (messageRecord.isPending()) {
|
||||
dateText.setText(" ··· ");
|
||||
if (messageRecord.isFailed()) setFailedStatusIcons();
|
||||
else if (messageRecord.isPendingSmsFallback()) setFallbackStatusIcons();
|
||||
else if (messageRecord.isPending()) dateText.setText(" ··· ");
|
||||
else setSentStatusIcons();
|
||||
|
||||
}
|
||||
|
||||
private void setSentStatusIcons() {
|
||||
final long timestamp;
|
||||
|
||||
if (messageRecord.isPush()) timestamp = messageRecord.getDateSent();
|
||||
else timestamp = messageRecord.getDateReceived();
|
||||
|
||||
dateText.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), timestamp));
|
||||
}
|
||||
|
||||
private void setFailedStatusIcons() {
|
||||
dateText.setText(R.string.ConversationItem_error_not_delivered);
|
||||
indicatorText.setText(R.string.ConversationItem_click_for_details);
|
||||
indicatorText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void setFallbackStatusIcons() {
|
||||
pendingIndicator.setVisibility(View.VISIBLE);
|
||||
indicatorText.setVisibility(View.VISIBLE);
|
||||
|
||||
if (messageRecord.isPendingSecureSmsFallback()) {
|
||||
if (messageRecord.isMms()) indicatorText.setText(R.string.ConversationItem_click_to_approve_mms);
|
||||
else indicatorText.setText(R.string.ConversationItem_click_to_approve_sms);
|
||||
} else {
|
||||
final long timestamp;
|
||||
|
||||
if (messageRecord.isPush()) timestamp = messageRecord.getDateSent();
|
||||
else timestamp = messageRecord.getDateReceived();
|
||||
|
||||
dateText.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), timestamp));
|
||||
indicatorText.setText(R.string.ConversationItem_click_to_approve_unencrypted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +332,9 @@ public class ConversationItem extends LinearLayout {
|
||||
}
|
||||
|
||||
private void setEvents(MessageRecord messageRecord) {
|
||||
setClickable(messageRecord.isPendingSmsFallback() ||
|
||||
setClickable(messageRecord.isPendingSmsFallback() ||
|
||||
messageRecord.hasNetworkFailures() ||
|
||||
messageRecord.isIdentityMismatchFailure() ||
|
||||
(messageRecord.isKeyExchange() &&
|
||||
!messageRecord.isCorruptedKeyExchange() &&
|
||||
!messageRecord.isOutgoing()));
|
||||
@@ -529,7 +540,7 @@ public class ConversationItem extends LinearLayout {
|
||||
private class MmsDownloadClickListener implements View.OnClickListener {
|
||||
public void onClick(View v) {
|
||||
NotificationMmsMessageRecord notificationRecord = (NotificationMmsMessageRecord)messageRecord;
|
||||
Log.w("MmsDownloadClickListener", "Content location: " + new String(notificationRecord.getContentLocation()));
|
||||
Log.w(TAG, "Content location: " + new String(notificationRecord.getContentLocation()));
|
||||
mmsDownloadButton.setVisibility(View.GONE);
|
||||
mmsDownloadingLabel.setVisibility(View.VISIBLE);
|
||||
|
||||
@@ -562,13 +573,22 @@ public class ConversationItem extends LinearLayout {
|
||||
|
||||
private class ClickListener implements View.OnClickListener {
|
||||
public void onClick(View v) {
|
||||
if (messageRecord.isKeyExchange() &&
|
||||
!messageRecord.isOutgoing() &&
|
||||
!messageRecord.isProcessedKeyExchange() &&
|
||||
!messageRecord.isStaleKeyExchange())
|
||||
if (messageRecord.isIdentityMismatchFailure() || messageRecord.hasNetworkFailures()) {
|
||||
Intent intent = new Intent(context, MessageDetailsActivity.class);
|
||||
intent.putExtra(MessageDetailsActivity.MASTER_SECRET_EXTRA, masterSecret);
|
||||
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, messageRecord.getId());
|
||||
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
|
||||
intent.putExtra(MessageDetailsActivity.PUSH_EXTRA, pushDestination);
|
||||
context.startActivity(intent);
|
||||
} else if (messageRecord.isKeyExchange() &&
|
||||
!messageRecord.isOutgoing() &&
|
||||
!messageRecord.isProcessedKeyExchange() &&
|
||||
!messageRecord.isStaleKeyExchange())
|
||||
{
|
||||
handleKeyExchangeClicked();
|
||||
else if (messageRecord.isPendingSmsFallback())
|
||||
} else if (messageRecord.isPendingSmsFallback()) {
|
||||
handleMessageApproval();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,17 +17,9 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.provider.Contacts.Intents;
|
||||
import android.provider.ContactsContract.QuickContact;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
@@ -39,6 +31,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Emoji;
|
||||
import org.thoughtcrime.securesms.util.RecipientViewUtil;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@@ -66,7 +59,6 @@ public class ConversationListItem extends RelativeLayout
|
||||
private TextView subjectView;
|
||||
private TextView fromView;
|
||||
private TextView dateView;
|
||||
private long count;
|
||||
private boolean read;
|
||||
private ImageView contactPhotoImage;
|
||||
|
||||
@@ -98,12 +90,11 @@ public class ConversationListItem extends RelativeLayout
|
||||
this.selectedThreads = selectedThreads;
|
||||
this.recipients = thread.getRecipients();
|
||||
this.threadId = thread.getThreadId();
|
||||
this.count = thread.getCount();
|
||||
this.read = thread.isRead();
|
||||
this.distributionType = thread.getDistributionType();
|
||||
|
||||
this.recipients.addListener(this);
|
||||
this.fromView.setText(formatFrom(recipients, count, read));
|
||||
this.fromView.setText(RecipientViewUtil.formatFrom(context, recipients, read));
|
||||
|
||||
this.subjectView.setText(Emoji.getInstance(context).emojify(thread.getDisplayBody(),
|
||||
Emoji.EMOJI_SMALL,
|
||||
@@ -118,7 +109,7 @@ public class ConversationListItem extends RelativeLayout
|
||||
}
|
||||
|
||||
setBackground(read, batchMode);
|
||||
setContactPhoto(this.recipients.getPrimaryRecipient());
|
||||
RecipientViewUtil.setContactPhoto(context, contactPhotoImage, recipients.getPrimaryRecipient(), true);
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
@@ -130,28 +121,6 @@ public class ConversationListItem extends RelativeLayout
|
||||
contactPhotoImage.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void setContactPhoto(final Recipient recipient) {
|
||||
if (recipient == null) return;
|
||||
|
||||
contactPhotoImage.setImageBitmap(recipient.getContactPhoto());
|
||||
|
||||
if (!recipient.isGroupRecipient()) {
|
||||
contactPhotoImage.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (recipient.getContactUri() != null) {
|
||||
QuickContact.showQuickContact(context, contactPhotoImage, recipient.getContactUri(), QuickContact.MODE_LARGE, null);
|
||||
} else {
|
||||
Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, Uri.fromParts("tel", recipient.getNumber(), null));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
contactPhotoImage.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void setBackground(boolean read, boolean batch) {
|
||||
int[] attributes = new int[]{R.attr.conversation_list_item_background_selected,
|
||||
R.attr.conversation_list_item_background_read,
|
||||
@@ -170,38 +139,6 @@ public class ConversationListItem extends RelativeLayout
|
||||
drawables.recycle();
|
||||
}
|
||||
|
||||
private CharSequence formatFrom(Recipients from, long count, boolean read) {
|
||||
int attributes[] = new int[] {R.attr.conversation_list_item_count_color};
|
||||
TypedArray colors = context.obtainStyledAttributes(attributes);
|
||||
|
||||
final String fromString;
|
||||
final boolean isUnnamedGroup = from.isGroupRecipient() && TextUtils.isEmpty(from.getPrimaryRecipient().getName());
|
||||
if (isUnnamedGroup) {
|
||||
fromString = context.getString(R.string.ConversationActivity_unnamed_group);
|
||||
} else {
|
||||
fromString = from.toShortString();
|
||||
}
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(fromString);
|
||||
|
||||
|
||||
final int typeface;
|
||||
if (isUnnamedGroup) {
|
||||
if (!read) typeface = Typeface.BOLD_ITALIC;
|
||||
else typeface = Typeface.ITALIC;
|
||||
} else if (!read) {
|
||||
typeface = Typeface.BOLD;
|
||||
} else {
|
||||
typeface = Typeface.NORMAL;
|
||||
}
|
||||
|
||||
builder.setSpan(new StyleSpan(typeface), 0, builder.length(),
|
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
|
||||
colors.recycle();
|
||||
return builder;
|
||||
}
|
||||
|
||||
public Recipients getRecipients() {
|
||||
return recipients;
|
||||
}
|
||||
@@ -219,8 +156,8 @@ public class ConversationListItem extends RelativeLayout
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ConversationListItem.this.fromView.setText(formatFrom(recipients, count, read));
|
||||
setContactPhoto(ConversationListItem.this.recipients.getPrimaryRecipient());
|
||||
ConversationListItem.this.fromView.setText(RecipientViewUtil.formatFrom(context, recipients, read));
|
||||
RecipientViewUtil.setContactPhoto(context, contactPhotoImage, recipients.getPrimaryRecipient(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
255
src/org/thoughtcrime/securesms/MessageDetailsActivity.java
Normal file
255
src/org/thoughtcrime/securesms/MessageDetailsActivity.java
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open 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
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.sql.Date;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity implements LoaderCallbacks<Cursor> {
|
||||
private final static String TAG = MessageDetailsActivity.class.getSimpleName();
|
||||
|
||||
public final static String MASTER_SECRET_EXTRA = "master_secret";
|
||||
public final static String MESSAGE_ID_EXTRA = "message_id";
|
||||
public final static String TYPE_EXTRA = "type";
|
||||
public final static String PUSH_EXTRA = "push";
|
||||
|
||||
private MasterSecret masterSecret;
|
||||
private ConversationItem conversationItem;
|
||||
private ViewGroup itemParent;
|
||||
private TextView sentDate;
|
||||
private TextView receivedDate;
|
||||
private View receivedContainer;
|
||||
private TextView transport;
|
||||
private TextView toFrom;
|
||||
private ListView recipientsList;
|
||||
private LayoutInflater inflater;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
setContentView(R.layout.message_details_activity);
|
||||
|
||||
initializeResources();
|
||||
|
||||
getSupportLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
inflater = LayoutInflater.from(this);
|
||||
View header = inflater.inflate(R.layout.message_details_header, recipientsList, false);
|
||||
|
||||
masterSecret = getIntent().getParcelableExtra(MASTER_SECRET_EXTRA);
|
||||
itemParent = (ViewGroup) findViewById(R.id.item_container );
|
||||
recipientsList = (ListView ) findViewById(R.id.recipients_list);
|
||||
sentDate = (TextView ) header.findViewById(R.id.sent_time);
|
||||
receivedContainer = header.findViewById(R.id.received_container);
|
||||
receivedDate = (TextView ) header.findViewById(R.id.received_time);
|
||||
transport = (TextView ) header.findViewById(R.id.transport);
|
||||
toFrom = (TextView ) header.findViewById(R.id.tofrom);
|
||||
recipientsList.setHeaderDividersEnabled(false);
|
||||
recipientsList.addHeaderView(header, null, false);
|
||||
}
|
||||
|
||||
private void updateTransport(MessageRecord messageRecord) {
|
||||
final String transportText;
|
||||
if (messageRecord.isOutgoing() && messageRecord.isFailed()) {
|
||||
transportText = "-";
|
||||
} else if (messageRecord.isPending()) {
|
||||
transportText = getString(R.string.ConversationFragment_pending);
|
||||
} else if (messageRecord.isPush()) {
|
||||
transportText = getString(R.string.ConversationFragment_push);
|
||||
} else if (messageRecord.isMms()) {
|
||||
transportText = getString(R.string.ConversationFragment_mms);
|
||||
} else {
|
||||
transportText = getString(R.string.ConversationFragment_sms);
|
||||
}
|
||||
|
||||
transport.setText(transportText);
|
||||
}
|
||||
|
||||
private void updateTime(MessageRecord messageRecord) {
|
||||
if (messageRecord.isPending() || messageRecord.isFailed()) {
|
||||
sentDate.setText("-");
|
||||
receivedContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(this);
|
||||
sentDate.setText(dateFormatter.format(new Date(messageRecord.getDateSent())));
|
||||
|
||||
if (messageRecord.getDateReceived() != messageRecord.getDateSent() && !messageRecord.isOutgoing()) {
|
||||
receivedDate.setText(dateFormatter.format(new Date(messageRecord.getDateReceived())));
|
||||
receivedContainer.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
receivedContainer.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRecipients(MessageRecord messageRecord, Recipients recipients) {
|
||||
final int toFromRes;
|
||||
if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) {
|
||||
toFromRes = R.string.message_details_header__with;
|
||||
} else if (messageRecord.isOutgoing()) {
|
||||
toFromRes = R.string.message_details_header__to;
|
||||
} else {
|
||||
toFromRes = R.string.message_details_header__from;
|
||||
}
|
||||
toFrom.setText(toFromRes);
|
||||
conversationItem.set(masterSecret, messageRecord, new HashSet<MessageRecord>(), null,
|
||||
recipients != messageRecord.getRecipients(),
|
||||
DirectoryHelper.isPushDestination(this, recipients));
|
||||
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, masterSecret, messageRecord, recipients));
|
||||
}
|
||||
|
||||
private void inflateMessageViewIfAbsent(MessageRecord messageRecord) {
|
||||
if (conversationItem == null) {
|
||||
if (messageRecord.isGroupAction()) {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_activity, itemParent, false);
|
||||
} else if (messageRecord.isOutgoing()) {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent, itemParent, false);
|
||||
} else {
|
||||
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received, itemParent, false);
|
||||
}
|
||||
itemParent.addView(conversationItem);
|
||||
}
|
||||
}
|
||||
|
||||
private MessageRecord getMessageRecord(Context context, Cursor cursor, String type) {
|
||||
switch (type) {
|
||||
case MmsSmsDatabase.SMS_TRANSPORT:
|
||||
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
SmsDatabase.Reader reader = smsDatabase.readerFor(masterSecret, cursor);
|
||||
return reader.getNext();
|
||||
case MmsSmsDatabase.MMS_TRANSPORT:
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
MmsDatabase.Reader mmsReader = mmsDatabase.readerFor(masterSecret, cursor);
|
||||
return mmsReader.getNext();
|
||||
default:
|
||||
throw new AssertionError("no valid message type specified");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new MessageDetailsLoader(this, getIntent().getStringExtra(TYPE_EXTRA),
|
||||
getIntent().getLongExtra(MESSAGE_ID_EXTRA, -1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
final MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA));
|
||||
new MessageRecipientAsyncTask(this, messageRecord).execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
recipientsList.setAdapter(null);
|
||||
}
|
||||
|
||||
private class MessageRecipientAsyncTask extends AsyncTask<Void,Void,Recipients> {
|
||||
private WeakReference<Context> weakContext;
|
||||
private MessageRecord messageRecord;
|
||||
|
||||
public MessageRecipientAsyncTask(Context context, MessageRecord messageRecord) {
|
||||
this.weakContext = new WeakReference<>(context);
|
||||
this.messageRecord = messageRecord;
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
return weakContext.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Recipients doInBackground(Void... voids) {
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
Log.w(TAG, "associated context is destroyed, finishing early");
|
||||
}
|
||||
|
||||
Recipients recipients;
|
||||
|
||||
final Recipients intermediaryRecipients;
|
||||
if (messageRecord.isMms()) {
|
||||
intermediaryRecipients = DatabaseFactory.getMmsAddressDatabase(context).getRecipientsForId(messageRecord.getId());
|
||||
} else {
|
||||
intermediaryRecipients = messageRecord.getRecipients();
|
||||
}
|
||||
|
||||
if (!intermediaryRecipients.isGroupRecipient()) {
|
||||
Log.w(TAG, "Recipient is not a group, resolving members immediately.");
|
||||
recipients = intermediaryRecipients;
|
||||
} else {
|
||||
try {
|
||||
String groupId = intermediaryRecipients.getPrimaryRecipient().getNumber();
|
||||
recipients = DatabaseFactory.getGroupDatabase(context)
|
||||
.getGroupMembers(GroupUtil.getDecodedId(groupId), false);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
recipients = new Recipients(new LinkedList<Recipient>());
|
||||
}
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(Recipients recipients) {
|
||||
if (getContext() == null) {
|
||||
Log.w(TAG, "AsyncTask finished with a destroyed context, leaving early.");
|
||||
return;
|
||||
}
|
||||
|
||||
inflateMessageViewIfAbsent(messageRecord);
|
||||
|
||||
updateRecipients(messageRecord, recipients);
|
||||
updateTransport(messageRecord);
|
||||
updateTime(messageRecord);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.BaseAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
|
||||
public class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
|
||||
|
||||
private Context context;
|
||||
private MasterSecret masterSecret;
|
||||
private MessageRecord record;
|
||||
private Recipients recipients;
|
||||
|
||||
public MessageDetailsRecipientAdapter(Context context, MasterSecret masterSecret, MessageRecord record, Recipients recipients) {
|
||||
this.context = context;
|
||||
this.masterSecret = masterSecret;
|
||||
this.record = record;
|
||||
this.recipients = recipients;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return recipients.getRecipientsList().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return recipients.getRecipientsList().get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return recipients.getRecipientsList().get(position).getRecipientId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(context).inflate(R.layout.message_details_recipient, parent, false);
|
||||
}
|
||||
|
||||
((MessageRecipientListItem)convertView).set(masterSecret, record, recipients, position);
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMovedToScrapHeap(View view) {
|
||||
((MessageRecipientListItem)view).unbind();
|
||||
}
|
||||
|
||||
}
|
||||
177
src/org/thoughtcrime/securesms/MessageRecipientListItem.java
Normal file
177
src/org/thoughtcrime/securesms/MessageRecipientListItem.java
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open 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
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* 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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.RecipientViewUtil;
|
||||
|
||||
/**
|
||||
* A simple view to show the recipients of a message
|
||||
*
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class MessageRecipientListItem extends RelativeLayout
|
||||
implements Recipient.RecipientModifiedListener
|
||||
{
|
||||
private final static String TAG = MessageRecipientListItem.class.getSimpleName();
|
||||
|
||||
private Recipient recipient;
|
||||
private TextView fromView;
|
||||
private TextView errorDescription;
|
||||
private Button conflictButton;
|
||||
private Button resendButton;
|
||||
private ImageView contactPhotoImage;
|
||||
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
public MessageRecipientListItem(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MessageRecipientListItem(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
this.fromView = (TextView) findViewById(R.id.from);
|
||||
this.errorDescription = (TextView) findViewById(R.id.error_description);
|
||||
this.contactPhotoImage = (ImageView) findViewById(R.id.contact_photo_image);
|
||||
this.conflictButton = (Button) findViewById(R.id.conflict_button);
|
||||
this.resendButton = (Button) findViewById(R.id.resend_button);
|
||||
}
|
||||
|
||||
public void set(final MasterSecret masterSecret, final MessageRecord record, final Recipients recipients, final int position) {
|
||||
recipient = recipients.getRecipientsList().get(position);
|
||||
recipient.addListener(this);
|
||||
fromView.setText(RecipientViewUtil.formatFrom(getContext(), recipient));
|
||||
|
||||
RecipientViewUtil.setContactPhoto(getContext(), contactPhotoImage, recipient, false);
|
||||
setIssueIndicators(masterSecret, record);
|
||||
}
|
||||
|
||||
private void setIssueIndicators(final MasterSecret masterSecret, final MessageRecord record) {
|
||||
final NetworkFailure networkFailure = getNetworkFailure(record);
|
||||
final IdentityKeyMismatch keyMismatch = networkFailure == null ? getKeyMismatch(record) : null;
|
||||
|
||||
String errorText = "";
|
||||
if (networkFailure != null) {
|
||||
errorText = getContext().getString(R.string.MessageDetailsRecipient_failed_to_send);
|
||||
resendButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
new ResendAsyncTask(masterSecret, record, networkFailure).execute();
|
||||
}
|
||||
});
|
||||
} else if (keyMismatch != null) {
|
||||
errorText = getContext().getString(R.string.MessageDetailsRecipient_new_identity);
|
||||
conflictButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
new ConfirmIdentityDialog(getContext(), masterSecret, record, keyMismatch).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
errorDescription.setText(errorText);
|
||||
errorDescription.setVisibility(TextUtils.isEmpty(errorText) ? View.GONE : View.VISIBLE);
|
||||
resendButton.setVisibility(networkFailure != null ? View.VISIBLE : View.GONE);
|
||||
conflictButton.setVisibility(keyMismatch != null ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private NetworkFailure getNetworkFailure(final MessageRecord record) {
|
||||
if (record.hasNetworkFailures()) {
|
||||
for (final NetworkFailure failure : record.getNetworkFailures()) {
|
||||
if (failure.getRecipientId() == recipient.getRecipientId()) {
|
||||
return failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private IdentityKeyMismatch getKeyMismatch(final MessageRecord record) {
|
||||
if (record.isIdentityMismatchFailure()) {
|
||||
for (final IdentityKeyMismatch mismatch : record.getIdentityKeyMismatches()) {
|
||||
if (mismatch.getRecipientId() == recipient.getRecipientId()) {
|
||||
return mismatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
if (this.recipient != null) this.recipient.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModified(final Recipient recipient) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
fromView.setText(RecipientViewUtil.formatFrom(getContext(), recipient));
|
||||
RecipientViewUtil.setContactPhoto(getContext(), contactPhotoImage, recipient, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class ResendAsyncTask extends AsyncTask<Void,Void,Void> {
|
||||
private final MasterSecret masterSecret;
|
||||
private final MessageRecord record;
|
||||
private final NetworkFailure failure;
|
||||
|
||||
public ResendAsyncTask(MasterSecret masterSecret, MessageRecord record, NetworkFailure failure) {
|
||||
this.masterSecret = masterSecret;
|
||||
this.record = record;
|
||||
this.failure = failure;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
|
||||
mmsDatabase.removeFailure(record.getId(), failure);
|
||||
|
||||
if (record.getRecipients().isGroupRecipient()) {
|
||||
MessageSender.resendGroupMessage(getContext(), masterSecret, record, failure.getRecipientId());
|
||||
} else {
|
||||
MessageSender.resend(getContext(), masterSecret, record);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -236,7 +236,7 @@ public class ReceiveKeyActivity extends BaseActivity {
|
||||
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new PushDecryptJob(context, pushId, message.getSender()));
|
||||
.add(new PushDecryptJob(context, pushId, messageId, message.getSender()));
|
||||
|
||||
smsDatabase.deleteMessage(messageId);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -17,20 +17,11 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
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;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -39,11 +30,7 @@ import com.makeramen.RoundedImageView;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.Emoji;
|
||||
|
||||
import java.util.Set;
|
||||
import org.thoughtcrime.securesms.util.RecipientViewUtil;
|
||||
|
||||
/**
|
||||
* A simple view to show the recipients of an open conversation
|
||||
@@ -51,7 +38,7 @@ import java.util.Set;
|
||||
* @author Jake McGinty
|
||||
*/
|
||||
public class ShareListItem extends RelativeLayout
|
||||
implements Recipient.RecipientModifiedListener
|
||||
implements Recipient.RecipientModifiedListener
|
||||
{
|
||||
private final static String TAG = ShareListItem.class.getSimpleName();
|
||||
|
||||
@@ -87,21 +74,16 @@ public class ShareListItem extends RelativeLayout
|
||||
this.distributionType = thread.getDistributionType();
|
||||
|
||||
this.recipients.addListener(this);
|
||||
this.fromView.setText(formatFrom(recipients));
|
||||
this.fromView.setText(RecipientViewUtil.formatFrom(getContext(), recipients));
|
||||
|
||||
setBackground();
|
||||
setContactPhoto(this.recipients.getPrimaryRecipient());
|
||||
RecipientViewUtil.setContactPhoto(getContext(), contactPhotoImage, this.recipients.getPrimaryRecipient(), false);
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
if (this.recipients != null) this.recipients.removeListener(this);
|
||||
}
|
||||
|
||||
private void setContactPhoto(final Recipient recipient) {
|
||||
if (recipient == null) return;
|
||||
contactPhotoImage.setImageBitmap(recipient.getContactPhoto());
|
||||
}
|
||||
|
||||
private void setBackground() {
|
||||
int[] attributes = new int[]{R.attr.conversation_list_item_background_read};
|
||||
TypedArray drawables = context.obtainStyledAttributes(attributes);
|
||||
@@ -111,26 +93,6 @@ public class ShareListItem extends RelativeLayout
|
||||
drawables.recycle();
|
||||
}
|
||||
|
||||
private CharSequence formatFrom(Recipients from) {
|
||||
final String fromString;
|
||||
final boolean isUnnamedGroup = from.isGroupRecipient() && TextUtils.isEmpty(from.getPrimaryRecipient().getName());
|
||||
if (isUnnamedGroup) {
|
||||
fromString = context.getString(R.string.ConversationActivity_unnamed_group);
|
||||
} else {
|
||||
fromString = from.toShortString();
|
||||
}
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(fromString);
|
||||
|
||||
final int typeface;
|
||||
if (isUnnamedGroup) typeface = Typeface.ITALIC;
|
||||
else typeface = Typeface.NORMAL;
|
||||
|
||||
builder.setSpan(new StyleSpan(typeface), 0, builder.length(),
|
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public Recipients getRecipients() {
|
||||
return recipients;
|
||||
}
|
||||
@@ -148,8 +110,8 @@ public class ShareListItem extends RelativeLayout
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
fromView.setText(formatFrom(recipients));
|
||||
setContactPhoto(recipients.getPrimaryRecipient());
|
||||
fromView.setText(RecipientViewUtil.formatFrom(getContext(), recipients));
|
||||
RecipientViewUtil.setContactPhoto(getContext(), contactPhotoImage, recipients.getPrimaryRecipient(), false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ package org.thoughtcrime.securesms.crypto;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.thoughtcrimegson.Gson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
@@ -30,10 +31,10 @@ import org.whispersystems.libaxolotl.InvalidKeyIdException;
|
||||
import org.whispersystems.libaxolotl.ecc.Curve;
|
||||
import org.whispersystems.libaxolotl.ecc.Curve25519;
|
||||
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
||||
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyStore;
|
||||
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
|
||||
import org.whispersystems.libaxolotl.util.Medium;
|
||||
|
||||
import java.io.File;
|
||||
@@ -109,7 +110,7 @@ public class PreKeyUtil {
|
||||
try {
|
||||
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
|
||||
FileOutputStream fout = new FileOutputStream(nextFile);
|
||||
fout.write(new Gson().toJson(new PreKeyIndex(id)).getBytes());
|
||||
fout.write(JsonUtils.toJson(new PreKeyIndex(id)).getBytes());
|
||||
fout.close();
|
||||
} catch (IOException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
@@ -120,7 +121,7 @@ public class PreKeyUtil {
|
||||
try {
|
||||
File nextFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME);
|
||||
FileOutputStream fout = new FileOutputStream(nextFile);
|
||||
fout.write(new Gson().toJson(new SignedPreKeyIndex(id)).getBytes());
|
||||
fout.write(JsonUtils.toJson(new SignedPreKeyIndex(id)).getBytes());
|
||||
fout.close();
|
||||
} catch (IOException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
@@ -135,7 +136,7 @@ public class PreKeyUtil {
|
||||
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
|
||||
} else {
|
||||
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
|
||||
PreKeyIndex index = new Gson().fromJson(reader, PreKeyIndex.class);
|
||||
PreKeyIndex index = JsonUtils.fromJson(reader, PreKeyIndex.class);
|
||||
reader.close();
|
||||
return index.nextPreKeyId;
|
||||
}
|
||||
@@ -153,7 +154,7 @@ public class PreKeyUtil {
|
||||
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
|
||||
} else {
|
||||
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
|
||||
SignedPreKeyIndex index = new Gson().fromJson(reader, SignedPreKeyIndex.class);
|
||||
SignedPreKeyIndex index = JsonUtils.fromJson(reader, SignedPreKeyIndex.class);
|
||||
reader.close();
|
||||
return index.nextSignedPreKeyId;
|
||||
}
|
||||
@@ -183,6 +184,7 @@ public class PreKeyUtil {
|
||||
private static class PreKeyIndex {
|
||||
public static final String FILE_NAME = "index.dat";
|
||||
|
||||
@JsonProperty
|
||||
private int nextPreKeyId;
|
||||
|
||||
public PreKeyIndex() {}
|
||||
@@ -195,6 +197,7 @@ public class PreKeyUtil {
|
||||
private static class SignedPreKeyIndex {
|
||||
public static final String FILE_NAME = "index.dat";
|
||||
|
||||
@JsonProperty
|
||||
private int nextSignedPreKeyId;
|
||||
|
||||
public SignedPreKeyIndex() {}
|
||||
|
||||
@@ -25,9 +25,9 @@ import java.util.Set;
|
||||
|
||||
public abstract class Database {
|
||||
|
||||
protected static final String ID_WHERE = "_id = ?";
|
||||
private static final String CONVERSATION_URI = "content://textsecure/thread/";
|
||||
private static final String CONVERSATION_LIST_URI = "content://textsecure/conversation-list";
|
||||
protected static final String ID_WHERE = "_id = ?";
|
||||
private static final String CONVERSATION_URI = "content://textsecure/thread/";
|
||||
private static final String CONVERSATION_LIST_URI = "content://textsecure/conversation-list";
|
||||
|
||||
protected SQLiteOpenHelper databaseHelper;
|
||||
protected final Context context;
|
||||
|
||||
@@ -45,21 +45,22 @@ import ws.com.google.android.mms.ContentType;
|
||||
|
||||
public class DatabaseFactory {
|
||||
|
||||
private static final int INTRODUCED_IDENTITIES_VERSION = 2;
|
||||
private static final int INTRODUCED_INDEXES_VERSION = 3;
|
||||
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 INTRODUCED_MMS_BODY_VERSION = 7;
|
||||
private static final int INTRODUCED_MMS_FROM_VERSION = 8;
|
||||
private static final int INTRODUCED_TOFU_IDENTITY_VERSION = 9;
|
||||
private static final int INTRODUCED_PUSH_DATABASE_VERSION = 10;
|
||||
private static final int INTRODUCED_GROUP_DATABASE_VERSION = 11;
|
||||
private static final int INTRODUCED_PUSH_FIX_VERSION = 12;
|
||||
private static final int INTRODUCED_DELIVERY_RECEIPTS = 13;
|
||||
private static final int INTRODUCED_PART_DATA_SIZE_VERSION = 14;
|
||||
private static final int INTRODUCED_THUMBNAILS_VERSION = 15;
|
||||
private static final int DATABASE_VERSION = 15;
|
||||
private static final int INTRODUCED_IDENTITIES_VERSION = 2;
|
||||
private static final int INTRODUCED_INDEXES_VERSION = 3;
|
||||
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 INTRODUCED_MMS_BODY_VERSION = 7;
|
||||
private static final int INTRODUCED_MMS_FROM_VERSION = 8;
|
||||
private static final int INTRODUCED_TOFU_IDENTITY_VERSION = 9;
|
||||
private static final int INTRODUCED_PUSH_DATABASE_VERSION = 10;
|
||||
private static final int INTRODUCED_GROUP_DATABASE_VERSION = 11;
|
||||
private static final int INTRODUCED_PUSH_FIX_VERSION = 12;
|
||||
private static final int INTRODUCED_DELIVERY_RECEIPTS = 13;
|
||||
private static final int INTRODUCED_PART_DATA_SIZE_VERSION = 14;
|
||||
private static final int INTRODUCED_THUMBNAILS_VERSION = 15;
|
||||
private static final int INTRODUCED_IDENTITY_COLUMN_VERSION = 16;
|
||||
private static final int DATABASE_VERSION = 16;
|
||||
|
||||
private static final String DATABASE_NAME = "messages.db";
|
||||
private static final Object lock = new Object();
|
||||
@@ -706,8 +707,14 @@ public class DatabaseFactory {
|
||||
}
|
||||
|
||||
if (oldVersion < INTRODUCED_THUMBNAILS_VERSION) {
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN thumbnail TEXT");
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN aspect_ratio REAL");
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN thumbnail TEXT;");
|
||||
db.execSQL("ALTER TABLE part ADD COLUMN aspect_ratio REAL;");
|
||||
}
|
||||
|
||||
if (oldVersion < INTRODUCED_IDENTITY_COLUMN_VERSION) {
|
||||
db.execSQL("ALTER TABLE sms ADD COLUMN mismatched_identities TEXT");
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN mismatched_identities TEXT");
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN network_failures TEXT");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
||||
145
src/org/thoughtcrime/securesms/database/MessagingDatabase.java
Normal file
145
src/org/thoughtcrime/securesms/database/MessagingDatabase.java
Normal file
@@ -0,0 +1,145 @@
|
||||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.database.documents.Document;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class MessagingDatabase extends Database implements MmsSmsColumns {
|
||||
|
||||
private static final String TAG = MessagingDatabase.class.getSimpleName();
|
||||
|
||||
public MessagingDatabase(Context context, SQLiteOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
protected abstract String getTableName();
|
||||
|
||||
public void addMismatchedIdentity(long messageId, long recipientId, IdentityKey identityKey) {
|
||||
try {
|
||||
addToDocument(messageId, MISMATCHED_IDENTITIES,
|
||||
new IdentityKeyMismatch(recipientId, identityKey),
|
||||
IdentityKeyMismatchList.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeMismatchedIdentity(long messageId, long recipientId, IdentityKey identityKey) {
|
||||
try {
|
||||
removeFromDocument(messageId, MISMATCHED_IDENTITIES,
|
||||
new IdentityKeyMismatch(recipientId, identityKey),
|
||||
IdentityKeyMismatchList.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected <D extends Document<I>, I> void removeFromDocument(long messageId, String column, I object, Class<D> clazz) throws IOException {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.beginTransaction();
|
||||
|
||||
try {
|
||||
D document = getDocument(database, messageId, column, clazz);
|
||||
Iterator<I> iterator = document.getList().iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
I item = iterator.next();
|
||||
|
||||
if (item.equals(object)) {
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setDocument(database, messageId, column, document);
|
||||
database.setTransactionSuccessful();
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
protected <T extends Document<I>, I> void addToDocument(long messageId, String column, final I object, Class<T> clazz) throws IOException {
|
||||
List<I> list = new ArrayList<I>() {{
|
||||
add(object);
|
||||
}};
|
||||
|
||||
addToDocument(messageId, column, list, clazz);
|
||||
}
|
||||
|
||||
protected <T extends Document<I>, I> void addToDocument(long messageId, String column, List<I> objects, Class<T> clazz) throws IOException {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.beginTransaction();
|
||||
|
||||
try {
|
||||
T document = getDocument(database, messageId, column, clazz);
|
||||
document.getList().addAll(objects);
|
||||
setDocument(database, messageId, column, document);
|
||||
|
||||
database.setTransactionSuccessful();
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void setDocument(SQLiteDatabase database, long messageId, String column, Document document) throws IOException {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
|
||||
if (document == null || document.size() == 0) {
|
||||
contentValues.put(column, (String)null);
|
||||
} else {
|
||||
contentValues.put(column, JsonUtils.toJson(document));
|
||||
}
|
||||
|
||||
database.update(getTableName(), contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
|
||||
}
|
||||
|
||||
private <D extends Document> D getDocument(SQLiteDatabase database, long messageId,
|
||||
String column, Class<D> clazz)
|
||||
{
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = database.query(getTableName(), new String[] {column},
|
||||
ID_WHERE, new String[] {String.valueOf(messageId)},
|
||||
null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
String document = cursor.getString(cursor.getColumnIndexOrThrow(column));
|
||||
|
||||
try {
|
||||
if (!TextUtils.isEmpty(document)) {
|
||||
return JsonUtils.fromJson(document, clazz);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,11 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.util.Log;
|
||||
|
||||
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 ws.com.google.android.mms.pdu.CharacterSets;
|
||||
import ws.com.google.android.mms.pdu.EncodedStringValue;
|
||||
import ws.com.google.android.mms.pdu.PduHeaders;
|
||||
@@ -33,6 +38,8 @@ import java.util.List;
|
||||
|
||||
public class MmsAddressDatabase extends Database {
|
||||
|
||||
private static final String TAG = MmsAddressDatabase.class.getSimpleName();
|
||||
|
||||
private static final String TABLE_NAME = "mms_addresses";
|
||||
private static final String ID = "_id";
|
||||
private static final String MMS_ID = "mms_id";
|
||||
@@ -127,6 +134,25 @@ public class MmsAddressDatabase extends Database {
|
||||
return results;
|
||||
}
|
||||
|
||||
public Recipients getRecipientsForId(long messageId) {
|
||||
List<String> numbers = getAddressesForId(messageId);
|
||||
List<Recipient> results = new LinkedList<>();
|
||||
|
||||
for (String number : numbers) {
|
||||
if (!PduHeaders.FROM_INSERT_ADDRESS_TOKEN_STR.equals(number)) {
|
||||
try {
|
||||
results.add(RecipientFactory.getRecipientsFromString(context, number, false)
|
||||
.getPrimaryRecipient());
|
||||
} catch (RecipientFormattingException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Recipients(results);
|
||||
}
|
||||
|
||||
|
||||
public void deleteAddressesForId(long messageId) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
database.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {messageId+""});
|
||||
|
||||
@@ -21,7 +21,6 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.TextUtils;
|
||||
@@ -34,6 +33,10 @@ import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailureList;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.model.DisplayRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
@@ -50,6 +53,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.LRUCache;
|
||||
import org.thoughtcrime.securesms.util.ListenableFutureTask;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -59,10 +63,12 @@ import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||
import org.whispersystems.textsecure.api.util.InvalidNumberException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -88,7 +94,9 @@ import static org.thoughtcrime.securesms.util.Util.canonicalizeNumberOrGroup;
|
||||
// 2) How many queries do we make? calling getMediaMessageForId() from within an existing query
|
||||
// seems wasteful.
|
||||
|
||||
public class MmsDatabase extends Database implements MmsSmsColumns {
|
||||
public class MmsDatabase extends MessagingDatabase {
|
||||
|
||||
private static final String TAG = MmsDatabase.class.getSimpleName();
|
||||
|
||||
public static final String TABLE_NAME = "mms";
|
||||
static final String DATE_SENT = "date";
|
||||
@@ -119,6 +127,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
||||
private static final String DELIVERY_TIME = "d_tm";
|
||||
private static final String DELIVERY_REPORT = "d_rpt";
|
||||
static final String PART_COUNT = "part_count";
|
||||
static final String NETWORK_FAILURE = "network_failures";
|
||||
|
||||
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, " +
|
||||
@@ -132,7 +141,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
||||
STATUS + " INTEGER, " + TRANSACTION_ID + " TEXT, " + RETRIEVE_STATUS + " INTEGER, " +
|
||||
RETRIEVE_TEXT + " TEXT, " + RETRIEVE_TEXT_CS + " INTEGER, " + READ_STATUS + " INTEGER, " +
|
||||
CONTENT_CLASS + " INTEGER, " + RESPONSE_TEXT + " TEXT, " + DELIVERY_TIME + " INTEGER, " +
|
||||
RECEIPT_COUNT + " INTEGER DEFAULT 0, " + DELIVERY_REPORT + " INTEGER);";
|
||||
RECEIPT_COUNT + " INTEGER DEFAULT 0, " + MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " +
|
||||
NETWORK_FAILURE + " TEXT DEFAULT NULL," + DELIVERY_REPORT + " INTEGER);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
|
||||
@@ -150,7 +160,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
||||
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, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID,
|
||||
RECEIPT_COUNT
|
||||
RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE
|
||||
};
|
||||
|
||||
public static final ExecutorService slideResolver = org.thoughtcrime.securesms.util.Util.newSingleThreadedLifoExecutor();
|
||||
@@ -164,6 +174,11 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
||||
this.jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTableName() {
|
||||
return TABLE_NAME;
|
||||
}
|
||||
|
||||
public int getMessageCountForThread(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
@@ -181,6 +196,22 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void addFailures(long messageId, List<NetworkFailure> failure) {
|
||||
try {
|
||||
addToDocument(messageId, NETWORK_FAILURE, failure, NetworkFailureList.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeFailure(long messageId, NetworkFailure failure) {
|
||||
try {
|
||||
removeFromDocument(messageId, NETWORK_FAILURE, failure, NetworkFailureList.class);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void incrementDeliveryReceiptCount(String address, long timestamp) {
|
||||
MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context);
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
@@ -319,6 +350,14 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
||||
}
|
||||
}
|
||||
|
||||
public Cursor getMessage(long messageId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, MMS_PROJECTION, ID_WHERE, new String[] {messageId+""},
|
||||
null, null, null);
|
||||
setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId));
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public void updateResponseStatus(long messageId, int status) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues();
|
||||
@@ -330,8 +369,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
||||
private void updateMailboxBitmask(long id, long maskOff, long maskOn) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.execSQL("UPDATE " + TABLE_NAME +
|
||||
" SET " + MESSAGE_BOX + " = (" + MESSAGE_BOX + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" +
|
||||
" WHERE " + ID + " = ?", new String[] {id + ""});
|
||||
" SET " + MESSAGE_BOX + " = (" + MESSAGE_BOX + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" +
|
||||
" WHERE " + ID + " = ?", new String[] {id + ""});
|
||||
}
|
||||
|
||||
public void markAsOutbox(long messageId) {
|
||||
@@ -1008,13 +1047,18 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
||||
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.RECEIPT_COUNT));
|
||||
DisplayRecord.Body body = getBody(cursor);
|
||||
int partCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.PART_COUNT));
|
||||
Recipients recipients = getRecipientsFor(address);
|
||||
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.MISMATCHED_IDENTITIES));
|
||||
String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.NETWORK_FAILURE));
|
||||
|
||||
Recipients recipients = getRecipientsFor(address);
|
||||
List<IdentityKeyMismatch> mismatches = getMismatchedIdentities(mismatchDocument);
|
||||
List<NetworkFailure> networkFailures = getFailures(networkDocument);
|
||||
|
||||
ListenableFutureTask<SlideDeck> slideDeck = getSlideDeck(masterSecret, id);
|
||||
|
||||
return new MediaMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(),
|
||||
addressDeviceId, dateSent, dateReceived, receiptCount,
|
||||
threadId, body, slideDeck, partCount, box);
|
||||
threadId, body, slideDeck, partCount, box, mismatches, networkFailures);
|
||||
}
|
||||
|
||||
private Recipients getRecipientsFor(String address) {
|
||||
@@ -1036,6 +1080,30 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
||||
}
|
||||
}
|
||||
|
||||
private List<IdentityKeyMismatch> getMismatchedIdentities(String document) {
|
||||
if (!TextUtils.isEmpty(document)) {
|
||||
try {
|
||||
return JsonUtils.fromJson(document, IdentityKeyMismatchList.class).getList();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
return new LinkedList<>();
|
||||
}
|
||||
|
||||
private List<NetworkFailure> getFailures(String document) {
|
||||
if (!TextUtils.isEmpty(document)) {
|
||||
try {
|
||||
return JsonUtils.fromJson(document, NetworkFailureList.class).getList();
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
}
|
||||
}
|
||||
|
||||
return new LinkedList<>();
|
||||
}
|
||||
|
||||
private DisplayRecord.Body getBody(Cursor cursor) {
|
||||
try {
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.BODY));
|
||||
|
||||
@@ -11,6 +11,7 @@ public interface MmsSmsColumns {
|
||||
public static final String ADDRESS = "address";
|
||||
public static final String ADDRESS_DEVICE_ID = "address_device_id";
|
||||
public static final String RECEIPT_COUNT = "delivery_receipt_count";
|
||||
public static final String MISMATCHED_IDENTITIES = "mismatched_identities";
|
||||
|
||||
public static class Types {
|
||||
protected static final long TOTAL_MASK = 0xFFFFFFFF;
|
||||
@@ -37,6 +38,7 @@ public interface MmsSmsColumns {
|
||||
protected static final long MESSAGE_FORCE_SMS_BIT = 0x40;
|
||||
|
||||
// Key Exchange Information
|
||||
protected static final long KEY_EXCHANGE_MASK = 0xFF00;
|
||||
protected static final long KEY_EXCHANGE_BIT = 0x8000;
|
||||
protected static final long KEY_EXCHANGE_STALE_BIT = 0x4000;
|
||||
protected static final long KEY_EXCHANGE_PROCESSED_BIT = 0x2000;
|
||||
|
||||
@@ -23,8 +23,8 @@ import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
@@ -49,13 +49,39 @@ public class MmsSmsDatabase extends Database {
|
||||
SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
|
||||
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
|
||||
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY,
|
||||
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
|
||||
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT,
|
||||
MmsSmsColumns.MISMATCHED_IDENTITIES,
|
||||
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
|
||||
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
|
||||
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
|
||||
Cursor cursor = queryTables(projection, selection, order, null, null);
|
||||
Cursor cursor = queryTables(projection, selection, selection, order, null, null);
|
||||
setNotifyConverationListeners(cursor, threadId);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public Cursor getIdentityConflictMessagesForThread(long threadId) {
|
||||
String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE,
|
||||
MmsSmsColumns.THREAD_ID,
|
||||
SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT,
|
||||
MmsSmsColumns.NORMALIZED_DATE_SENT,
|
||||
MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
|
||||
MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX,
|
||||
SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
|
||||
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
|
||||
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY,
|
||||
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT,
|
||||
MmsSmsColumns.MISMATCHED_IDENTITIES,
|
||||
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
|
||||
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
|
||||
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.MISMATCHED_IDENTITIES + " IS NOT NULL";
|
||||
|
||||
Cursor cursor = queryTables(projection, selection, selection, order, null, null);
|
||||
setNotifyConverationListeners(cursor, threadId);
|
||||
|
||||
return cursor;
|
||||
@@ -71,12 +97,14 @@ public class MmsSmsDatabase extends Database {
|
||||
SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
|
||||
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
|
||||
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY,
|
||||
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
|
||||
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT,
|
||||
MmsSmsColumns.MISMATCHED_IDENTITIES,
|
||||
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
|
||||
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||
|
||||
return queryTables(projection, selection, order, null, "1");
|
||||
return queryTables(projection, selection, selection, order, null, "1");
|
||||
}
|
||||
|
||||
public Cursor getUnread() {
|
||||
@@ -89,12 +117,14 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.PART_COUNT,
|
||||
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
|
||||
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY,
|
||||
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
|
||||
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT,
|
||||
MmsSmsColumns.MISMATCHED_IDENTITIES,
|
||||
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
|
||||
|
||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
|
||||
String selection = MmsSmsColumns.READ + " = 0";
|
||||
|
||||
return queryTables(projection, selection, order, null, null);
|
||||
return queryTables(projection, selection, selection, order, null, null);
|
||||
}
|
||||
|
||||
public int getConversationCount(long threadId) {
|
||||
@@ -109,7 +139,7 @@ public class MmsSmsDatabase extends Database {
|
||||
DatabaseFactory.getMmsDatabase(context).incrementDeliveryReceiptCount(address, timestamp);
|
||||
}
|
||||
|
||||
private Cursor queryTables(String[] projection, String selection, String order, String groupBy, String limit) {
|
||||
private Cursor queryTables(String[] projection, String smsSelection, String mmsSelection, String order, String groupBy, String limit) {
|
||||
String[] mmsProjection = {MmsDatabase.DATE_SENT + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
|
||||
MmsDatabase.DATE_RECEIVED + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
|
||||
MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
|
||||
@@ -117,7 +147,8 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
|
||||
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
|
||||
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
|
||||
MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
|
||||
MmsSmsColumns.RECEIPT_COUNT, MmsSmsColumns.MISMATCHED_IDENTITIES,
|
||||
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
|
||||
|
||||
String[] smsProjection = {SmsDatabase.DATE_SENT + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
|
||||
SmsDatabase.DATE_RECEIVED + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
|
||||
@@ -126,7 +157,8 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
|
||||
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
|
||||
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
|
||||
MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
|
||||
MmsSmsColumns.RECEIPT_COUNT, MmsSmsColumns.MISMATCHED_IDENTITIES,
|
||||
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
|
||||
|
||||
|
||||
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
|
||||
@@ -146,6 +178,7 @@ public class MmsSmsDatabase extends Database {
|
||||
mmsColumnsPresent.add(MmsSmsColumns.ADDRESS);
|
||||
mmsColumnsPresent.add(MmsSmsColumns.ADDRESS_DEVICE_ID);
|
||||
mmsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT);
|
||||
mmsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES);
|
||||
mmsColumnsPresent.add(MmsDatabase.MESSAGE_TYPE);
|
||||
mmsColumnsPresent.add(MmsDatabase.MESSAGE_BOX);
|
||||
mmsColumnsPresent.add(MmsDatabase.DATE_SENT);
|
||||
@@ -156,6 +189,7 @@ public class MmsSmsDatabase extends Database {
|
||||
mmsColumnsPresent.add(MmsDatabase.MESSAGE_SIZE);
|
||||
mmsColumnsPresent.add(MmsDatabase.EXPIRY);
|
||||
mmsColumnsPresent.add(MmsDatabase.STATUS);
|
||||
mmsColumnsPresent.add(MmsDatabase.NETWORK_FAILURE);
|
||||
|
||||
Set<String> smsColumnsPresent = new HashSet<String>();
|
||||
smsColumnsPresent.add(MmsSmsColumns.ID);
|
||||
@@ -165,14 +199,15 @@ public class MmsSmsDatabase extends Database {
|
||||
smsColumnsPresent.add(MmsSmsColumns.READ);
|
||||
smsColumnsPresent.add(MmsSmsColumns.THREAD_ID);
|
||||
smsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT);
|
||||
smsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES);
|
||||
smsColumnsPresent.add(SmsDatabase.TYPE);
|
||||
smsColumnsPresent.add(SmsDatabase.SUBJECT);
|
||||
smsColumnsPresent.add(SmsDatabase.DATE_SENT);
|
||||
smsColumnsPresent.add(SmsDatabase.DATE_RECEIVED);
|
||||
smsColumnsPresent.add(SmsDatabase.STATUS);
|
||||
|
||||
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 2, MMS_TRANSPORT, selection, null, null, null);
|
||||
String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 2, SMS_TRANSPORT, selection, null, null, null);
|
||||
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 2, MMS_TRANSPORT, mmsSelection, null, null, null);
|
||||
String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 2, SMS_TRANSPORT, smsSelection, null, null, null);
|
||||
|
||||
SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
|
||||
String unionQuery = unionQueryBuilder.buildUnionQuery(new String[] {smsSubQuery, mmsSubQuery}, order, null);
|
||||
|
||||
@@ -28,6 +28,8 @@ import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.model.DisplayRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
@@ -39,9 +41,13 @@ import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.whispersystems.jobqueue.JobManager;
|
||||
import org.whispersystems.textsecure.api.util.InvalidNumberException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.thoughtcrime.securesms.util.Util.canonicalizeNumber;
|
||||
@@ -52,7 +58,9 @@ import static org.thoughtcrime.securesms.util.Util.canonicalizeNumber;
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class SmsDatabase extends Database implements MmsSmsColumns {
|
||||
public class SmsDatabase extends MessagingDatabase {
|
||||
|
||||
private static final String TAG = SmsDatabase.class.getSimpleName();
|
||||
|
||||
public static final String TABLE_NAME = "sms";
|
||||
public static final String PERSON = "person";
|
||||
@@ -70,7 +78,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
||||
DATE_RECEIVED + " INTEGER, " + DATE_SENT + " INTEGER, " + PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " +
|
||||
STATUS + " INTEGER DEFAULT -1," + TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " +
|
||||
RECEIPT_COUNT + " INTEGER DEFAULT 0," + SUBJECT + " TEXT, " + BODY + " TEXT, " +
|
||||
SERVICE_CENTER + " TEXT);";
|
||||
MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + SERVICE_CENTER + " TEXT);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS sms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
|
||||
@@ -85,7 +93,8 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
||||
DATE_RECEIVED + " AS " + NORMALIZED_DATE_RECEIVED,
|
||||
DATE_SENT + " AS " + NORMALIZED_DATE_SENT,
|
||||
PROTOCOL, READ, STATUS, TYPE,
|
||||
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, RECEIPT_COUNT
|
||||
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, RECEIPT_COUNT,
|
||||
MISMATCHED_IDENTITIES
|
||||
};
|
||||
|
||||
private final JobManager jobManager;
|
||||
@@ -95,6 +104,10 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
||||
this.jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
}
|
||||
|
||||
protected String getTableName() {
|
||||
return TABLE_NAME;
|
||||
}
|
||||
|
||||
private void updateTypeBitmask(long id, long maskOff, long maskOn) {
|
||||
Log.w("MessageDatabase", "Updating ID: " + id + " to base type: " + maskOn);
|
||||
|
||||
@@ -162,6 +175,14 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void markAsEndSession(long id) {
|
||||
updateTypeBitmask(id, Types.KEY_EXCHANGE_MASK, Types.END_SESSION_BIT);
|
||||
}
|
||||
|
||||
public void markAsPreKeyBundle(long id) {
|
||||
updateTypeBitmask(id, Types.KEY_EXCHANGE_MASK, Types.KEY_EXCHANGE_BIT | Types.KEY_EXCHANGE_BUNDLE_BIT);
|
||||
}
|
||||
|
||||
public void markAsStaleKeyExchange(long id) {
|
||||
updateTypeBitmask(id, 0, Types.KEY_EXCHANGE_STALE_BIT);
|
||||
}
|
||||
@@ -303,9 +324,9 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
||||
protected void updateMessageBodyAndType(long messageId, String body, long maskOff, long maskOn) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
db.execSQL("UPDATE " + TABLE_NAME + " SET " + BODY + " = ?, " +
|
||||
TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + ") " +
|
||||
"WHERE " + ID + " = ?",
|
||||
new String[] {body, messageId+""});
|
||||
TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + ") " +
|
||||
"WHERE " + ID + " = ?",
|
||||
new String[] {body, messageId + ""});
|
||||
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
|
||||
@@ -487,9 +508,11 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
||||
}
|
||||
|
||||
public Cursor getMessage(long messageId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
return db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[] {messageId+""},
|
||||
null, null, null);
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[]{messageId + ""},
|
||||
null, null, null);
|
||||
setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId));
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public void deleteMessage(long messageId) {
|
||||
@@ -516,7 +539,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
||||
|
||||
where += (" ELSE " + DATE_RECEIVED + " < " + date + " END)");
|
||||
|
||||
db.delete(TABLE_NAME, where, new String[] {threadId+""});
|
||||
db.delete(TABLE_NAME, where, new String[] {threadId + ""});
|
||||
}
|
||||
|
||||
/*package*/ void deleteThreads(Set<Long> threadIds) {
|
||||
@@ -606,14 +629,17 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.THREAD_ID));
|
||||
int status = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.STATUS));
|
||||
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.RECEIPT_COUNT));
|
||||
Recipients recipients = getRecipientsFor(address);
|
||||
DisplayRecord.Body body = getBody(cursor);
|
||||
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.MISMATCHED_IDENTITIES));
|
||||
|
||||
return new SmsMessageRecord(context, messageId, body, recipients,
|
||||
recipients.getPrimaryRecipient(),
|
||||
addressDeviceId,
|
||||
dateSent, dateReceived, receiptCount, type,
|
||||
threadId, status);
|
||||
List<IdentityKeyMismatch> mismatches = getMismatches(mismatchDocument);
|
||||
Recipients recipients = getRecipientsFor(address);
|
||||
DisplayRecord.Body body = getBody(cursor);
|
||||
|
||||
return new SmsMessageRecord(context, messageId, body, recipients,
|
||||
recipients.getPrimaryRecipient(),
|
||||
addressDeviceId,
|
||||
dateSent, dateReceived, receiptCount, type,
|
||||
threadId, status, mismatches);
|
||||
}
|
||||
|
||||
private Recipients getRecipientsFor(String address) {
|
||||
@@ -631,6 +657,18 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
||||
}
|
||||
}
|
||||
|
||||
private List<IdentityKeyMismatch> getMismatches(String document) {
|
||||
try {
|
||||
if (!TextUtils.isEmpty(document)) {
|
||||
return JsonUtils.fromJson(document, IdentityKeyMismatchList.class).getList();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return new LinkedList<>();
|
||||
}
|
||||
|
||||
protected DisplayRecord.Body getBody(Cursor cursor) {
|
||||
long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE));
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.database.documents;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface Document<T> {
|
||||
|
||||
public int size();
|
||||
public List<T> getList();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.thoughtcrime.securesms.database.documents;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class IdentityKeyMismatch {
|
||||
|
||||
private static final String TAG = IdentityKeyMismatch.class.getSimpleName();
|
||||
|
||||
@JsonProperty(value = "r")
|
||||
private long recipientId;
|
||||
|
||||
@JsonProperty(value = "k")
|
||||
@JsonSerialize(using = IdentityKeySerializer.class)
|
||||
@JsonDeserialize(using = IdentityKeyDeserializer.class)
|
||||
private IdentityKey identityKey;
|
||||
|
||||
public IdentityKeyMismatch() {}
|
||||
|
||||
public IdentityKeyMismatch(long recipientId, IdentityKey identityKey) {
|
||||
this.recipientId = recipientId;
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public long getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof IdentityKeyMismatch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IdentityKeyMismatch that = (IdentityKeyMismatch)other;
|
||||
return that.recipientId == this.recipientId && that.identityKey.equals(this.identityKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (int)recipientId ^ identityKey.hashCode();
|
||||
}
|
||||
|
||||
private static class IdentityKeySerializer extends JsonSerializer<IdentityKey> {
|
||||
@Override
|
||||
public void serialize(IdentityKey value, JsonGenerator jsonGenerator, SerializerProvider serializers)
|
||||
throws IOException
|
||||
{
|
||||
jsonGenerator.writeString(Base64.encodeBytes(value.serialize()));
|
||||
}
|
||||
}
|
||||
|
||||
private static class IdentityKeyDeserializer extends JsonDeserializer<IdentityKey> {
|
||||
@Override
|
||||
public IdentityKey deserialize(JsonParser jsonParser, DeserializationContext ctxt)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
return new IdentityKey(Base64.decode(jsonParser.getValueAsString()), 0);
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, e);
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.database.documents;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class IdentityKeyMismatchList implements Document<IdentityKeyMismatch> {
|
||||
|
||||
@JsonProperty(value = "m")
|
||||
private List<IdentityKeyMismatch> mismatches;
|
||||
|
||||
public IdentityKeyMismatchList() {
|
||||
this.mismatches = new LinkedList<>();
|
||||
}
|
||||
|
||||
public IdentityKeyMismatchList(List<IdentityKeyMismatch> mismatches) {
|
||||
this.mismatches = mismatches;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
if (mismatches == null) return 0;
|
||||
else return mismatches.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IdentityKeyMismatch> getList() {
|
||||
return mismatches;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.database.documents;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class NetworkFailure {
|
||||
|
||||
@JsonProperty(value = "r")
|
||||
private long recipientId;
|
||||
|
||||
public NetworkFailure(long recipientId) {
|
||||
this.recipientId = recipientId;
|
||||
}
|
||||
|
||||
public NetworkFailure() {}
|
||||
|
||||
public long getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null || !(other instanceof NetworkFailure)) return false;
|
||||
|
||||
NetworkFailure that = (NetworkFailure)other;
|
||||
return this.recipientId == that.recipientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (int)recipientId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.database.documents;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class NetworkFailureList implements Document<NetworkFailure> {
|
||||
|
||||
@JsonProperty(value = "l")
|
||||
private List<NetworkFailure> failures;
|
||||
|
||||
public NetworkFailureList() {
|
||||
this.failures = new LinkedList<>();
|
||||
}
|
||||
|
||||
public NetworkFailureList(List<NetworkFailure> failures) {
|
||||
this.failures = failures;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
if (failures == null) return 0;
|
||||
else return failures.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@JsonIgnore
|
||||
public List<NetworkFailure> getList() {
|
||||
return failures;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (C) 2015 Open 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
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* 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.database.loaders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
|
||||
|
||||
public class MessageDetailsLoader extends AbstractCursorLoader {
|
||||
private final String type;
|
||||
private final long messageId;
|
||||
|
||||
public MessageDetailsLoader(Context context, String type, long messageId) {
|
||||
super(context);
|
||||
this.type = type;
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
switch (type) {
|
||||
case MmsSmsDatabase.SMS_TRANSPORT:
|
||||
return DatabaseFactory.getEncryptingSmsDatabase(context).getMessage(messageId);
|
||||
case MmsSmsDatabase.MMS_TRANSPORT:
|
||||
return DatabaseFactory.getMmsDatabase(context).getMessage(messageId);
|
||||
default:
|
||||
throw new AssertionError("no valid message type specified");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.mms.MediaNotFoundException;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
@@ -30,6 +32,7 @@ import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.FutureTaskListener;
|
||||
import org.thoughtcrime.securesms.util.ListenableFutureTask;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
@@ -52,10 +55,13 @@ public class MediaMmsMessageRecord extends MessageRecord {
|
||||
long dateSent, long dateReceived, int deliveredCount,
|
||||
long threadId, Body body,
|
||||
ListenableFutureTask<SlideDeck> slideDeck,
|
||||
int partCount, long mailbox)
|
||||
int partCount, long mailbox,
|
||||
List<IdentityKeyMismatch> mismatches,
|
||||
List<NetworkFailure> failures)
|
||||
{
|
||||
super(context, id, body, recipients, individualRecipient, recipientDeviceId,
|
||||
dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, deliveredCount, mailbox);
|
||||
dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, deliveredCount, mailbox,
|
||||
mismatches, failures);
|
||||
|
||||
this.context = context.getApplicationContext();
|
||||
this.partCount = partCount;
|
||||
|
||||
@@ -19,18 +19,20 @@ package org.thoughtcrime.securesms.database.model;
|
||||
import android.content.Context;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TextAppearanceSpan;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The base class for message record models that are displayed in
|
||||
* conversations, as opposed to models that are displayed in a thread list.
|
||||
@@ -48,16 +50,20 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
|
||||
private static final int MAX_DISPLAY_LENGTH = 2000;
|
||||
|
||||
private final Recipient individualRecipient;
|
||||
private final int recipientDeviceId;
|
||||
private final long id;
|
||||
private final int deliveryStatus;
|
||||
private final int receiptCount;
|
||||
private final Recipient individualRecipient;
|
||||
private final int recipientDeviceId;
|
||||
private final long id;
|
||||
private final int deliveryStatus;
|
||||
private final int receiptCount;
|
||||
private final List<IdentityKeyMismatch> mismatches;
|
||||
private final List<NetworkFailure> networkFailures;
|
||||
|
||||
MessageRecord(Context context, long id, Body body, Recipients recipients,
|
||||
Recipient individualRecipient, int recipientDeviceId,
|
||||
long dateSent, long dateReceived, long threadId,
|
||||
int deliveryStatus, int receiptCount, long type)
|
||||
int deliveryStatus, int receiptCount, long type,
|
||||
List<IdentityKeyMismatch> mismatches,
|
||||
List<NetworkFailure> networkFailures)
|
||||
{
|
||||
super(context, body, recipients, dateSent, dateReceived, threadId, type);
|
||||
this.id = id;
|
||||
@@ -65,6 +71,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
this.recipientDeviceId = recipientDeviceId;
|
||||
this.deliveryStatus = deliveryStatus;
|
||||
this.receiptCount = receiptCount;
|
||||
this.mismatches = mismatches;
|
||||
this.networkFailures = networkFailures;
|
||||
}
|
||||
|
||||
public abstract boolean isMms();
|
||||
@@ -145,6 +153,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return SmsDatabase.Types.isPendingSmsFallbackType(type);
|
||||
}
|
||||
|
||||
public boolean isIdentityMismatchFailure() {
|
||||
return mismatches != null && !mismatches.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isPendingSecureSmsFallback() {
|
||||
return SmsDatabase.Types.isPendingSecureSmsFallbackType(type);
|
||||
}
|
||||
@@ -181,6 +193,18 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return type;
|
||||
}
|
||||
|
||||
public List<IdentityKeyMismatch> getIdentityKeyMismatches() {
|
||||
return mismatches;
|
||||
}
|
||||
|
||||
public List<NetworkFailure> getNetworkFailures() {
|
||||
return networkFailures;
|
||||
}
|
||||
|
||||
public boolean hasNetworkFailures() {
|
||||
return networkFailures != null && !networkFailures.isEmpty();
|
||||
}
|
||||
|
||||
protected SpannableString emphasisAdded(String sequence) {
|
||||
SpannableString spannable = new SpannableString(sequence);
|
||||
spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
@@ -199,4 +223,5 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
public int hashCode() {
|
||||
return (int)getId();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,9 +21,13 @@ import android.text.SpannableString;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Represents the message record model for MMS messages that are
|
||||
* notifications (ie: they're pointers to undownloaded media).
|
||||
@@ -47,7 +51,8 @@ public class NotificationMmsMessageRecord extends MessageRecord {
|
||||
long expiry, int status, byte[] transactionId, long mailbox)
|
||||
{
|
||||
super(context, id, new Body("", true), recipients, individualRecipient, recipientDeviceId,
|
||||
dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, receiptCount, mailbox);
|
||||
dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, receiptCount, mailbox,
|
||||
new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>());
|
||||
|
||||
this.contentLocation = contentLocation;
|
||||
this.messageSize = messageSize;
|
||||
|
||||
@@ -23,10 +23,15 @@ import android.text.SpannableString;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.protocol.Tag;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The message record model which represents standard SMS messages.
|
||||
*
|
||||
@@ -43,10 +48,11 @@ public class SmsMessageRecord extends MessageRecord {
|
||||
long dateSent, long dateReceived,
|
||||
int receiptCount,
|
||||
long type, long threadId,
|
||||
int status)
|
||||
int status, List<IdentityKeyMismatch> mismatches)
|
||||
{
|
||||
super(context, id, body, recipients, individualRecipient, recipientDeviceId,
|
||||
dateSent, dateReceived, threadId, receiptCount, getGenericDeliveryStatus(status), type);
|
||||
dateSent, dateReceived, threadId, receiptCount, getGenericDeliveryStatus(status), type,
|
||||
mismatches, new LinkedList<NetworkFailure>());
|
||||
}
|
||||
|
||||
public long getType() {
|
||||
@@ -55,7 +61,9 @@ public class SmsMessageRecord extends MessageRecord {
|
||||
|
||||
@Override
|
||||
public SpannableString getDisplayBody() {
|
||||
if (isProcessedKeyExchange()) {
|
||||
if (SmsDatabase.Types.isFailedDecryptType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
|
||||
} else if (isProcessedKeyExchange()) {
|
||||
return new SpannableString("");
|
||||
} else if (isStaleKeyExchange()) {
|
||||
return emphasisAdded(context.getString(R.string.ConversationItem_error_received_stale_key_exchange_message));
|
||||
@@ -73,8 +81,6 @@ public class SmsMessageRecord extends MessageRecord {
|
||||
return new SpannableString("");
|
||||
} else if (isKeyExchange() && !isOutgoing()) {
|
||||
return emphasisAdded(context.getString(R.string.ConversationItem_received_key_exchange_message_click_to_process));
|
||||
} else if (SmsDatabase.Types.isFailedDecryptType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
|
||||
} else if (SmsDatabase.Types.isDuplicateMessageType(type)) {
|
||||
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
|
||||
} else if (SmsDatabase.Types.isDecryptInProgressType(type)) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.jobqueue.JobParameters;
|
||||
import org.whispersystems.libaxolotl.DuplicateMessageException;
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyIdException;
|
||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||
@@ -37,13 +38,14 @@ import org.whispersystems.libaxolotl.InvalidVersionException;
|
||||
import org.whispersystems.libaxolotl.LegacyMessageException;
|
||||
import org.whispersystems.libaxolotl.NoSessionException;
|
||||
import org.whispersystems.libaxolotl.UntrustedIdentityException;
|
||||
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
|
||||
import org.whispersystems.libaxolotl.state.AxolotlStore;
|
||||
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||
import org.whispersystems.textsecure.api.crypto.TextSecureCipher;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
|
||||
import org.whispersystems.textsecure.api.crypto.TextSecureCipher;
|
||||
|
||||
import ws.com.google.android.mms.MmsException;
|
||||
|
||||
@@ -52,14 +54,20 @@ public class PushDecryptJob extends MasterSecretJob {
|
||||
public static final String TAG = PushDecryptJob.class.getSimpleName();
|
||||
|
||||
private final long messageId;
|
||||
private final long smsMessageId;
|
||||
|
||||
public PushDecryptJob(Context context, long messageId, String sender) {
|
||||
public PushDecryptJob(Context context, long pushMessageId, String sender) {
|
||||
this(context, pushMessageId, -1, sender);
|
||||
}
|
||||
|
||||
public PushDecryptJob(Context context, long pushMessageId, long smsMessageId, String sender) {
|
||||
super(context, JobParameters.newBuilder()
|
||||
.withPersistence()
|
||||
.withRequirement(new MasterSecretRequirement(context))
|
||||
.withGroupId(sender)
|
||||
.create());
|
||||
this.messageId = messageId;
|
||||
this.messageId = pushMessageId;
|
||||
this.smsMessageId = smsMessageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,7 +82,7 @@ public class PushDecryptJob extends MasterSecretJob {
|
||||
PushDatabase database = DatabaseFactory.getPushDatabase(context);
|
||||
TextSecureEnvelope envelope = database.get(messageId);
|
||||
|
||||
handleMessage(masterSecret, envelope);
|
||||
handleMessage(masterSecret, envelope, smsMessageId);
|
||||
database.delete(messageId);
|
||||
}
|
||||
|
||||
@@ -88,7 +96,7 @@ public class PushDecryptJob extends MasterSecretJob {
|
||||
|
||||
}
|
||||
|
||||
private void handleMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
|
||||
private void handleMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
|
||||
try {
|
||||
Recipients recipients = RecipientFactory.getRecipientsFromString(context, envelope.getSource(), false);
|
||||
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
|
||||
@@ -98,59 +106,72 @@ public class PushDecryptJob extends MasterSecretJob {
|
||||
|
||||
TextSecureMessage message = cipher.decrypt(envelope);
|
||||
|
||||
if (message.isEndSession()) handleEndSessionMessage(masterSecret, recipientId, envelope, message);
|
||||
else if (message.isGroupUpdate()) handleGroupMessage(masterSecret, envelope, message);
|
||||
else if (message.getAttachments().isPresent()) handleMediaMessage(masterSecret, envelope, message);
|
||||
else handleTextMessage(masterSecret, envelope, message);
|
||||
if (message.isEndSession()) handleEndSessionMessage(masterSecret, recipientId, envelope, message, smsMessageId);
|
||||
else if (message.isGroupUpdate()) handleGroupMessage(masterSecret, envelope, message, smsMessageId);
|
||||
else if (message.getAttachments().isPresent()) handleMediaMessage(masterSecret, envelope, message, smsMessageId);
|
||||
else handleTextMessage(masterSecret, envelope, message, smsMessageId);
|
||||
|
||||
if (envelope.isPreKeyWhisperMessage()) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob(context));
|
||||
}
|
||||
} catch (InvalidVersionException e) {
|
||||
Log.w(TAG, e);
|
||||
handleInvalidVersionMessage(masterSecret, envelope);
|
||||
handleInvalidVersionMessage(masterSecret, envelope, smsMessageId);
|
||||
} catch (InvalidMessageException | InvalidKeyIdException | InvalidKeyException | MmsException | RecipientFormattingException e) {
|
||||
Log.w(TAG, e);
|
||||
handleCorruptMessage(masterSecret, envelope);
|
||||
handleCorruptMessage(masterSecret, envelope, smsMessageId);
|
||||
} catch (NoSessionException e) {
|
||||
Log.w(TAG, e);
|
||||
handleNoSessionMessage(masterSecret, envelope);
|
||||
handleNoSessionMessage(masterSecret, envelope, smsMessageId);
|
||||
} catch (LegacyMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
handleLegacyMessage(masterSecret, envelope);
|
||||
handleLegacyMessage(masterSecret, envelope, smsMessageId);
|
||||
} catch (DuplicateMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
handleDuplicateMessage(masterSecret, envelope);
|
||||
handleDuplicateMessage(masterSecret, envelope, smsMessageId);
|
||||
} catch (UntrustedIdentityException e) {
|
||||
Log.w(TAG, e);
|
||||
handleUntrustedIdentityMessage(masterSecret, envelope);
|
||||
handleUntrustedIdentityMessage(masterSecret, envelope, smsMessageId);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEndSessionMessage(MasterSecret masterSecret, long recipientId,
|
||||
TextSecureEnvelope envelope, TextSecureMessage message)
|
||||
TextSecureEnvelope envelope, TextSecureMessage message,
|
||||
long smsMessageId)
|
||||
{
|
||||
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(envelope.getSource(),
|
||||
envelope.getSourceDevice(),
|
||||
message.getTimestamp(),
|
||||
"", Optional.<TextSecureGroup>absent());
|
||||
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(envelope.getSource(),
|
||||
envelope.getSourceDevice(),
|
||||
message.getTimestamp(),
|
||||
"", Optional.<TextSecureGroup>absent());
|
||||
|
||||
IncomingEndSessionMessage incomingEndSessionMessage = new IncomingEndSessionMessage(incomingTextMessage);
|
||||
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, incomingEndSessionMessage);
|
||||
long threadId;
|
||||
|
||||
if (smsMessageId <= 0) {
|
||||
IncomingEndSessionMessage incomingEndSessionMessage = new IncomingEndSessionMessage(incomingTextMessage);
|
||||
Pair<Long, Long> messageAndThreadId = smsDatabase.insertMessageInbox(masterSecret, incomingEndSessionMessage);
|
||||
threadId = messageAndThreadId.second;
|
||||
} else {
|
||||
smsDatabase.markAsEndSession(smsMessageId);
|
||||
threadId = smsDatabase.getThreadIdForMessage(smsMessageId);
|
||||
}
|
||||
|
||||
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||
sessionStore.deleteAllSessions(recipientId);
|
||||
|
||||
SecurityEvent.broadcastSecurityUpdateEvent(context, messageAndThreadId.second);
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
SecurityEvent.broadcastSecurityUpdateEvent(context, threadId);
|
||||
MessageNotifier.updateNotification(context, masterSecret, threadId);
|
||||
}
|
||||
|
||||
private void handleGroupMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureMessage message) {
|
||||
private void handleGroupMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureMessage message, long smsMessageId) {
|
||||
GroupMessageProcessor.process(context, masterSecret, envelope, message);
|
||||
|
||||
if (smsMessageId > 0) {
|
||||
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMediaMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureMessage message)
|
||||
private void handleMediaMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureMessage message, long smsMessageId)
|
||||
throws MmsException
|
||||
{
|
||||
String localNumber = TextSecurePreferences.getLocalNumber(context);
|
||||
@@ -174,73 +195,123 @@ public class PushDecryptJob extends MasterSecretJob {
|
||||
.getJobManager()
|
||||
.add(new AttachmentDownloadJob(context, messageAndThreadId.first));
|
||||
|
||||
if (smsMessageId >= 0) {
|
||||
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId);
|
||||
}
|
||||
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
}
|
||||
|
||||
private void handleTextMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureMessage message) {
|
||||
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
String body = message.getBody().isPresent() ? message.getBody().get() : "";
|
||||
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(),
|
||||
private void handleTextMessage(MasterSecret masterSecret, TextSecureEnvelope envelope,
|
||||
TextSecureMessage message, long smsMessageId)
|
||||
{
|
||||
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
String body = message.getBody().isPresent() ? message.getBody().get() : "";
|
||||
|
||||
if (smsMessageId > 0) {
|
||||
database.updateBundleMessageBody(masterSecret, smsMessageId, body);
|
||||
} else {
|
||||
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(),
|
||||
envelope.getSourceDevice(),
|
||||
message.getTimestamp(), body,
|
||||
message.getGroupInfo());
|
||||
|
||||
if (message.isSecure()) {
|
||||
textMessage = new IncomingEncryptedMessage(textMessage, body);
|
||||
if (message.isSecure()) {
|
||||
textMessage = new IncomingEncryptedMessage(textMessage, body);
|
||||
}
|
||||
|
||||
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, textMessage);
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
}
|
||||
|
||||
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, textMessage);
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
}
|
||||
|
||||
private void handleInvalidVersionMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
|
||||
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
|
||||
DatabaseFactory.getEncryptingSmsDatabase(context).markAsInvalidVersionKeyExchange(messageAndThreadId.first);
|
||||
private void handleInvalidVersionMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
|
||||
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
if (smsMessageId <= 0) {
|
||||
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
|
||||
smsDatabase.markAsInvalidVersionKeyExchange(messageAndThreadId.first);
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
} else {
|
||||
smsDatabase.markAsInvalidVersionKeyExchange(smsMessageId);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCorruptMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
|
||||
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
|
||||
DatabaseFactory.getEncryptingSmsDatabase(context).markAsDecryptFailed(messageAndThreadId.first);
|
||||
private void handleCorruptMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
|
||||
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
if (smsMessageId <= 0) {
|
||||
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
|
||||
smsDatabase.markAsDecryptFailed(messageAndThreadId.first);
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
} else {
|
||||
smsDatabase.markAsDecryptFailed(smsMessageId);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleNoSessionMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
|
||||
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
|
||||
DatabaseFactory.getEncryptingSmsDatabase(context).markAsNoSession(messageAndThreadId.first);
|
||||
private void handleNoSessionMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
|
||||
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
if (smsMessageId <= 0) {
|
||||
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
|
||||
smsDatabase.markAsNoSession(messageAndThreadId.first);
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
} else {
|
||||
smsDatabase.markAsNoSession(smsMessageId);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLegacyMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
|
||||
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
|
||||
DatabaseFactory.getEncryptingSmsDatabase(context).markAsLegacyVersion(messageAndThreadId.first);
|
||||
private void handleLegacyMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
|
||||
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
if (smsMessageId <= 0) {
|
||||
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
|
||||
smsDatabase.markAsLegacyVersion(messageAndThreadId.first);
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
} else {
|
||||
smsDatabase.markAsLegacyVersion(smsMessageId);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDuplicateMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
|
||||
// Let's start ignoring these now.
|
||||
|
||||
// Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
|
||||
// DatabaseFactory.getEncryptingSmsDatabase(context).markAsDecryptDuplicate(messageAndThreadId.first);
|
||||
private void handleDuplicateMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
|
||||
// Let's start ignoring these now
|
||||
// SmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
//
|
||||
// MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
// if (smsMessageId <= 0) {
|
||||
// Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
|
||||
// smsDatabase.markAsDecryptDuplicate(messageAndThreadId.first);
|
||||
// MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
// } else {
|
||||
// smsDatabase.markAsDecryptDuplicate(smsMessageId);
|
||||
// }
|
||||
}
|
||||
|
||||
private void handleUntrustedIdentityMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
|
||||
String encoded = Base64.encodeBytes(envelope.getMessage());
|
||||
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
|
||||
envelope.getTimestamp(), encoded,
|
||||
Optional.<TextSecureGroup>absent());
|
||||
private void handleUntrustedIdentityMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
|
||||
try {
|
||||
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
Recipients recipients = RecipientFactory.getRecipientsFromString(context, envelope.getSource(), false);
|
||||
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
|
||||
PreKeyWhisperMessage whisperMessage = new PreKeyWhisperMessage(envelope.getMessage());
|
||||
IdentityKey identityKey = whisperMessage.getIdentityKey();
|
||||
String encoded = Base64.encodeBytes(envelope.getMessage());
|
||||
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
|
||||
envelope.getTimestamp(), encoded,
|
||||
Optional.<TextSecureGroup>absent());
|
||||
|
||||
IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded);
|
||||
Pair<Long, Long> messageAndThreadId = DatabaseFactory.getEncryptingSmsDatabase(context)
|
||||
.insertMessageInbox(masterSecret, bundleMessage);
|
||||
if (smsMessageId <= 0) {
|
||||
IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded);
|
||||
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, bundleMessage);
|
||||
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
database.addMismatchedIdentity(messageAndThreadId.first, recipientId, identityKey);
|
||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||
} else {
|
||||
database.updateMessageBody(masterSecret, smsMessageId, encoded);
|
||||
database.markAsPreKeyBundle(smsMessageId);
|
||||
database.addMismatchedIdentity(smsMessageId, recipientId, identityKey);
|
||||
}
|
||||
} catch (RecipientFormattingException | InvalidMessageException | InvalidVersionException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Pair<Long, Long> insertPlaceholder(MasterSecret masterSecret, TextSecureEnvelope envelope) {
|
||||
|
||||
@@ -8,13 +8,14 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
|
||||
import org.thoughtcrime.securesms.mms.PartParser;
|
||||
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.sms.IncomingIdentityUpdateMessage;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.jobqueue.JobParameters;
|
||||
@@ -25,9 +26,11 @@ import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
|
||||
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
|
||||
import org.whispersystems.textsecure.api.push.PushAddress;
|
||||
import org.whispersystems.textsecure.internal.push.PushMessageProtos;
|
||||
import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
|
||||
import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException;
|
||||
import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException;
|
||||
import org.whispersystems.textsecure.api.util.InvalidNumberException;
|
||||
import org.whispersystems.textsecure.internal.push.PushMessageProtos;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
@@ -47,8 +50,9 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
@Inject transient TextSecureMessageSenderFactory messageSenderFactory;
|
||||
|
||||
private final long messageId;
|
||||
private final long filterRecipientId;
|
||||
|
||||
public PushGroupSendJob(Context context, long messageId, String destination) {
|
||||
public PushGroupSendJob(Context context, long messageId, String destination, long filterRecipientId) {
|
||||
super(context, JobParameters.newBuilder()
|
||||
.withPersistence()
|
||||
.withGroupId(destination)
|
||||
@@ -57,21 +61,25 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
.withRetryCount(5)
|
||||
.create());
|
||||
|
||||
this.messageId = messageId;
|
||||
this.messageId = messageId;
|
||||
this.filterRecipientId = filterRecipientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdded() {
|
||||
|
||||
DatabaseFactory.getMmsDatabase(context)
|
||||
.markAsSending(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSend(MasterSecret masterSecret) throws MmsException, IOException, NoSuchMessageException {
|
||||
public void onSend(MasterSecret masterSecret)
|
||||
throws MmsException, IOException, NoSuchMessageException, RecipientFormattingException
|
||||
{
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
|
||||
|
||||
try {
|
||||
deliver(masterSecret, message);
|
||||
deliver(masterSecret, message, filterRecipientId);
|
||||
|
||||
database.markAsPush(messageId);
|
||||
database.markAsSecure(messageId);
|
||||
@@ -82,16 +90,27 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
} catch (EncapsulatedExceptions e) {
|
||||
Log.w(TAG, e);
|
||||
if (!e.getUnregisteredUserExceptions().isEmpty()) {
|
||||
database.markAsSentFailed(messageId);
|
||||
List<NetworkFailure> failures = new LinkedList<>();
|
||||
|
||||
for (NetworkFailureException nfe : e.getNetworkExceptions()) {
|
||||
Recipient recipient = RecipientFactory.getRecipientsFromString(context, nfe.getE164number(), false).getPrimaryRecipient();
|
||||
failures.add(new NetworkFailure(recipient.getRecipientId()));
|
||||
}
|
||||
|
||||
// for (UnregisteredUserException uue : e.getUnregisteredUserExceptions()) {
|
||||
// Recipient recipient = RecipientFactory.getRecipientsFromString(context, uue.getE164Number(), false).getPrimaryRecipient();
|
||||
// failures.add(new NetworkFailure(recipient.getRecipientId(), NetworkFailure.UNREGISTERED_FAILURE));
|
||||
// }
|
||||
|
||||
for (UntrustedIdentityException uie : e.getUntrustedIdentityExceptions()) {
|
||||
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(message.getTo()[0].getString(), uie.getIdentityKey());
|
||||
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
|
||||
database.markAsSentFailed(messageId);
|
||||
Recipient recipient = RecipientFactory.getRecipientsFromString(context, uie.getE164Number(), false).getPrimaryRecipient();
|
||||
database.addMismatchedIdentity(messageId, recipient.getRecipientId(), uie.getIdentityKey());
|
||||
}
|
||||
|
||||
database.addFailures(messageId, failures);
|
||||
database.markAsSentFailed(messageId);
|
||||
database.markAsPush(messageId);
|
||||
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
}
|
||||
@@ -107,14 +126,17 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
|
||||
}
|
||||
|
||||
private void deliver(MasterSecret masterSecret, SendReq message)
|
||||
private void deliver(MasterSecret masterSecret, SendReq message, long filterRecipientId)
|
||||
throws IOException, RecipientFormattingException, InvalidNumberException, EncapsulatedExceptions
|
||||
{
|
||||
TextSecureMessageSender messageSender = messageSenderFactory.create(masterSecret);
|
||||
byte[] groupId = GroupUtil.getDecodedId(message.getTo()[0].getString());
|
||||
Recipients recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
|
||||
List<PushAddress> addresses = getPushAddresses(recipients);
|
||||
List<TextSecureAttachment> attachments = getAttachments(masterSecret, message);
|
||||
List<PushAddress> addresses;
|
||||
|
||||
if (filterRecipientId >= 0) addresses = getPushAddresses(filterRecipientId);
|
||||
else addresses = getPushAddresses(recipients);
|
||||
|
||||
if (MmsSmsColumns.Types.isGroupUpdate(message.getDatabaseMessageBox()) ||
|
||||
MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox()))
|
||||
@@ -149,4 +171,10 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
return addresses;
|
||||
}
|
||||
|
||||
private List<PushAddress> getPushAddresses(long filterRecipientId) throws InvalidNumberException {
|
||||
List<PushAddress> addresses = new LinkedList<>();
|
||||
addresses.add(getPushAddress(RecipientFactory.getRecipientForId(context, filterRecipientId, false)));
|
||||
return addresses;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.PartParser;
|
||||
@@ -16,7 +17,6 @@ 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.sms.IncomingIdentityUpdateMessage;
|
||||
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException;
|
||||
@@ -55,12 +55,15 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
@Override
|
||||
public void onAdded() {
|
||||
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
mmsDatabase.markAsSending(messageId);
|
||||
mmsDatabase.markAsPush(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSend(MasterSecret masterSecret)
|
||||
throws RetryLaterException, MmsException, NoSuchMessageException, UndeliverableMessageException
|
||||
throws RetryLaterException, MmsException, NoSuchMessageException,
|
||||
UndeliverableMessageException, RecipientFormattingException
|
||||
{
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
|
||||
@@ -80,9 +83,13 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
|
||||
database.markAsPendingSecureSmsFallback(messageId);
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
} catch (UntrustedIdentityException uie) {
|
||||
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(message.getTo()[0].getString(), uie.getIdentityKey());
|
||||
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
|
||||
Log.w(TAG, uie);
|
||||
Recipients recipients = RecipientFactory.getRecipientsFromString(context, uie.getE164Number(), false);
|
||||
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
|
||||
|
||||
database.addMismatchedIdentity(messageId, recipientId, uie.getIdentityKey());
|
||||
database.markAsSentFailed(messageId);
|
||||
database.markAsPush(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@ import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.InjectableType;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
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.sms.IncomingIdentityUpdateMessage;
|
||||
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException;
|
||||
@@ -47,14 +49,16 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
|
||||
|
||||
@Override
|
||||
public void onAdded() {
|
||||
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
smsDatabase.markAsSending(messageId);
|
||||
smsDatabase.markAsPush(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSend(MasterSecret masterSecret) throws NoSuchMessageException, RetryLaterException {
|
||||
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
|
||||
String destination = record.getIndividualRecipient().getNumber();
|
||||
public void onSend(MasterSecret masterSecret) throws NoSuchMessageException, RetryLaterException, RecipientFormattingException {
|
||||
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
|
||||
String destination = record.getIndividualRecipient().getNumber();
|
||||
|
||||
try {
|
||||
Log.w(TAG, "Sending message: " + messageId);
|
||||
@@ -74,9 +78,12 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
|
||||
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
|
||||
} catch (UntrustedIdentityException e) {
|
||||
Log.w(TAG, e);
|
||||
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(e.getE164Number(), e.getIdentityKey());
|
||||
database.insertMessageInbox(masterSecret, identityUpdateMessage);
|
||||
Recipients recipients = RecipientFactory.getRecipientsFromString(context, e.getE164Number(), false);
|
||||
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
|
||||
|
||||
database.addMismatchedIdentity(record.getId(), recipientId, e.getIdentityKey());
|
||||
database.markAsSentFailed(record.getId());
|
||||
database.markAsPush(record.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class Recipients {
|
||||
public class Recipients implements Iterable<Recipient> {
|
||||
|
||||
private List<Recipient> recipients;
|
||||
|
||||
@@ -165,4 +165,9 @@ public class Recipients {
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Recipient> iterator() {
|
||||
return recipients.iterator();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,16 +111,24 @@ public class MessageSender {
|
||||
}
|
||||
}
|
||||
|
||||
public static void resendGroupMessage(Context context, MasterSecret masterSecret, MessageRecord messageRecord, long filterRecipientId) {
|
||||
if (!messageRecord.isMms()) throw new AssertionError("Not Group");
|
||||
|
||||
Recipients recipients = DatabaseFactory.getMmsAddressDatabase(context).getRecipientsForId(messageRecord.getId());
|
||||
sendGroupPush(context, recipients, messageRecord.getId(), filterRecipientId);
|
||||
}
|
||||
|
||||
public static void resend(Context context, MasterSecret masterSecret, MessageRecord messageRecord) {
|
||||
try {
|
||||
Recipients recipients = messageRecord.getRecipients();
|
||||
long messageId = messageRecord.getId();
|
||||
boolean forceSms = messageRecord.isForcedSms();
|
||||
boolean keyExchange = messageRecord.isKeyExchange();
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
Recipients recipients = DatabaseFactory.getMmsAddressDatabase(context).getRecipientsForId(messageId);
|
||||
sendMediaMessage(context, masterSecret, recipients, forceSms, messageId);
|
||||
} else {
|
||||
Recipients recipients = messageRecord.getRecipients();
|
||||
sendTextMessage(context, recipients, forceSms, keyExchange, messageId);
|
||||
}
|
||||
} catch (MmsException e) {
|
||||
@@ -135,7 +143,7 @@ public class MessageSender {
|
||||
if (!forceSms && isSelfSend(context, recipients)) {
|
||||
sendMediaSelf(context, masterSecret, messageId);
|
||||
} else if (isGroupPushSend(recipients)) {
|
||||
sendGroupPush(context, recipients, messageId);
|
||||
sendGroupPush(context, recipients, messageId, -1);
|
||||
} else if (!forceSms && isPushMediaSend(context, recipients)) {
|
||||
sendMediaPush(context, recipients, messageId);
|
||||
} else {
|
||||
@@ -186,9 +194,9 @@ public class MessageSender {
|
||||
jobManager.add(new PushMediaSendJob(context, messageId, recipients.getPrimaryRecipient().getNumber()));
|
||||
}
|
||||
|
||||
private static void sendGroupPush(Context context, Recipients recipients, long messageId) {
|
||||
private static void sendGroupPush(Context context, Recipients recipients, long messageId, long filterRecipientId) {
|
||||
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
|
||||
jobManager.add(new PushGroupSendJob(context, messageId, recipients.getPrimaryRecipient().getNumber()));
|
||||
jobManager.add(new PushGroupSendJob(context, messageId, recipients.getPrimaryRecipient().getNumber(), filterRecipientId));
|
||||
}
|
||||
|
||||
private static void sendSms(Context context, Recipients recipients, long messageId) {
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.format.DateFormat;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
@@ -71,4 +75,17 @@ public class DateUtils extends android.text.format.DateUtils {
|
||||
return DateUtils.formatDateTime(c, timestamp, formatFlags);
|
||||
}
|
||||
}
|
||||
|
||||
public static SimpleDateFormat getDetailedDateFormatter(Context context) {
|
||||
String dateFormatPattern;
|
||||
|
||||
if (DateFormat.is24HourFormat(context)) {
|
||||
dateFormatPattern = "MMM d, yyyy HH:mm:ss zzz";
|
||||
} else {
|
||||
dateFormatPattern = "MMM d, yyyy hh:mm:ssa zzz";
|
||||
}
|
||||
|
||||
return new SimpleDateFormat(dateFormatPattern, Locale.getDefault());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,14 +19,12 @@ import android.util.Pair;
|
||||
import android.util.SparseArray;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.thoughtcrimegson.Gson;
|
||||
import com.google.thoughtcrimegson.reflect.TypeToken;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -35,6 +33,8 @@ import java.util.regex.Pattern;
|
||||
|
||||
public class Emoji {
|
||||
|
||||
private static final String TAG = Emoji.class.getSimpleName();
|
||||
|
||||
private static ExecutorService executor = Util.newSingleThreadedLifoExecutor();
|
||||
|
||||
public static final int[][] PAGES = {
|
||||
@@ -303,9 +303,16 @@ public class Emoji {
|
||||
}
|
||||
|
||||
String serialized = prefs.getString(EMOJI_LRU_PREFERENCE, "[]");
|
||||
Type type = new TypeToken<LinkedHashSet<String>>() {
|
||||
}.getType();
|
||||
recentlyUsed = new Gson().fromJson(serialized, type);
|
||||
|
||||
try {
|
||||
recentlyUsed = JsonUtils.getMapper().readValue(serialized, TypeFactory.defaultInstance()
|
||||
.constructCollectionType(LinkedHashSet.class, String.class));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
recentlyUsed = new LinkedHashSet<>();
|
||||
}
|
||||
|
||||
recentlyUsed = new LinkedHashSet<>();
|
||||
}
|
||||
|
||||
public static String[] getRecentlyUsed(Context context) {
|
||||
@@ -337,10 +344,15 @@ public class Emoji {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
String serialized = new Gson().toJson(latestRecentlyUsed);
|
||||
prefs.edit()
|
||||
.putString(EMOJI_LRU_PREFERENCE, serialized)
|
||||
.apply();
|
||||
try {
|
||||
String serialized = JsonUtils.toJson(latestRecentlyUsed);
|
||||
prefs.edit()
|
||||
.putString(EMOJI_LRU_PREFERENCE, serialized)
|
||||
.apply();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
|
||||
33
src/org/thoughtcrime/securesms/util/JsonUtils.java
Normal file
33
src/org/thoughtcrime/securesms/util/JsonUtils.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class JsonUtils {
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(String serialized, Class<T> clazz) throws IOException {
|
||||
return objectMapper.readValue(serialized, clazz);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(InputStreamReader serialized, Class<T> clazz) throws IOException {
|
||||
return objectMapper.readValue(serialized, clazz);
|
||||
}
|
||||
|
||||
public static String toJson(Object object) throws IOException {
|
||||
return objectMapper.writeValueAsString(object);
|
||||
}
|
||||
|
||||
public static ObjectMapper getMapper() {
|
||||
return objectMapper;
|
||||
}
|
||||
}
|
||||
83
src/org/thoughtcrime/securesms/util/RecipientViewUtil.java
Normal file
83
src/org/thoughtcrime/securesms/util/RecipientViewUtil.java
Normal file
@@ -0,0 +1,83 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.provider.Contacts.Intents;
|
||||
import android.provider.ContactsContract.QuickContact;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
|
||||
public class RecipientViewUtil {
|
||||
public static CharSequence formatFrom(Context context, Recipient recipient) {
|
||||
return formatFrom(context, new Recipients(recipient));
|
||||
}
|
||||
|
||||
public static CharSequence formatFrom(Context context, Recipients from) {
|
||||
return formatFrom(context, from, true);
|
||||
}
|
||||
|
||||
public static CharSequence formatFrom(Context context, Recipients from, boolean read) {
|
||||
int attributes[] = new int[] {R.attr.conversation_list_item_count_color};
|
||||
TypedArray colors = context.obtainStyledAttributes(attributes);
|
||||
|
||||
final String fromString;
|
||||
final boolean isUnnamedGroup = from.isGroupRecipient() && TextUtils.isEmpty(from.getPrimaryRecipient().getName());
|
||||
if (isUnnamedGroup) {
|
||||
fromString = context.getString(R.string.ConversationActivity_unnamed_group);
|
||||
} else {
|
||||
fromString = from.toShortString();
|
||||
}
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(fromString);
|
||||
|
||||
final int typeface;
|
||||
if (isUnnamedGroup) {
|
||||
if (!read) typeface = Typeface.BOLD_ITALIC;
|
||||
else typeface = Typeface.ITALIC;
|
||||
} else if (!read) {
|
||||
typeface = Typeface.BOLD;
|
||||
} else {
|
||||
typeface = Typeface.NORMAL;
|
||||
}
|
||||
|
||||
builder.setSpan(new StyleSpan(typeface), 0, builder.length(),
|
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
|
||||
colors.recycle();
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static void setContactPhoto(final Context context, final ImageView imageView, final Recipient recipient, boolean showQuickContact) {
|
||||
if (recipient == null) return;
|
||||
|
||||
imageView.setImageBitmap(recipient.getContactPhoto());
|
||||
|
||||
if (!recipient.isGroupRecipient() && showQuickContact) {
|
||||
imageView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (recipient.getContactUri() != null) {
|
||||
QuickContact.showQuickContact(context, imageView, recipient.getContactUri(), QuickContact.MODE_LARGE, null);
|
||||
} else {
|
||||
Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, Uri.fromParts("tel", recipient.getNumber(), null));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
imageView.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user