diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 58a6883244..a134836939 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -94,7 +94,6 @@ import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.Slide; -import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.providers.CaptureProvider; import org.thoughtcrime.securesms.recipients.Recipient; @@ -563,8 +562,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity .setType(GroupContext.Type.QUIT) .build(); - OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(self, getRecipients(), - context, null); + OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(getRecipients(), context, null); MessageSender.send(self, masterSecret, outgoingMessage, threadId, false); DatabaseFactory.getGroupDatabase(self).remove(groupId, TextSecurePreferences.getLocalNumber(self)); initializeEnabledCheck(); @@ -1246,30 +1244,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private void sendMediaMessage(final boolean forceSms) throws InvalidMessageException { - final Context context = getApplicationContext(); - SlideDeck slideDeck; - - if (attachmentManager.isAttachmentPresent()) { - Slide mediaSlide = attachmentManager.getSlideDeck().getThumbnailSlide(); - MediaConstraints constraints = getCurrentMediaConstraints(); - - if (mediaSlide != null && - !constraints.isSatisfied(this, masterSecret, mediaSlide.getPart()) && - !constraints.canResize(mediaSlide.getPart())) - { - Toast.makeText(context, - R.string.ConversationActivity_attachment_exceeds_size_limits, - Toast.LENGTH_SHORT).show(); - return; - } - - slideDeck = new SlideDeck(attachmentManager.getSlideDeck()); - } else { - slideDeck = new SlideDeck(); - } - - OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(this, recipients, slideDeck, - getMessage(), distributionType); + final Context context = getApplicationContext(); + OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(recipients, + attachmentManager.getSlideDeck(), + getMessage(), + System.currentTimeMillis(), + distributionType); if (isSecureText && !forceSms) { outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessage); diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index 654395fa1e..48c0e62c54 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -47,7 +47,7 @@ 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.PartDatabase; +import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; @@ -262,10 +262,10 @@ public class ConversationItem extends LinearLayout setNotificationMmsAttributes((NotificationMmsMessageRecord) messageRecord); } else if (hasMedia(messageRecord)) { mediaThumbnail.setVisibility(View.VISIBLE); - mediaThumbnail.setImageResource(masterSecret, messageRecord.getId(), - messageRecord.getDateReceived(), - ((MediaMmsMessageRecord)messageRecord).getSlideDeckFuture()); - mediaThumbnail.hideControls(messageRecord.isFailed() || (messageRecord.isOutgoing() && !messageRecord.isPending())); + mediaThumbnail.setImageResource(masterSecret, + ((MediaMmsMessageRecord)messageRecord).getSlideDeckFuture(), + !messageRecord.isFailed() && (!messageRecord.isOutgoing() || messageRecord.isPending()), + false); bodyText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); } else { mediaThumbnail.setVisibility(View.GONE); @@ -401,7 +401,9 @@ public class ConversationItem extends LinearLayout private class ThumbnailDownloadClickListener implements ThumbnailView.ThumbnailClickListener { @Override public void onClick(View v, final Slide slide) { - DatabaseFactory.getPartDatabase(context).setTransferState(messageRecord.getId(), slide.getPart().getPartId(), PartDatabase.TRANSFER_PROGRESS_STARTED); + DatabaseFactory.getAttachmentDatabase(context).setTransferState(messageRecord.getId(), + slide.asAttachment(), + AttachmentDatabase.TRANSFER_PROGRESS_STARTED); } } @@ -410,7 +412,7 @@ public class ConversationItem extends LinearLayout Log.w(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType()); Intent intent = new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setDataAndType(PartAuthority.getPublicPartUri(slide.getUri()), slide.getContentType()); + intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), slide.getContentType()); try { context.startActivity(intent); } catch (ActivityNotFoundException anfe) { diff --git a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java index c2679783f0..9f4f30baf5 100644 --- a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java +++ b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java @@ -27,6 +27,7 @@ import android.util.Log; import android.view.View; import android.widget.ProgressBar; +import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore; @@ -34,7 +35,7 @@ import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase.Reader; -import org.thoughtcrime.securesms.database.PartDatabase; +import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; @@ -51,8 +52,6 @@ import java.util.List; import java.util.SortedSet; import java.util.TreeSet; -import ws.com.google.android.mms.pdu.PduPart; - public class DatabaseUpgradeActivity extends BaseActivity { private static final String TAG = DatabaseUpgradeActivity.class.getSimpleName(); @@ -236,23 +235,23 @@ public class DatabaseUpgradeActivity extends BaseActivity { } private void schedulePendingIncomingParts(Context context) { - final PartDatabase partDb = DatabaseFactory.getPartDatabase(context); - final MmsDatabase mmsDb = DatabaseFactory.getMmsDatabase(context); - final List pendingParts = DatabaseFactory.getPartDatabase(context).getPendingParts(); + final AttachmentDatabase attachmentDb = DatabaseFactory.getAttachmentDatabase(context); + final MmsDatabase mmsDb = DatabaseFactory.getMmsDatabase(context); + final List pendingAttachments = DatabaseFactory.getAttachmentDatabase(context).getPendingAttachments(); - Log.w(TAG, pendingParts.size() + " pending parts."); - for (PduPart part : pendingParts) { - final Reader reader = mmsDb.readerFor(masterSecret, mmsDb.getMessage(part.getMmsId())); + Log.w(TAG, pendingAttachments.size() + " pending parts."); + for (DatabaseAttachment attachment : pendingAttachments) { + final Reader reader = mmsDb.readerFor(masterSecret, mmsDb.getMessage(attachment.getMmsId())); final MessageRecord record = reader.getNext(); - if (part.getDataUri() != null) { - Log.w(TAG, "corrected a pending media part " + part.getPartId() + "that already had data."); - partDb.setTransferState(part.getMmsId(), part.getPartId(), PartDatabase.TRANSFER_PROGRESS_DONE); + if (attachment.hasData()) { + Log.w(TAG, "corrected a pending media part " + attachment.getAttachmentId() + "that already had data."); + attachmentDb.setTransferState(attachment.getMmsId(), attachment.getAttachmentId(), AttachmentDatabase.TRANSFER_PROGRESS_DONE); } else if (record != null && !record.isOutgoing() && record.isPush()) { - Log.w(TAG, "queuing new attachment download job for incoming push part " + part.getPartId() + "."); + Log.w(TAG, "queuing new attachment download job for incoming push part " + attachment.getAttachmentId() + "."); ApplicationContext.getInstance(context) .getJobManager() - .add(new AttachmentDownloadJob(context, part.getMmsId(), part.getPartId())); + .add(new AttachmentDownloadJob(context, attachment.getMmsId(), attachment.getAttachmentId())); } reader.close(); } diff --git a/src/org/thoughtcrime/securesms/GroupCreateActivity.java b/src/org/thoughtcrime/securesms/GroupCreateActivity.java index 31aa8dbcf3..5520718a31 100644 --- a/src/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/src/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -46,16 +46,20 @@ import com.bumptech.glide.request.target.SimpleTarget; import com.google.protobuf.ByteString; import com.soundcloud.android.crop.Crop; +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.attachments.UriAttachment; import org.thoughtcrime.securesms.components.PushRecipientsPanel; import org.thoughtcrime.securesms.contacts.RecipientsEditor; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.NotInDirectoryException; +import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.TextSecureDirectory; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.RoundedCorners; +import org.thoughtcrime.securesms.providers.SingleUseBlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; @@ -80,6 +84,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; +import ws.com.google.android.mms.ContentType; import ws.com.google.android.mms.MmsException; @@ -469,8 +474,10 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity { .addAllMembers(e164numbers) .build(); - OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(this, groupRecipient, context, avatar); - long threadId = MessageSender.send(this, masterSecret, outgoingMessage, -1, false); + Uri avatarUri = SingleUseBlobProvider.getInstance().createUri(avatar); + Attachment avatarAttachment = new UriAttachment(avatarUri, ContentType.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length); + OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, context, avatarAttachment); + long threadId = MessageSender.send(this, masterSecret, outgoingMessage, -1, false); return new Pair<>(threadId, groupRecipient); } diff --git a/src/org/thoughtcrime/securesms/ImageMediaAdapter.java b/src/org/thoughtcrime/securesms/ImageMediaAdapter.java index 61d7565a42..11c96766ea 100644 --- a/src/org/thoughtcrime/securesms/ImageMediaAdapter.java +++ b/src/org/thoughtcrime/securesms/ImageMediaAdapter.java @@ -31,14 +31,12 @@ import org.thoughtcrime.securesms.ImageMediaAdapter.ViewHolder; import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; -import org.thoughtcrime.securesms.database.PartDatabase.ImageRecord; +import org.thoughtcrime.securesms.database.ImageDatabase.ImageRecord; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.util.MediaUtil; -import ws.com.google.android.mms.pdu.PduPart; - public class ImageMediaAdapter extends CursorRecyclerViewAdapter { private static final String TAG = ImageMediaAdapter.class.getSimpleName(); @@ -67,43 +65,38 @@ public class ImageMediaAdapter extends CursorRecyclerViewAdapter { @Override public void onBindViewHolder(final ViewHolder viewHolder, final @NonNull Cursor cursor) { final ThumbnailView imageView = viewHolder.imageView; - final ImageRecord imageRecord = ImageRecord.from(cursor); + final ImageRecord imageRecord = ImageRecord.from(cursor); - PduPart part = new PduPart(); + Slide slide = MediaUtil.getSlideForAttachment(getContext(), imageRecord.getAttachment()); - part.setDataUri(imageRecord.getUri()); - part.setContentType(imageRecord.getContentType().getBytes()); - part.setPartId(imageRecord.getPartId()); - - Slide slide = MediaUtil.getSlideForPart(getContext(), part, imageRecord.getContentType()); if (slide != null) { - imageView.setImageResource(slide, masterSecret); + imageView.setImageResource(masterSecret, slide, false, false); } imageView.setOnClickListener(new OnMediaClickListener(imageRecord)); } private class OnMediaClickListener implements OnClickListener { - private ImageRecord record; + private final ImageRecord imageRecord; - private OnMediaClickListener(ImageRecord record) { - this.record = record; + private OnMediaClickListener(ImageRecord imageRecord) { + this.imageRecord = imageRecord; } @Override public void onClick(View v) { Intent intent = new Intent(getContext(), MediaPreviewActivity.class); - intent.putExtra(MediaPreviewActivity.DATE_EXTRA, record.getDate()); + intent.putExtra(MediaPreviewActivity.DATE_EXTRA, imageRecord.getDate()); - if (!TextUtils.isEmpty(record.getAddress())) { + if (!TextUtils.isEmpty(imageRecord.getAddress())) { Recipients recipients = RecipientFactory.getRecipientsFromString(getContext(), - record.getAddress(), + imageRecord.getAddress(), true); if (recipients != null && recipients.getPrimaryRecipient() != null) { intent.putExtra(MediaPreviewActivity.RECIPIENT_EXTRA, recipients.getPrimaryRecipient().getRecipientId()); } } - intent.setDataAndType(record.getUri(), record.getContentType()); + intent.setDataAndType(imageRecord.getAttachment().getDataUri(), imageRecord.getContentType()); getContext().startActivity(intent); } diff --git a/src/org/thoughtcrime/securesms/MediaOverviewActivity.java b/src/org/thoughtcrime/securesms/MediaOverviewActivity.java index 57956c8c8a..bfbd2c2c77 100644 --- a/src/org/thoughtcrime/securesms/MediaOverviewActivity.java +++ b/src/org/thoughtcrime/securesms/MediaOverviewActivity.java @@ -175,7 +175,7 @@ public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity i @Override public Cursor getCursor() { - return DatabaseFactory.getPartDatabase(getContext()).getImagesForThread(threadId); + return DatabaseFactory.getImageDatabase(getContext()).getImagesForThread(threadId); } } } diff --git a/src/org/thoughtcrime/securesms/attachments/Attachment.java b/src/org/thoughtcrime/securesms/attachments/Attachment.java new file mode 100644 index 0000000000..ce579d81e8 --- /dev/null +++ b/src/org/thoughtcrime/securesms/attachments/Attachment.java @@ -0,0 +1,88 @@ +package org.thoughtcrime.securesms.attachments; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.thoughtcrime.securesms.database.AttachmentDatabase; + +public abstract class Attachment { + + @NonNull + private final String contentType; + private final int transferState; + private final long size; + + @Nullable + private final String location; + + @Nullable + private final String key; + + @Nullable + private final String relay; + + // XXX - This shouldn't be here. + @Nullable + private Bitmap thumbnail; + + public Attachment(@NonNull String contentType, int transferState, long size, + @Nullable String location, @Nullable String key, @Nullable String relay) + { + this.contentType = contentType; + this.transferState = transferState; + this.size = size; + this.location = location; + this.key = key; + this.relay = relay; + } + + @Nullable + public abstract Uri getDataUri(); + + @Nullable + public abstract Uri getThumbnailUri(); + + public int getTransferState() { + return transferState; + } + + public boolean isInProgress() { + return transferState != AttachmentDatabase.TRANSFER_PROGRESS_DONE && + transferState != AttachmentDatabase.TRANSFER_PROGRESS_FAILED; + } + + public long getSize() { + return size; + } + + @NonNull + public String getContentType() { + return contentType; + } + + @Nullable + public String getLocation() { + return location; + } + + @Nullable + public String getKey() { + return key; + } + + @Nullable + public String getRelay() { + return relay; + } + + public void setThumbnail(@Nullable Bitmap thumbnail) { + this.thumbnail = thumbnail; + } + + @Nullable + public Bitmap getThumbnail() { + return thumbnail; + } +} diff --git a/src/org/thoughtcrime/securesms/attachments/AttachmentId.java b/src/org/thoughtcrime/securesms/attachments/AttachmentId.java new file mode 100644 index 0000000000..cf6b8f9cc6 --- /dev/null +++ b/src/org/thoughtcrime/securesms/attachments/AttachmentId.java @@ -0,0 +1,50 @@ +package org.thoughtcrime.securesms.attachments; + +import org.thoughtcrime.securesms.util.Util; + +public class AttachmentId { + + private final long rowId; + private final long uniqueId; + + public AttachmentId(long rowId, long uniqueId) { + this.rowId = rowId; + this.uniqueId = uniqueId; + } + + public long getRowId() { + return rowId; + } + + public long getUniqueId() { + return uniqueId; + } + + public String[] toStrings() { + return new String[] {String.valueOf(rowId), String.valueOf(uniqueId)}; + } + + public String toString() { + return "(row id: " + rowId + ", unique ID: " + uniqueId + ")"; + } + + public boolean isValid() { + return rowId >= 0 && uniqueId >= 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AttachmentId attachmentId = (AttachmentId)o; + + if (rowId != attachmentId.rowId) return false; + return uniqueId == attachmentId.uniqueId; + } + + @Override + public int hashCode() { + return Util.hashCode(rowId, uniqueId); + } +} diff --git a/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java b/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java new file mode 100644 index 0000000000..a8f47be164 --- /dev/null +++ b/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java @@ -0,0 +1,59 @@ +package org.thoughtcrime.securesms.attachments; + +import android.net.Uri; +import android.support.annotation.NonNull; + +import org.thoughtcrime.securesms.mms.PartAuthority; + +public class DatabaseAttachment extends Attachment { + + private final AttachmentId attachmentId; + private final long mmsId; + private final boolean hasData; + + public DatabaseAttachment(AttachmentId attachmentId, long mmsId, boolean hasData, + String contentType, int transferProgress, long size, + String location, String key, String relay) + { + super(contentType, transferProgress, size, location, key, relay); + this.attachmentId = attachmentId; + this.hasData = hasData; + this.mmsId = mmsId; + } + + @Override + @NonNull + public Uri getDataUri() { + return PartAuthority.getAttachmentDataUri(attachmentId); + } + + @Override + @NonNull + public Uri getThumbnailUri() { + return PartAuthority.getAttachmentThumbnailUri(attachmentId); + } + + public AttachmentId getAttachmentId() { + return attachmentId; + } + + @Override + public boolean equals(Object other) { + return other != null && + other instanceof DatabaseAttachment && + ((DatabaseAttachment) other).attachmentId.equals(this.attachmentId); + } + + @Override + public int hashCode() { + return attachmentId.hashCode(); + } + + public long getMmsId() { + return mmsId; + } + + public boolean hasData() { + return hasData; + } +} diff --git a/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java b/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java new file mode 100644 index 0000000000..7f666db109 --- /dev/null +++ b/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java @@ -0,0 +1,55 @@ +package org.thoughtcrime.securesms.attachments; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.thoughtcrime.securesms.crypto.MasterSecretUnion; +import org.thoughtcrime.securesms.crypto.MediaKey; +import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.whispersystems.libaxolotl.util.guava.Optional; +import org.whispersystems.textsecure.api.messages.TextSecureAttachment; + +import java.util.LinkedList; +import java.util.List; + +public class PointerAttachment extends Attachment { + + public PointerAttachment(@NonNull String contentType, int transferState, long size, + @NonNull String location, @NonNull String key, @NonNull String relay) + { + super(contentType, transferState, size, location, key, relay); + } + + @Nullable + @Override + public Uri getDataUri() { + return null; + } + + @Nullable + @Override + public Uri getThumbnailUri() { + return null; + } + + + public static List forPointers(@NonNull MasterSecretUnion masterSecret, Optional> pointers) { + List results = new LinkedList<>(); + + if (pointers.isPresent()) { + for (TextSecureAttachment pointer : pointers.get()) { + if (pointer.isPointer()) { + String encryptedKey = MediaKey.getEncrypted(masterSecret, pointer.asPointer().getKey()); + results.add(new PointerAttachment(pointer.getContentType(), + AttachmentDatabase.TRANSFER_PROGRESS_AUTO_PENDING, + pointer.asPointer().getSize().or(0), + String.valueOf(pointer.asPointer().getId()), + encryptedKey, pointer.asPointer().getRelay().orNull())); + } + } + } + + return results; + } +} diff --git a/src/org/thoughtcrime/securesms/attachments/UriAttachment.java b/src/org/thoughtcrime/securesms/attachments/UriAttachment.java new file mode 100644 index 0000000000..8c2749878b --- /dev/null +++ b/src/org/thoughtcrime/securesms/attachments/UriAttachment.java @@ -0,0 +1,42 @@ +package org.thoughtcrime.securesms.attachments; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; + +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.util.MediaUtil; +import org.whispersystems.libaxolotl.util.guava.Optional; + +import java.io.IOException; +import java.io.InputStream; + +public class UriAttachment extends Attachment { + + private final Uri dataUri; + private final Uri thumbnailUri; + + public UriAttachment(Uri uri, String contentType, int transferState, long size) { + this(uri, uri, contentType, transferState, size); + } + + public UriAttachment(Uri dataUri, Uri thumbnailUri, + String contentType, int transferState, long size) + { + super(contentType, transferState, size, null, null, null); + this.dataUri = dataUri; + this.thumbnailUri = thumbnailUri; + } + + @Override + @NonNull + public Uri getDataUri() { + return dataUri; + } + + @Override + @NonNull + public Uri getThumbnailUri() { + return thumbnailUri; + } +} diff --git a/src/org/thoughtcrime/securesms/components/ThumbnailView.java b/src/org/thoughtcrime/securesms/components/ThumbnailView.java index 418ab42789..3f25d2bbe8 100644 --- a/src/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/src/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -8,14 +8,13 @@ import android.graphics.Color; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; -import com.bumptech.glide.DrawableTypeRequest; +import com.bumptech.glide.DrawableRequestBuilder; import com.bumptech.glide.GenericRequestBuilder; import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.bitmap.GlideBitmapDrawable; @@ -24,8 +23,9 @@ import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.database.PartDatabase; +import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.RoundedCorners; import org.thoughtcrime.securesms.mms.Slide; @@ -36,12 +36,9 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.whispersystems.libaxolotl.util.guava.Optional; -import ws.com.google.android.mms.pdu.PduPart; - public class ThumbnailView extends FrameLayout { private static final String TAG = ThumbnailView.class.getSimpleName(); - private boolean hideControls; private ImageView image; private ImageView removeButton; private int backgroundColorHint; @@ -53,7 +50,6 @@ public class ThumbnailView extends FrameLayout { private SlideDeckListener slideDeckListener = null; private ThumbnailClickListener thumbnailClickListener = null; private ThumbnailClickListener downloadClickListener = null; - private String slideId = null; private Slide slide = null; public ThumbnailView(Context context) { @@ -78,21 +74,25 @@ public class ThumbnailView extends FrameLayout { } } - @Override public void setOnClickListener(OnClickListener l) { + @Override + public void setOnClickListener(OnClickListener l) { parentClickListener = l; } - @Override public void setFocusable(boolean focusable) { + @Override + public void setFocusable(boolean focusable) { super.setFocusable(focusable); if (transferControls.isPresent()) transferControls.get().setFocusable(focusable); } - @Override public void setClickable(boolean clickable) { + @Override + public void setClickable(boolean clickable) { super.setClickable(clickable); if (transferControls.isPresent()) transferControls.get().setClickable(clickable); } - @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (removeButton != null) { final int paddingHorizontal = removeButton.getWidth() / 2; @@ -117,32 +117,30 @@ public class ThumbnailView extends FrameLayout { this.backgroundColorHint = color; } - public void setImageResource(@Nullable MasterSecret masterSecret, - long id, - long timestamp, - @NonNull ListenableFutureTask slideDeckFuture) + public void setImageResource(@NonNull MasterSecret masterSecret, + @NonNull ListenableFutureTask slideDeckFuture, + boolean showControls, boolean showRemove) { if (this.slideDeckFuture != null && this.slideDeckListener != null) { this.slideDeckFuture.removeListener(this.slideDeckListener); } - String slideId = id + "::" + timestamp; - - if (!slideId.equals(this.slideId)) { + if (!slideDeckFuture.equals(this.slideDeckFuture)) { if (transferControls.isPresent()) getTransferControls().clear(); image.setImageDrawable(null); - this.slide = null; - this.slideId = slideId; + this.slide = null; } - this.slideDeckListener = new SlideDeckListener(masterSecret); + this.slideDeckListener = new SlideDeckListener(masterSecret, showControls, showRemove); this.slideDeckFuture = slideDeckFuture; this.slideDeckFuture.addListener(this.slideDeckListener); } - public void setImageResource(@NonNull Slide slide, @Nullable MasterSecret masterSecret) { + public void setImageResource(@NonNull MasterSecret masterSecret, @NonNull Slide slide, + boolean showControls, boolean showRemove) + { if (Util.equals(slide, this.slide)) { - Log.w(TAG, "Not re-loading slide " + slide.getPart().getPartId()); + Log.w(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri()); return; } @@ -151,16 +149,21 @@ public class ThumbnailView extends FrameLayout { return; } - Log.w(TAG, "loading part with id " + slide.getPart().getPartId() - + ", progress " + slide.getTransferProgress()); - - this.slide = slide; - loadInto(slide, masterSecret, image); - - if (!hideControls) { + if (showControls) { getTransferControls().setSlide(slide); getTransferControls().setDownloadClickListener(new DownloadClickDispatcher()); + } else if (transferControls.isPresent()) { + getTransferControls().setVisibility(View.GONE); } + + Log.w(TAG, "loading part with id " + slide.asAttachment().getDataUri() + + ", progress " + slide.getTransferState()); + + this.slide = slide; + + if (slide.getThumbnailUri() != null) buildThumbnailGlideRequest(slide, masterSecret, showRemove).into(image); + else if (slide.hasPlaceholder()) buildPlaceholderGlideRequest(slide).into(image); + else Glide.clear(image); } public void setThumbnailClickListener(ThumbnailClickListener listener) { @@ -182,16 +185,10 @@ public class ThumbnailView extends FrameLayout { if (slideDeckFuture != null) slideDeckFuture.removeListener(slideDeckListener); if (transferControls.isPresent()) getTransferControls().clear(); slide = null; - slideId = null; slideDeckFuture = null; slideDeckListener = null; } - public void hideControls(boolean hideControls) { - this.hideControls = hideControls; - if (hideControls && transferControls.isPresent()) getTransferControls().setVisibility(View.GONE); - } - public void showProgressSpinner() { getTransferControls().showProgressSpinner(); } @@ -203,48 +200,19 @@ public class ThumbnailView extends FrameLayout { !((Activity)getContext()).isDestroyed(); } - private void loadInto(@NonNull Slide slide, - @Nullable MasterSecret masterSecret, - @NonNull ImageView view) - { - if (slide.getThumbnailUri() != null) { - buildThumbnailGlideRequest(slide, masterSecret).into(view); - } else if (!slide.isInProgress()) { - buildPlaceholderGlideRequest(slide).into(view); - } else { - Glide.clear(view); + private GenericRequestBuilder buildThumbnailGlideRequest(@NonNull Slide slide, @NonNull MasterSecret masterSecret, boolean showRemove) { + DrawableRequestBuilder builder = Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri())) + .crossFade() + .transform(new RoundedCorners(getContext(), true, radius, backgroundColorHint)); + + if (showRemove) { + builder = builder.listener(new ThumbnailSetListener(slide.asAttachment())); } - } - - private GenericRequestBuilder buildThumbnailGlideRequest(Slide slide, MasterSecret masterSecret) { - final GenericRequestBuilder builder; - - if (slide.isDraft()) builder = buildDraftGlideRequest(slide, masterSecret); - else builder = buildPartGlideRequest(slide, masterSecret); if (slide.isInProgress()) return builder; else return builder.error(R.drawable.ic_missing_thumbnail_picture); } - private GenericRequestBuilder buildDraftGlideRequest(Slide slide, MasterSecret masterSecret) { - final DrawableTypeRequest request; - if (masterSecret == null) request = Glide.with(getContext()).load(slide.getThumbnailUri()); - else request = Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri())); - - return request.transform(new RoundedCorners(getContext(), false, radius, backgroundColorHint)) - .listener(new PduThumbnailSetListener(slide.getPart())); - } - - private GenericRequestBuilder buildPartGlideRequest(Slide slide, MasterSecret masterSecret) { - if (masterSecret == null) { - throw new IllegalStateException("null MasterSecret when loading non-draft thumbnail"); - } - - return Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri())) - .crossFade() - .transform(new RoundedCorners(getContext(), true, radius, backgroundColorHint)); - } - private GenericRequestBuilder buildPlaceholderGlideRequest(Slide slide) { return Glide.with(getContext()).load(slide.getPlaceholderRes(getContext().getTheme())) .asBitmap() @@ -253,9 +221,13 @@ public class ThumbnailView extends FrameLayout { private class SlideDeckListener implements FutureTaskListener { private final MasterSecret masterSecret; + private final boolean showControls; + private final boolean showRemove; - public SlideDeckListener(MasterSecret masterSecret) { + public SlideDeckListener(@NonNull MasterSecret masterSecret, boolean showControls, boolean showRemove) { this.masterSecret = masterSecret; + this.showControls = showControls; + this.showRemove = showRemove; } @Override @@ -268,7 +240,7 @@ public class ThumbnailView extends FrameLayout { Util.runOnMain(new Runnable() { @Override public void run() { - setImageResource(slide, masterSecret); + setImageResource(masterSecret, slide, showControls, showRemove); } }); } else { @@ -302,10 +274,10 @@ public class ThumbnailView extends FrameLayout { private class ThumbnailClickDispatcher implements View.OnClickListener { @Override public void onClick(View view) { - if (thumbnailClickListener != null && - slide != null && - slide.getPart().getDataUri() != null && - slide.getTransferProgress() == PartDatabase.TRANSFER_PROGRESS_DONE) + if (thumbnailClickListener != null && + slide != null && + slide.asAttachment().getDataUri() != null && + slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) { thumbnailClickListener.onClick(view, slide); } else if (parentClickListener != null) { @@ -323,11 +295,12 @@ public class ThumbnailView extends FrameLayout { } } - private class PduThumbnailSetListener implements RequestListener { - private PduPart part; + private class ThumbnailSetListener implements RequestListener { - public PduThumbnailSetListener(@NonNull PduPart part) { - this.part = part; + private final Attachment attachment; + + public ThumbnailSetListener(@NonNull Attachment attachment) { + this.attachment = attachment; } @Override @@ -339,7 +312,7 @@ public class ThumbnailView extends FrameLayout { public boolean onResourceReady(GlideDrawable resource, Object model, Target target, boolean isFromMemoryCache, boolean isFirstResource) { if (resource instanceof GlideBitmapDrawable) { Log.w(TAG, "onResourceReady() for a Bitmap. Saving."); - part.setThumbnail(((GlideBitmapDrawable)resource).getBitmap()); + attachment.setThumbnail(((GlideBitmapDrawable) resource).getBitmap()); } LayoutParams layoutParams = (LayoutParams) getRemoveButton().getLayoutParams(); if (resource.getIntrinsicWidth() < getWidth()) { diff --git a/src/org/thoughtcrime/securesms/components/TransferControlView.java b/src/org/thoughtcrime/securesms/components/TransferControlView.java index 48fe39c6f5..67479cad20 100644 --- a/src/org/thoughtcrime/securesms/components/TransferControlView.java +++ b/src/org/thoughtcrime/securesms/components/TransferControlView.java @@ -21,7 +21,7 @@ import com.nineoldandroids.animation.ValueAnimator.AnimatorUpdateListener; import com.pnikosis.materialishprogress.ProgressWheel; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.database.PartDatabase; +import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.jobs.PartProgressEvent; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.util.Util; @@ -92,7 +92,7 @@ public class TransferControlView extends FrameLayout { public void setSlide(final @NonNull Slide slide) { this.slide = slide; - if (slide.getTransferProgress() == PartDatabase.TRANSFER_PROGRESS_STARTED) { + if (slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) { showProgressSpinner(); } else if (slide.isPendingDownload()) { downloadDetails.setText(slide.getContentDescription()); @@ -164,7 +164,7 @@ public class TransferControlView extends FrameLayout { @SuppressWarnings("unused") public void onEventAsync(final PartProgressEvent event) { - if (this.slide != null && event.partId.equals(this.slide.getPart().getPartId())) { + if (this.slide != null && event.attachment.equals(this.slide.asAttachment())) { Util.runOnMain(new Runnable() { @Override public void run() { diff --git a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java new file mode 100644 index 0000000000..17022793e3 --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -0,0 +1,530 @@ +/** + * Copyright (C) 2011 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (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 . + */ +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.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.attachments.AttachmentId; +import org.thoughtcrime.securesms.attachments.DatabaseAttachment; +import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; +import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.crypto.MasterSecretUnion; +import org.thoughtcrime.securesms.mms.PartAuthority; +import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData; +import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.VisibleForTesting; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; + +import ws.com.google.android.mms.MmsException; + +public class AttachmentDatabase extends Database { + + private static final String TAG = AttachmentDatabase.class.getSimpleName(); + + static final String TABLE_NAME = "part"; + static final String ROW_ID = "_id"; + static final String MMS_ID = "mid"; + static final String CONTENT_TYPE = "ct"; + private static final String NAME = "name"; + private static final String CONTENT_DISPOSITION = "cd"; + private static final String CONTENT_LOCATION = "cl"; + static final String DATA = "_data"; + static final String TRANSFER_STATE = "pending_push"; + static final String SIZE = "data_size"; + private static final String THUMBNAIL = "thumbnail"; + static final String THUMBNAIL_ASPECT_RATIO = "aspect_ratio"; + static final String UNIQUE_ID = "unique_id"; + + public static final int TRANSFER_PROGRESS_DONE = 0; + public static final int TRANSFER_PROGRESS_STARTED = 1; + public static final int TRANSFER_PROGRESS_AUTO_PENDING = 2; + public static final int TRANSFER_PROGRESS_FAILED = 3; + + private static final String PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?"; + + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " + + MMS_ID + " INTEGER, " + "seq" + " INTEGER DEFAULT 0, " + + CONTENT_TYPE + " TEXT, " + NAME + " TEXT, " + "chset" + " INTEGER, " + + CONTENT_DISPOSITION + " TEXT, " + "fn" + " TEXT, " + "cid" + " TEXT, " + + CONTENT_LOCATION + " TEXT, " + "ctt_s" + " INTEGER, " + + "ctt_t" + " TEXT, " + "encrypted" + " INTEGER, " + + TRANSFER_STATE + " INTEGER, "+ DATA + " TEXT, " + SIZE + " INTEGER, " + + THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " + UNIQUE_ID + " INTEGER NOT NULL);"; + + public static final String[] CREATE_INDEXS = { + "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", + "CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + TRANSFER_STATE + ");", + }; + + private final ExecutorService thumbnailExecutor = Util.newSingleThreadedLifoExecutor(); + + public AttachmentDatabase(Context context, SQLiteOpenHelper databaseHelper) { + super(context, databaseHelper); + } + + public @NonNull InputStream getAttachmentStream(MasterSecret masterSecret, AttachmentId attachmentId) + throws IOException + { + InputStream dataStream = getDataStream(masterSecret, attachmentId, DATA); + + if (dataStream == null) throw new IOException("No stream for: " + attachmentId); + else return dataStream; + } + + public @NonNull InputStream getThumbnailStream(@NonNull MasterSecret masterSecret, @NonNull AttachmentId attachmentId) + throws IOException + { + Log.w(TAG, "getThumbnailStream(" + attachmentId + ")"); + InputStream dataStream = getDataStream(masterSecret, attachmentId, THUMBNAIL); + + if (dataStream != null) { + return dataStream; + } + + try { + InputStream generatedStream = thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret, attachmentId)).get(); + + if (generatedStream == null) throw new IOException("No thumbnail stream available: " + attachmentId); + else return generatedStream; + } catch (InterruptedException ie) { + throw new AssertionError("interrupted"); + } catch (ExecutionException ee) { + Log.w(TAG, ee); + throw new IOException(ee); + } + } + + public void setTransferProgressFailed(AttachmentId attachmentId, long mmsId) + throws MmsException + { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(TRANSFER_STATE, TRANSFER_PROGRESS_FAILED); + + database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); + notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(mmsId)); + } + + public @Nullable Attachment getAttachment(AttachmentId attachmentId) { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + Cursor cursor = null; + + try { + cursor = database.query(TABLE_NAME, null, PART_ID_WHERE, attachmentId.toStrings(), null, null, null); + + if (cursor != null && cursor.moveToFirst()) return getAttachment(cursor); + else return null; + + } finally { + if (cursor != null) + cursor.close(); + } + } + + public @NonNull List getAttachmentsForMessage(long mmsId) { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + List results = new LinkedList<>(); + Cursor cursor = null; + + try { + cursor = database.query(TABLE_NAME, null, MMS_ID + " = ?", new String[] {mmsId+""}, + null, null, null); + + while (cursor != null && cursor.moveToNext()) { + results.add(getAttachment(cursor)); + } + + return results; + } finally { + if (cursor != null) + cursor.close(); + } + } + + public @NonNull List getPendingAttachments() { + final SQLiteDatabase database = databaseHelper.getReadableDatabase(); + final List attachments = new LinkedList<>(); + + Cursor cursor = null; + try { + cursor = database.query(TABLE_NAME, null, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null); + while (cursor != null && cursor.moveToNext()) { + attachments.add(getAttachment(cursor)); + } + } finally { + if (cursor != null) cursor.close(); + } + + return attachments; + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public void deleteAttachmentsForMessage(long mmsId) { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + Cursor cursor = null; + + try { + cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL}, MMS_ID + " = ?", + new String[] {mmsId+""}, null, null, null); + + while (cursor != null && cursor.moveToNext()) { + String data = cursor.getString(0); + String thumbnail = cursor.getString(1); + + if (!TextUtils.isEmpty(data)) { + new File(data).delete(); + } + + if (!TextUtils.isEmpty(thumbnail)) { + new File(thumbnail).delete(); + } + } + } finally { + if (cursor != null) + cursor.close(); + } + + database.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {mmsId + ""}); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public void deleteAllAttachments() { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + database.delete(TABLE_NAME, null, null); + + File attachmentsDirectory = context.getDir("parts", Context.MODE_PRIVATE); + File[] attachments = attachmentsDirectory.listFiles(); + + for (File attachment : attachments) { + attachment.delete(); + } + } + + public long insertAttachmentsForPlaceholder(@NonNull MasterSecret masterSecret, long mmsId, + @NonNull AttachmentId attachmentId, + @NonNull InputStream inputStream) + throws MmsException + { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + Pair partData = setAttachmentData(masterSecret, inputStream); + ContentValues values = new ContentValues(); + + values.put(DATA, partData.first.getAbsolutePath()); + values.put(SIZE, partData.second); + values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); + values.put(CONTENT_LOCATION, (String)null); + values.put(CONTENT_DISPOSITION, (String)null); + values.put(NAME, (String) null); + + if (database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()) == 0) { + //noinspection ResultOfMethodCallIgnored + partData.first.delete(); + } else { + notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(mmsId)); + } + + thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret, attachmentId)); + return partData.second; + } + + + void insertAttachmentsForMessage(@NonNull MasterSecretUnion masterSecret, + long mmsId, + @NonNull List attachments) + throws MmsException + { + Log.w(TAG, "insertParts(" + attachments.size() + ")"); + + for (Attachment attachment : attachments) { + AttachmentId attachmentId = insertAttachment(masterSecret, mmsId, attachment); + Log.w(TAG, "Inserted attachment at ID: " + attachmentId); + } + } + + public @NonNull Attachment updateAttachmentData(@NonNull MasterSecret masterSecret, + @NonNull Attachment attachment, + @NonNull InputStream inputStream) + throws MmsException + { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + DatabaseAttachment databaseAttachment = (DatabaseAttachment) attachment; + File dataFile = getAttachmentDataFile(databaseAttachment.getAttachmentId(), DATA); + + if (dataFile == null) { + throw new MmsException("No attachment data found!"); + } + + long dataSize = setAttachmentData(masterSecret, dataFile, inputStream); + + ContentValues contentValues = new ContentValues(); + contentValues.put(SIZE, dataSize); + + database.update(TABLE_NAME, contentValues, PART_ID_WHERE, databaseAttachment.getAttachmentId().toStrings()); + + return new DatabaseAttachment(databaseAttachment.getAttachmentId(), + databaseAttachment.getMmsId(), + databaseAttachment.hasData(), + databaseAttachment.getContentType(), + databaseAttachment.getTransferState(), + dataSize, databaseAttachment.getLocation(), + databaseAttachment.getKey(), + databaseAttachment.getRelay()); + } + + + public void markAttachmentUploaded(long messageId, Attachment attachment) { + ContentValues values = new ContentValues(1); + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + + values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); + database.update(TABLE_NAME, values, PART_ID_WHERE, ((DatabaseAttachment)attachment).getAttachmentId().toStrings()); + + notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId)); + } + + public void setTransferState(long messageId, @NonNull Attachment attachment, int transferState) { + if (!(attachment instanceof DatabaseAttachment)) { + throw new AssertionError("Attempt to update attachment that doesn't belong to DB!"); + } + + setTransferState(messageId, ((DatabaseAttachment) attachment).getAttachmentId(), transferState); + } + + public void setTransferState(long messageId, @NonNull AttachmentId attachmentId, int transferState) { + final ContentValues values = new ContentValues(1); + final SQLiteDatabase database = databaseHelper.getWritableDatabase(); + + values.put(TRANSFER_STATE, transferState); + database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); + notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId)); + ApplicationContext.getInstance(context).notifyMediaControlEvent(); + } + + @VisibleForTesting + @Nullable InputStream getDataStream(MasterSecret masterSecret, AttachmentId attachmentId, String dataType) + { + File dataFile = getAttachmentDataFile(attachmentId, dataType); + + try { + if (dataFile != null) return new DecryptingPartInputStream(dataFile, masterSecret); + else return null; + } catch (FileNotFoundException e) { + Log.w(TAG, e); + return null; + } + } + + private @Nullable File getAttachmentDataFile(@NonNull AttachmentId attachmentId, + @NonNull String dataType) + { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + Cursor cursor = null; + + try { + cursor = database.query(TABLE_NAME, new String[]{dataType}, PART_ID_WHERE, attachmentId.toStrings(), + null, null, null); + + if (cursor != null && cursor.moveToFirst()) { + if (cursor.isNull(0)) { + return null; + } + + return new File(cursor.getString(0)); + } else { + return null; + } + } finally { + if (cursor != null) + cursor.close(); + } + + } + + private @NonNull Pair setAttachmentData(@NonNull MasterSecret masterSecret, + @NonNull Uri uri) + throws MmsException + { + try { + InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, uri); + return setAttachmentData(masterSecret, inputStream); + } catch (IOException e) { + throw new MmsException(e); + } + } + + private @NonNull Pair setAttachmentData(@NonNull MasterSecret masterSecret, + @NonNull InputStream in) + throws MmsException + { + try { + File partsDirectory = context.getDir("parts", Context.MODE_PRIVATE); + File dataFile = File.createTempFile("part", ".mms", partsDirectory); + + return new Pair<>(dataFile, setAttachmentData(masterSecret, dataFile, in)); + } catch (IOException e) { + throw new MmsException(e); + } + } + + private long setAttachmentData(@NonNull MasterSecret masterSecret, + @NonNull File destination, + @NonNull InputStream in) + throws MmsException + { + try { + OutputStream out = new EncryptingPartOutputStream(destination, masterSecret); + return Util.copy(in, out); + } catch (IOException e) { + throw new MmsException(e); + } + } + + private DatabaseAttachment getAttachment(Cursor cursor) { + return new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)), + cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))), + cursor.getLong(cursor.getColumnIndexOrThrow(MMS_ID)), + !cursor.isNull(cursor.getColumnIndexOrThrow(DATA)), + cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)), + cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)), + cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), + cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)), + cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)), + cursor.getString(cursor.getColumnIndexOrThrow(NAME))); + } + + + private AttachmentId insertAttachment(MasterSecretUnion masterSecret, long mmsId, Attachment attachment) + throws MmsException + { + Log.w(TAG, "Inserting attachment for mms id: " + mmsId); + + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + Pair partData = null; + long uniqueId = System.currentTimeMillis(); + + if (masterSecret.getMasterSecret().isPresent() && attachment.getDataUri() != null) { + partData = setAttachmentData(masterSecret.getMasterSecret().get(), attachment.getDataUri()); + Log.w(TAG, "Wrote part to file: " + partData.first.getAbsolutePath()); + } + + ContentValues contentValues = new ContentValues(); + contentValues.put(MMS_ID, mmsId); + contentValues.put(CONTENT_TYPE, attachment.getContentType()); + contentValues.put(TRANSFER_STATE, attachment.getTransferState()); + contentValues.put(UNIQUE_ID, uniqueId); + contentValues.put(CONTENT_LOCATION, attachment.getLocation()); + contentValues.put(CONTENT_DISPOSITION, attachment.getKey()); + contentValues.put(NAME, attachment.getRelay()); + + if (partData != null) { + contentValues.put(DATA, partData.first.getAbsolutePath()); + contentValues.put(SIZE, partData.second); + } + + long rowId = database.insert(TABLE_NAME, null, contentValues); + AttachmentId attachmentId = new AttachmentId(rowId, uniqueId); + + if (attachment.getThumbnail() != null && masterSecret.getMasterSecret().isPresent()) { + Log.w(TAG, "inserting pre-generated thumbnail"); + ThumbnailData data = new ThumbnailData(attachment.getThumbnail()); + updateAttachmentThumbnail(masterSecret.getMasterSecret().get(), attachmentId, data.toDataStream(), data.getAspectRatio()); + } else if (!attachment.isInProgress()) { + thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret.getMasterSecret().get(), attachmentId)); + } + + return attachmentId; + } + + + @VisibleForTesting + void updateAttachmentThumbnail(MasterSecret masterSecret, AttachmentId attachmentId, InputStream in, float aspectRatio) + throws MmsException + { + Log.w(TAG, "updating part thumbnail for #" + attachmentId); + + Pair thumbnailFile = setAttachmentData(masterSecret, in); + + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + ContentValues values = new ContentValues(2); + + values.put(THUMBNAIL, thumbnailFile.first.getAbsolutePath()); + values.put(THUMBNAIL_ASPECT_RATIO, aspectRatio); + + database.update(TABLE_NAME, values, PART_ID_WHERE, attachmentId.toStrings()); + } + + + @VisibleForTesting + class ThumbnailFetchCallable implements Callable { + private final MasterSecret masterSecret; + private final AttachmentId attachmentId; + + public ThumbnailFetchCallable(MasterSecret masterSecret, AttachmentId attachmentId) { + this.masterSecret = masterSecret; + this.attachmentId = attachmentId; + } + + @Override + public @Nullable InputStream call() throws Exception { + final InputStream stream = getDataStream(masterSecret, attachmentId, THUMBNAIL); + + if (stream != null) { + return stream; + } + + Attachment attachment = getAttachment(attachmentId); + + if (attachment == null || attachment.isInProgress()) { + return null; + } + + ThumbnailData data = MediaUtil.generateThumbnail(context, masterSecret, attachment.getContentType(), attachment.getDataUri()); + + if (data == null) { + return null; + } + + updateAttachmentThumbnail(masterSecret, attachmentId, data.toDataStream(), data.getAspectRatio()); + + return getDataStream(masterSecret, attachmentId, THUMBNAIL); + } + } +} diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index eec89aeb7c..d160c7243b 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -78,7 +78,8 @@ public class DatabaseFactory { private final SmsDatabase sms; private final EncryptingSmsDatabase encryptingSms; private final MmsDatabase mms; - private final PartDatabase part; + private final AttachmentDatabase attachments; + private final ImageDatabase image; private final ThreadDatabase thread; private final CanonicalAddressDatabase address; private final MmsAddressDatabase mmsAddress; @@ -123,8 +124,12 @@ public class DatabaseFactory { return getInstance(context).encryptingSms; } - public static PartDatabase getPartDatabase(Context context) { - return getInstance(context).part; + public static AttachmentDatabase getAttachmentDatabase(Context context) { + return getInstance(context).attachments; + } + + public static ImageDatabase getImageDatabase(Context context) { + return getInstance(context).image; } public static MmsAddressDatabase getMmsAddressDatabase(Context context) { @@ -160,7 +165,8 @@ public class DatabaseFactory { this.sms = new SmsDatabase(context, databaseHelper); this.encryptingSms = new EncryptingSmsDatabase(context, databaseHelper); this.mms = new MmsDatabase(context, databaseHelper); - this.part = new PartDatabase(context, databaseHelper); + this.attachments = new AttachmentDatabase(context, databaseHelper); + this.image = new ImageDatabase(context, databaseHelper); this.thread = new ThreadDatabase(context, databaseHelper); this.address = CanonicalAddressDatabase.getInstance(context); this.mmsAddress = new MmsAddressDatabase(context, databaseHelper); @@ -180,7 +186,7 @@ public class DatabaseFactory { this.sms.reset(databaseHelper); this.encryptingSms.reset(databaseHelper); this.mms.reset(databaseHelper); - this.part.reset(databaseHelper); + this.attachments.reset(databaseHelper); this.thread.reset(databaseHelper); this.mmsAddress.reset(databaseHelper); this.mmsSmsDatabase.reset(databaseHelper); @@ -377,6 +383,7 @@ public class DatabaseFactory { body = (body == null) ? Util.readFullyAsString(is) : body + " " + Util.readFullyAsString(is); + //noinspection ResultOfMethodCallIgnored dataFile.delete(); db.delete("part", "_id = ?", new String[] {partId+""}); } catch (IOException e) { @@ -491,7 +498,7 @@ public class DatabaseFactory { public void onCreate(SQLiteDatabase db) { db.execSQL(SmsDatabase.CREATE_TABLE); db.execSQL(MmsDatabase.CREATE_TABLE); - db.execSQL(PartDatabase.CREATE_TABLE); + db.execSQL(AttachmentDatabase.CREATE_TABLE); db.execSQL(ThreadDatabase.CREATE_TABLE); db.execSQL(MmsAddressDatabase.CREATE_TABLE); db.execSQL(IdentityDatabase.CREATE_TABLE); @@ -502,7 +509,7 @@ public class DatabaseFactory { executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); - executeStatements(db, PartDatabase.CREATE_INDEXS); + executeStatements(db, AttachmentDatabase.CREATE_INDEXS); executeStatements(db, ThreadDatabase.CREATE_INDEXS); executeStatements(db, MmsAddressDatabase.CREATE_INDEXS); executeStatements(db, DraftDatabase.CREATE_INDEXS); diff --git a/src/org/thoughtcrime/securesms/database/ImageDatabase.java b/src/org/thoughtcrime/securesms/database/ImageDatabase.java new file mode 100644 index 0000000000..4f1329f57c --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/ImageDatabase.java @@ -0,0 +1,100 @@ +package org.thoughtcrime.securesms.database; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.attachments.AttachmentId; +import org.thoughtcrime.securesms.attachments.DatabaseAttachment; + +public class ImageDatabase extends Database { + + private final static String IMAGES_QUERY = "SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL_ASPECT_RATIO + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.NORMALIZED_DATE_RECEIVED + ", " + + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ADDRESS + " " + + "FROM " + AttachmentDatabase.TABLE_NAME + " LEFT JOIN " + MmsDatabase.TABLE_NAME + + " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " + + "WHERE " + AttachmentDatabase.MMS_ID + " IN (SELECT " + MmsSmsColumns.ID + + " FROM " + MmsDatabase.TABLE_NAME + + " WHERE " + MmsDatabase.THREAD_ID + " = ?) AND " + + AttachmentDatabase.CONTENT_TYPE + " LIKE 'image/%' " + + "ORDER BY " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " DESC"; + + public ImageDatabase(Context context, SQLiteOpenHelper databaseHelper) { + super(context, databaseHelper); + } + + public Cursor getImagesForThread(long threadId) { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + Cursor cursor = database.rawQuery(IMAGES_QUERY, new String[]{threadId+""}); + setNotifyConverationListeners(cursor, threadId); + return cursor; + } + + public static class ImageRecord { + private final AttachmentId attachmentId; + private final long mmsId; + private final boolean hasData; + private final String contentType; + private final String address; + private final long date; + private final int transferState; + private final long size; + + private ImageRecord(AttachmentId attachmentId, long mmsId, boolean hasData, + String contentType, String address, long date, + int transferState, long size) + { + this.attachmentId = attachmentId; + this.mmsId = mmsId; + this.hasData = hasData; + this.contentType = contentType; + this.address = address; + this.date = date; + this.transferState = transferState; + this.size = size; + } + + public static ImageRecord from(Cursor cursor) { + AttachmentId attachmentId = new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID)), + cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID))); + + return new ImageRecord(attachmentId, + cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID)), + !cursor.isNull(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA)), + cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.CONTENT_TYPE)), + cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)), + cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_RECEIVED)), + cursor.getInt(cursor.getColumnIndexOrThrow(AttachmentDatabase.TRANSFER_STATE)), + cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE))); + } + + public Attachment getAttachment() { + return new DatabaseAttachment(attachmentId, mmsId, hasData, contentType, transferState, size, null, null, null); + } + + public String getContentType() { + return contentType; + } + + public String getAddress() { + return address; + } + + public long getDate() { + return date; + } + + } + + +} diff --git a/src/org/thoughtcrime/securesms/database/MmsAddressDatabase.java b/src/org/thoughtcrime/securesms/database/MmsAddressDatabase.java index 754a398d29..05f1ef80a7 100644 --- a/src/org/thoughtcrime/securesms/database/MmsAddressDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsAddressDatabase.java @@ -21,21 +21,17 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import android.util.Log; +import android.support.annotation.NonNull; 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; - -import java.io.UnsupportedEncodingException; import java.util.LinkedList; import java.util.List; +import ws.com.google.android.mms.pdu.PduHeaders; + public class MmsAddressDatabase extends Database { private static final String TAG = MmsAddressDatabase.class.getSimpleName(); @@ -59,83 +55,77 @@ public class MmsAddressDatabase extends Database { super(context, databaseHelper); } - private void insertAddress(long messageId, int type, EncodedStringValue address) { - if (address != null) { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - ContentValues contentValues = new ContentValues(); - contentValues.put(MMS_ID, messageId); - contentValues.put(TYPE, type); - contentValues.put(ADDRESS, toIsoString(address.getTextString())); - contentValues.put(ADDRESS_CHARSET, address.getCharacterSet()); - database.insert(TABLE_NAME, null, contentValues); + private void insertAddress(long messageId, int type, @NonNull String value) { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + ContentValues contentValues = new ContentValues(); + contentValues.put(MMS_ID, messageId); + contentValues.put(TYPE, type); + contentValues.put(ADDRESS, value); + contentValues.put(ADDRESS_CHARSET, "UTF-8"); + database.insert(TABLE_NAME, null, contentValues); + } + + private void insertAddress(long messageId, int type, @NonNull List addresses) { + for (String address : addresses) { + insertAddress(messageId, type, address); } } - private void insertAddress(long messageId, int type, EncodedStringValue[] addresses) { - if (addresses != null) { - for (int i=0;i getAddressesForId(long messageId) { - List results = new LinkedList(); + public MmsAddresses getAddressesForId(long messageId) { SQLiteDatabase database = databaseHelper.getReadableDatabase(); Cursor cursor = null; + String from = null; + List to = new LinkedList<>(); + List cc = new LinkedList<>(); + List bcc = new LinkedList<>(); try { cursor = database.query(TABLE_NAME, null, MMS_ID + " = ?", new String[] {messageId+""}, null, null, null); while (cursor != null && cursor.moveToNext()) { - results.add(cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS))); + long type = cursor.getLong(cursor.getColumnIndexOrThrow(TYPE)); + String address = cursor.getString(cursor.getColumnIndexOrThrow(ADDRESS)); + + if (type == PduHeaders.FROM) from = address; + if (type == PduHeaders.TO) to.add(address); + if (type == PduHeaders.CC) cc.add(address); + if (type == PduHeaders.BCC) bcc.add(address); } } finally { if (cursor != null) cursor.close(); } + return new MmsAddresses(from, to, cc, bcc); + } + + public List getAddressesListForId(long messageId) { + List results = new LinkedList<>(); + MmsAddresses addresses = getAddressesForId(messageId); + + if (addresses.getFrom() != null) { + results.add(addresses.getFrom()); + } + + results.addAll(addresses.getTo()); + results.addAll(addresses.getCc()); + results.addAll(addresses.getBcc()); + return results; } public Recipients getRecipientsForId(long messageId) { - List numbers = getAddressesForId(messageId); + List numbers = getAddressesListForId(messageId); List results = new LinkedList<>(); for (String number : numbers) { @@ -158,22 +148,4 @@ public class MmsAddressDatabase extends Database { SQLiteDatabase database = databaseHelper.getWritableDatabase(); database.delete(TABLE_NAME, null, null); } - - private byte[] getBytes(String data) { - try { - return data.getBytes(CharacterSets.MIMENAME_ISO_8859_1); - } catch (UnsupportedEncodingException e) { - Log.e("PduHeadersBuilder", "ISO_8859_1 must be supported!", e); - return new byte[0]; - } - } - - private String toIsoString(byte[] bytes) { - try { - return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1); - } catch (UnsupportedEncodingException e) { - Log.e("MmsDatabase", "ISO_8859_1 must be supported!", e); - return ""; - } - } } diff --git a/src/org/thoughtcrime/securesms/database/MmsAddresses.java b/src/org/thoughtcrime/securesms/database/MmsAddresses.java new file mode 100644 index 0000000000..5913ae978a --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/MmsAddresses.java @@ -0,0 +1,56 @@ +package org.thoughtcrime.securesms.database; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.LinkedList; +import java.util.List; + +public class MmsAddresses { + + private final @Nullable String from; + private final @NonNull List to; + private final @NonNull List cc; + private final @NonNull List bcc; + + public MmsAddresses(@Nullable String from, @NonNull List to, + @NonNull List cc, @NonNull List bcc) + { + this.from = from; + this.to = to; + this.cc = cc; + this.bcc = bcc; + } + + @NonNull + public List getTo() { + return to; + } + + @NonNull + public List getCc() { + return cc; + } + + @NonNull + public List getBcc() { + return bcc; + } + + @Nullable + public String getFrom() { + return from; + } + + public static MmsAddresses forTo(@NonNull List to) { + return new MmsAddresses(null, to, new LinkedList(), new LinkedList()); + } + + public static MmsAddresses forBcc(@NonNull List bcc) { + return new MmsAddresses(null, new LinkedList(), new LinkedList(), bcc); + } + + public static MmsAddresses forFrom(@NonNull String from) { + return new MmsAddresses(from, new LinkedList(), new LinkedList(), new LinkedList()); + } +} diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index a85c2aa703..6df4a2a4c8 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -23,6 +23,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; @@ -32,14 +33,16 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher; import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUnion; -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.documents.NetworkFailure; +import org.thoughtcrime.securesms.database.documents.NetworkFailureList; import org.thoughtcrime.securesms.database.model.DisplayRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; @@ -48,9 +51,8 @@ import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; -import org.thoughtcrime.securesms.mms.PartParser; +import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.SlideDeck; -import org.thoughtcrime.securesms.mms.TextSlide; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; @@ -67,7 +69,6 @@ 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; @@ -78,25 +79,13 @@ import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; -import ws.com.google.android.mms.ContentType; -import ws.com.google.android.mms.InvalidHeaderValueException; import ws.com.google.android.mms.MmsException; -import ws.com.google.android.mms.pdu.CharacterSets; -import ws.com.google.android.mms.pdu.EncodedStringValue; import ws.com.google.android.mms.pdu.NotificationInd; -import ws.com.google.android.mms.pdu.PduBody; import ws.com.google.android.mms.pdu.PduHeaders; -import ws.com.google.android.mms.pdu.PduPart; -import ws.com.google.android.mms.pdu.SendReq; import static org.thoughtcrime.securesms.util.Util.canonicalizeNumber; import static org.thoughtcrime.securesms.util.Util.canonicalizeNumberOrGroup; -// XXXX Clean up MMS efficiency: -// 1) We need to be careful about how much memory we're using for parts. SoftRefereences. -// 2) How many queries do we make? calling getMediaMessageForId() from within an existing query -// seems wasteful. - public class MmsDatabase extends MessagingDatabase { private static final String TAG = MmsDatabase.class.getSimpleName(); @@ -105,47 +94,29 @@ public class MmsDatabase extends MessagingDatabase { static final String DATE_SENT = "date"; static final String DATE_RECEIVED = "date_received"; public static final String MESSAGE_BOX = "msg_box"; - private static final String MESSAGE_ID = "m_id"; - private static final String SUBJECT = "sub"; - private static final String SUBJECT_CHARSET = "sub_cs"; - static final String CONTENT_TYPE = "ct_t"; static final String CONTENT_LOCATION = "ct_l"; static final String EXPIRY = "exp"; - private static final String MESSAGE_CLASS = "m_cls"; public static final String MESSAGE_TYPE = "m_type"; - private static final String MMS_VERSION = "v"; static final String MESSAGE_SIZE = "m_size"; - private static final String PRIORITY = "pri"; - private static final String READ_REPORT = "rr"; - private static final String REPORT_ALLOWED = "rpt_a"; - private static final String RESPONSE_STATUS = "resp_st"; static final String STATUS = "st"; static final String TRANSACTION_ID = "tr_id"; - private static final String RETRIEVE_STATUS = "retr_st"; - private static final String RETRIEVE_TEXT = "retr_txt"; - private static final String RETRIEVE_TEXT_CS = "retr_txt_cs"; - private static final String READ_STATUS = "read_status"; - private static final String CONTENT_CLASS = "ct_cls"; - private static final String RESPONSE_TEXT = "resp_txt"; - private static final String DELIVERY_TIME = "d_tm"; - private static final String DELIVERY_REPORT = "d_rpt"; static final String PART_COUNT = "part_count"; 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, " + - READ + " INTEGER DEFAULT 0, " + MESSAGE_ID + " TEXT, " + SUBJECT + " TEXT, " + - SUBJECT_CHARSET + " INTEGER, " + BODY + " TEXT, " + PART_COUNT + " INTEGER, " + - CONTENT_TYPE + " TEXT, " + CONTENT_LOCATION + " TEXT, " + ADDRESS + " TEXT, " + + READ + " INTEGER DEFAULT 0, " + "m_id" + " TEXT, " + "sub" + " TEXT, " + + "sub_cs" + " INTEGER, " + BODY + " TEXT, " + PART_COUNT + " INTEGER, " + + "ct_t" + " TEXT, " + CONTENT_LOCATION + " TEXT, " + ADDRESS + " TEXT, " + ADDRESS_DEVICE_ID + " INTEGER, " + - EXPIRY + " INTEGER, " + MESSAGE_CLASS + " TEXT, " + MESSAGE_TYPE + " INTEGER, " + - MMS_VERSION + " INTEGER, " + MESSAGE_SIZE + " INTEGER, " + PRIORITY + " INTEGER, " + - READ_REPORT + " INTEGER, " + REPORT_ALLOWED + " INTEGER, " + RESPONSE_STATUS + " INTEGER, " + - 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, " + + EXPIRY + " INTEGER, " + "m_cls" + " TEXT, " + MESSAGE_TYPE + " INTEGER, " + + "v" + " INTEGER, " + MESSAGE_SIZE + " INTEGER, " + "pri" + " INTEGER, " + + "rr" + " INTEGER, " + "rpt_a" + " INTEGER, " + "resp_st" + " INTEGER, " + + STATUS + " INTEGER, " + TRANSACTION_ID + " TEXT, " + "retr_st" + " INTEGER, " + + "retr_txt" + " TEXT, " + "retr_txt_cs" + " INTEGER, " + "read_status" + " INTEGER, " + + "ct_cls" + " INTEGER, " + "resp_txt" + " TEXT, " + "d_tm" + " INTEGER, " + RECEIPT_COUNT + " INTEGER DEFAULT 0, " + MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + - NETWORK_FAILURE + " TEXT DEFAULT NULL," + DELIVERY_REPORT + " INTEGER);"; + NETWORK_FAILURE + " TEXT DEFAULT NULL," + "d_rpt" + " INTEGER);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");", @@ -159,11 +130,10 @@ public class MmsDatabase extends MessagingDatabase { private static final String[] MMS_PROJECTION = new String[] { ID, THREAD_ID, DATE_SENT + " AS " + NORMALIZED_DATE_SENT, DATE_RECEIVED + " AS " + NORMALIZED_DATE_RECEIVED, - MESSAGE_BOX, READ, MESSAGE_ID, SUBJECT, SUBJECT_CHARSET, CONTENT_TYPE, - CONTENT_LOCATION, EXPIRY, MESSAGE_CLASS, MESSAGE_TYPE, MMS_VERSION, - MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS, - RETRIEVE_TEXT, RETRIEVE_TEXT_CS, READ_STATUS, CONTENT_CLASS, RESPONSE_TEXT, - DELIVERY_TIME, DELIVERY_REPORT, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID, + MESSAGE_BOX, READ, + CONTENT_LOCATION, EXPIRY, MESSAGE_TYPE, + MESSAGE_SIZE, STATUS, TRANSACTION_ID, + BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID, RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE }; @@ -226,7 +196,7 @@ public class MmsDatabase extends MessagingDatabase { while (cursor.moveToNext()) { if (Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)))) { - List addresses = addressDatabase.getAddressesForId(cursor.getLong(cursor.getColumnIndexOrThrow(ID))); + List addresses = addressDatabase.getAddressesListForId(cursor.getLong(cursor.getColumnIndexOrThrow(ID))); for (String storedAddress : addresses) { try { @@ -280,19 +250,13 @@ public class MmsDatabase extends MessagingDatabase { return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients); } - try { - PduHeaders headers = retrieved.getPduHeaders(); - Set group = new HashSet(); + Set group = new HashSet<>(); - EncodedStringValue encodedFrom = headers.getEncodedStringValue(PduHeaders.FROM); - EncodedStringValue[] encodedCcList = headers.getEncodedStringValues(PduHeaders.CC); - EncodedStringValue[] encodedToList = headers.getEncodedStringValues(PduHeaders.TO); - - if (encodedFrom == null) { + if (retrieved.getAddresses().getFrom() == null) { throw new MmsException("FROM value in PduHeaders did not exist."); } - group.add(new String(encodedFrom.getTextString(), CharacterSets.MIMENAME_ISO_8859_1)); + group.add(retrieved.getAddresses().getFrom()); TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); String localNumber = telephonyManager.getLine1Number(); @@ -301,46 +265,35 @@ public class MmsDatabase extends MessagingDatabase { localNumber = TextSecurePreferences.getLocalNumber(context); } - if (encodedCcList != null) { - for (EncodedStringValue encodedCc : encodedCcList) { - String cc = new String(encodedCc.getTextString(), CharacterSets.MIMENAME_ISO_8859_1); + for (String cc : retrieved.getAddresses().getCc()) { + PhoneNumberUtil.MatchType match; - PhoneNumberUtil.MatchType match; + if (localNumber == null) match = PhoneNumberUtil.MatchType.NO_MATCH; + else match = PhoneNumberUtil.getInstance().isNumberMatch(localNumber, cc); - if (localNumber == null) match = PhoneNumberUtil.MatchType.NO_MATCH; - else match = PhoneNumberUtil.getInstance().isNumberMatch(localNumber, cc); - - if (match == PhoneNumberUtil.MatchType.NO_MATCH || - match == PhoneNumberUtil.MatchType.NOT_A_NUMBER) - { - group.add(cc); - } + if (match == PhoneNumberUtil.MatchType.NO_MATCH || + match == PhoneNumberUtil.MatchType.NOT_A_NUMBER) + { + group.add(cc); } } - if (encodedToList != null && (encodedToList.length > 1 || group.size() > 1)) { - for (EncodedStringValue encodedTo : encodedToList) { - String to = new String(encodedTo.getTextString(), CharacterSets.MIMENAME_ISO_8859_1); + for (String to : retrieved.getAddresses().getTo()) { + PhoneNumberUtil.MatchType match; - PhoneNumberUtil.MatchType match; + if (localNumber == null) match = PhoneNumberUtil.MatchType.NO_MATCH; + else match = PhoneNumberUtil.getInstance().isNumberMatch(localNumber, to); - if (localNumber == null) match = PhoneNumberUtil.MatchType.NO_MATCH; - else match = PhoneNumberUtil.getInstance().isNumberMatch(localNumber, to); - - if (match == PhoneNumberUtil.MatchType.NO_MATCH || - match == PhoneNumberUtil.MatchType.NOT_A_NUMBER) - { - group.add(to); - } + if (match == PhoneNumberUtil.MatchType.NO_MATCH || + match == PhoneNumberUtil.MatchType.NOT_A_NUMBER) + { + group.add(to); } } String recipientsList = Util.join(group, ","); Recipients recipients = RecipientFactory.getRecipientsFromString(context, recipientsList, false); return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } } private long getThreadIdFor(@NonNull NotificationInd notification) { @@ -354,7 +307,7 @@ public class MmsDatabase extends MessagingDatabase { public Cursor getMessage(long messageId) { SQLiteDatabase db = databaseHelper.getReadableDatabase(); - Cursor cursor = db.query(TABLE_NAME, MMS_PROJECTION, ID_WHERE, new String[] {messageId+""}, + Cursor cursor = db.query(TABLE_NAME, MMS_PROJECTION, ID_WHERE, new String[] {messageId + ""}, null, null, null); setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId)); return cursor; @@ -399,13 +352,7 @@ public class MmsDatabase extends MessagingDatabase { notifyConversationListeners(getThreadIdForMessage(messageId)); } - public void markAsSent(long messageId, byte[] mmsId, long status) { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - ContentValues contentValues = new ContentValues(); - contentValues.put(RESPONSE_STATUS, status); - contentValues.put(MESSAGE_ID, new String(mmsId)); - - database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {messageId+""}); + public void markAsSent(long messageId) { updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE); notifyConversationListeners(getThreadIdForMessage(messageId)); } @@ -498,69 +445,74 @@ public class MmsDatabase extends MessagingDatabase { } public Optional getNotification(long messageId) { - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context); - - Cursor cursor = null; + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + Cursor cursor = null; try { cursor = db.query(TABLE_NAME, MMS_PROJECTION, ID_WHERE, new String[] {String.valueOf(messageId)}, null, null, null); if (cursor != null && cursor.moveToNext()) { - PduHeaders headers = getHeadersFromCursor(cursor); - addressDatabase.getAddressesForId(messageId, headers); + PduHeaders headers = new PduHeaders(); + PduHeadersBuilder builder = new PduHeadersBuilder(headers, cursor); + builder.addText(CONTENT_LOCATION, PduHeaders.CONTENT_LOCATION); + builder.addLong(NORMALIZED_DATE_SENT, PduHeaders.DATE); + builder.addLong(EXPIRY, PduHeaders.EXPIRY); + builder.addLong(MESSAGE_SIZE, PduHeaders.MESSAGE_SIZE); + builder.addText(TRANSACTION_ID, PduHeaders.TRANSACTION_ID); return Optional.of(new NotificationInd(headers)); } else { return Optional.absent(); } - } catch (InvalidHeaderValueException e) { - Log.w("MmsDatabase", e); - return Optional.absent(); } finally { if (cursor != null) cursor.close(); } } - public SendReq getOutgoingMessage(MasterSecret masterSecret, long messageId) + public OutgoingMediaMessage getOutgoingMessage(MasterSecret masterSecret, long messageId) throws MmsException, NoSuchMessageException { - MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context); - PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context); - SQLiteDatabase database = databaseHelper.getReadableDatabase(); - MasterCipher masterCipher = new MasterCipher(masterSecret); - Cursor cursor = null; - - String selection = ID_WHERE; - String[] selectionArgs = new String[]{String.valueOf(messageId)}; + MmsAddressDatabase addr = DatabaseFactory.getMmsAddressDatabase(context); + AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + Cursor cursor = null; try { - cursor = database.query(TABLE_NAME, MMS_PROJECTION, selection, selectionArgs, null, null, null); + cursor = database.query(TABLE_NAME, MMS_PROJECTION, ID_WHERE, new String[]{String.valueOf(messageId)}, null, null, null); if (cursor != null && cursor.moveToNext()) { - long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)); - String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY)); - long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)); - PduHeaders headers = getHeadersFromCursor(cursor); - addr.getAddressesForId(messageId, headers); + long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX)); + String messageText = cursor.getString(cursor.getColumnIndexOrThrow(BODY)); + long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(NORMALIZED_DATE_SENT)); + List attachments = new LinkedList(attachmentDatabase.getAttachmentsForMessage(messageId)); + MmsAddresses addresses = addr.getAddressesForId(messageId); + List destinations = new LinkedList<>(); + String body = getDecryptedBody(masterSecret, messageText, outboxType); - PduBody body = getPartsAsBody(partDatabase.getParts(messageId)); + destinations.addAll(addresses.getBcc()); + destinations.addAll(addresses.getCc()); + destinations.addAll(addresses.getTo()); - try { - if (!TextUtils.isEmpty(messageText) && Types.isSymmetricEncryption(outboxType)) { - body.addPart(new TextSlide(context, masterCipher.decryptBody(messageText)).getPart()); - } else if (!TextUtils.isEmpty(messageText)) { - body.addPart(new TextSlide(context, messageText).getPart()); - } - } catch (InvalidMessageException e) { - Log.w("MmsDatabase", e); + Recipients recipients = RecipientFactory.getRecipientsFromStrings(context, destinations, false); + + if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) { + return new OutgoingGroupMediaMessage(recipients, body, attachments, timestamp); } - return new SendReq(headers, body, messageId, outboxType, timestamp); + OutgoingMediaMessage message = new OutgoingMediaMessage(recipients, body, attachments, timestamp, + !addresses.getBcc().isEmpty() ? ThreadDatabase.DistributionTypes.BROADCAST : + ThreadDatabase.DistributionTypes.DEFAULT); + if (Types.isSecureType(outboxType)) { + return new OutgoingSecureMediaMessage(message); + } + + return message; } throw new NoSuchMessageException("No record found for id: " + messageId); + } catch (IOException e) { + throw new MmsException(e); } finally { if (cursor != null) cursor.close(); @@ -569,20 +521,35 @@ public class MmsDatabase extends MessagingDatabase { public long copyMessageInbox(MasterSecret masterSecret, long messageId) throws MmsException { try { - SendReq request = getOutgoingMessage(masterSecret, messageId); - ContentValues contentValues = getContentValuesFromHeader(request.getPduHeaders()); - + OutgoingMediaMessage request = getOutgoingMessage(masterSecret, messageId); + ContentValues contentValues = new ContentValues(); + contentValues.put(ADDRESS, request.getRecipients().getPrimaryRecipient().getNumber()); + contentValues.put(DATE_SENT, request.getSentTimeMillis()); contentValues.put(MESSAGE_BOX, Types.BASE_INBOX_TYPE | Types.SECURE_MESSAGE_BIT | Types.ENCRYPTION_SYMMETRIC_BIT); contentValues.put(THREAD_ID, getThreadIdForMessage(messageId)); contentValues.put(READ, 1); contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT)); - for (int i = 0; i < request.getBody().getPartsNum(); i++) { - request.getBody().getPart(i).setTransferProgress(PartDatabase.TRANSFER_PROGRESS_DONE); + List attachments = new LinkedList<>(); + + for (Attachment attachment : request.getAttachments()) { + DatabaseAttachment databaseAttachment = (DatabaseAttachment)attachment; + attachments.add(new DatabaseAttachment(databaseAttachment.getAttachmentId(), + databaseAttachment.getMmsId(), + databaseAttachment.hasData(), + databaseAttachment.getContentType(), + AttachmentDatabase.TRANSFER_PROGRESS_DONE, + databaseAttachment.getSize(), + databaseAttachment.getLocation(), + databaseAttachment.getKey(), + databaseAttachment.getRelay())); } - return insertMediaMessage(new MasterSecretUnion(masterSecret), request.getPduHeaders(), - request.getBody(), contentValues); + return insertMediaMessage(new MasterSecretUnion(masterSecret), + MmsAddresses.forTo(request.getRecipients().toNumberStringList(false)), + request.getBody(), + attachments, + contentValues); } catch (NoSuchMessageException e) { throw new MmsException(e); } @@ -594,11 +561,6 @@ public class MmsDatabase extends MessagingDatabase { long threadId, long mailbox) throws MmsException { - PduHeaders headers = retrieved.getPduHeaders(); - ContentValues contentValues = getContentValuesFromHeader(headers); - boolean unread = org.thoughtcrime.securesms.util.Util.isDefaultSmsProvider(context) || - ((mailbox & Types.SECURE_MESSAGE_BIT) != 0); - if (threadId == -1 || retrieved.isGroupMessage()) { try { threadId = getThreadIdFor(retrieved); @@ -609,24 +571,28 @@ public class MmsDatabase extends MessagingDatabase { } } + ContentValues contentValues = new ContentValues(); + + contentValues.put(DATE_SENT, retrieved.getSentTimeMillis()); + contentValues.put(ADDRESS, retrieved.getAddresses().getFrom()); + contentValues.put(MESSAGE_BOX, mailbox); + contentValues.put(MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF); contentValues.put(THREAD_ID, threadId); contentValues.put(CONTENT_LOCATION, contentLocation); contentValues.put(STATUS, Status.DOWNLOAD_INITIALIZED); contentValues.put(DATE_RECEIVED, generatePduCompatTimestamp()); - contentValues.put(READ, unread ? 0 : 1); + contentValues.put(READ, 0); if (!contentValues.containsKey(DATE_SENT)) { contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); } - long messageId = insertMediaMessage(masterSecret, retrieved.getPduHeaders(), - retrieved.getBody(), contentValues); - - if (unread) { - DatabaseFactory.getThreadDatabase(context).setUnread(threadId); - } + long messageId = insertMediaMessage(masterSecret, retrieved.getAddresses(), + retrieved.getBody(), retrieved.getAttachments(), + contentValues); + DatabaseFactory.getThreadDatabase(context).setUnread(threadId); DatabaseFactory.getThreadDatabase(context).update(threadId); notifyConversationListeners(threadId); jobManager.add(new TrimThreadJob(context, threadId)); @@ -677,12 +643,27 @@ public class MmsDatabase extends MessagingDatabase { public Pair insertMessageInbox(@NonNull NotificationInd notification) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context); - long threadId = getThreadIdFor(notification); - PduHeaders headers = notification.getPduHeaders(); - ContentValues contentValues = getContentValuesFromHeader(headers); + long threadId = getThreadIdFor(notification); + PduHeaders headers = notification.getPduHeaders(); + ContentValues contentValues = new ContentValues(); + ContentValuesBuilder contentBuilder = new ContentValuesBuilder(contentValues); + Log.w(TAG, "Message received type: " + headers.getOctet(PduHeaders.MESSAGE_TYPE)); + contentBuilder.add(CONTENT_LOCATION, headers.getTextString(PduHeaders.CONTENT_LOCATION)); + contentBuilder.add(DATE_SENT, headers.getLongInteger(PduHeaders.DATE) * 1000L); + contentBuilder.add(EXPIRY, headers.getLongInteger(PduHeaders.EXPIRY)); + contentBuilder.add(MESSAGE_SIZE, headers.getLongInteger(PduHeaders.MESSAGE_SIZE)); + contentBuilder.add(TRANSACTION_ID, headers.getTextString(PduHeaders.TRANSACTION_ID)); + contentBuilder.add(MESSAGE_TYPE, headers.getOctet(PduHeaders.MESSAGE_TYPE)); + + if (headers.getEncodedStringValue(PduHeaders.FROM) != null) { + contentBuilder.add(ADDRESS, headers.getEncodedStringValue(PduHeaders.FROM).getTextString()); + } else { + contentBuilder.add(ADDRESS, null); + } + contentValues.put(MESSAGE_BOX, Types.BASE_INBOX_TYPE); contentValues.put(THREAD_ID, threadId); contentValues.put(STATUS, Status.DOWNLOAD_INITIALIZED); @@ -693,7 +674,7 @@ public class MmsDatabase extends MessagingDatabase { contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); long messageId = db.insert(TABLE_NAME, null, contentValues); - addressDatabase.insertAddressesForId(messageId, headers); + addressDatabase.insertAddressesForId(messageId, MmsAddresses.forFrom(Util.toIsoString(notification.getFrom().getTextString()))); return new Pair<>(messageId, threadId); } @@ -711,7 +692,7 @@ public class MmsDatabase extends MessagingDatabase { public long insertMessageOutbox(@NonNull MasterSecretUnion masterSecret, @NonNull OutgoingMediaMessage message, - long threadId, boolean forceSms, long timestamp) + long threadId, boolean forceSms) throws MmsException { long type = Types.BASE_OUTBOX_TYPE; @@ -727,26 +708,21 @@ public class MmsDatabase extends MessagingDatabase { else if (((OutgoingGroupMediaMessage)message).isGroupQuit()) type |= Types.GROUP_QUIT_BIT; } - SendReq sendRequest = new SendReq(); - sendRequest.setDate(timestamp / 1000L); - sendRequest.setBody(message.getPduBody()); - sendRequest.setContentType(ContentType.MULTIPART_MIXED.getBytes()); + List recipientNumbers = message.getRecipients().toNumberStringList(true); - String[] recipientsArray = message.getRecipients().toNumberStringArray(true); - EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(recipientsArray); + MmsAddresses addresses; - if (message.getRecipients().isSingleRecipient()) { - sendRequest.setTo(encodedNumbers); - } else if (message.getDistributionType() == ThreadDatabase.DistributionTypes.BROADCAST) { - sendRequest.setBcc(encodedNumbers); - } else if (message.getDistributionType() == ThreadDatabase.DistributionTypes.CONVERSATION || - message.getDistributionType() == 0) + if (!message.getRecipients().isSingleRecipient() && + message.getDistributionType() == ThreadDatabase.DistributionTypes.BROADCAST) { - sendRequest.setTo(encodedNumbers); + addresses = MmsAddresses.forBcc(recipientNumbers); + } else { + addresses = MmsAddresses.forTo(recipientNumbers); } - PduHeaders headers = sendRequest.getPduHeaders(); - ContentValues contentValues = getContentValuesFromHeader(headers); + ContentValues contentValues = new ContentValues(); + contentValues.put(DATE_SENT, message.getSentTimeMillis()); + contentValues.put(MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ); contentValues.put(MESSAGE_BOX, type); contentValues.put(THREAD_ID, threadId); @@ -754,15 +730,9 @@ public class MmsDatabase extends MessagingDatabase { contentValues.put(DATE_RECEIVED, contentValues.getAsLong(DATE_SENT)); contentValues.remove(ADDRESS); - if (sendRequest.getBody() != null) { - for (int i = 0; i < sendRequest.getBody().getPartsNum(); i++) { - sendRequest.getBody().getPart(i).setTransferProgress(PartDatabase.TRANSFER_PROGRESS_STARTED); - } - } + long messageId = insertMediaMessage(masterSecret, addresses, message.getBody(), + message.getAttachments(), contentValues); - long messageId = insertMediaMessage(masterSecret, - sendRequest.getPduHeaders(), - sendRequest.getBody(), contentValues); jobManager.add(new TrimThreadJob(context, threadId)); return messageId; @@ -776,35 +746,50 @@ public class MmsDatabase extends MessagingDatabase { } } - private long insertMediaMessage(MasterSecretUnion masterSecret, - PduHeaders headers, - PduBody body, - ContentValues contentValues) + private @Nullable String getDecryptedBody(@NonNull MasterSecret masterSecret, + @Nullable String body, long outboxType) + { + try { + if (!TextUtils.isEmpty(body) && Types.isSymmetricEncryption(outboxType)) { + MasterCipher masterCipher = new MasterCipher(masterSecret); + return masterCipher.decryptBody(body); + } else { + return body; + } + } catch (InvalidMessageException e) { + Log.w(TAG, e); + } + + return null; + } + + private long insertMediaMessage(@NonNull MasterSecretUnion masterSecret, + @NonNull MmsAddresses addresses, + @Nullable String body, + @NonNull List attachments, + @NonNull ContentValues contentValues) throws MmsException { SQLiteDatabase db = databaseHelper.getWritableDatabase(); - PartDatabase partsDatabase = DatabaseFactory.getPartDatabase(context); + AttachmentDatabase partsDatabase = DatabaseFactory.getAttachmentDatabase(context); MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context); if (Types.isSymmetricEncryption(contentValues.getAsLong(MESSAGE_BOX)) || Types.isAsymmetricEncryption(contentValues.getAsLong(MESSAGE_BOX))) { - String messageText = PartParser.getMessageText(body); - body = PartParser.getSupportedMediaParts(body); - - if (!TextUtils.isEmpty(messageText)) { - contentValues.put(BODY, getEncryptedBody(masterSecret, messageText)); + if (!TextUtils.isEmpty(body)) { + contentValues.put(BODY, getEncryptedBody(masterSecret, body)); } } - contentValues.put(PART_COUNT, PartParser.getSupportedMediaPartCount(body)); + contentValues.put(PART_COUNT, attachments.size()); db.beginTransaction(); try { long messageId = db.insert(TABLE_NAME, null, contentValues); - addressDatabase.insertAddressesForId(messageId, headers); - partsDatabase.insertParts(masterSecret, messageId, body); + addressDatabase.insertAddressesForId(messageId, addresses); + partsDatabase.insertAttachmentsForMessage(masterSecret, messageId, attachments); notifyConversationListeners(contentValues.getAsLong(THREAD_ID)); DatabaseFactory.getThreadDatabase(context).update(contentValues.getAsLong(THREAD_ID)); @@ -817,10 +802,10 @@ public class MmsDatabase extends MessagingDatabase { } public boolean delete(long messageId) { - long threadId = getThreadIdForMessage(messageId); - MmsAddressDatabase addrDatabase = DatabaseFactory.getMmsAddressDatabase(context); - PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context); - partDatabase.deleteParts(messageId); + long threadId = getThreadIdForMessage(messageId); + MmsAddressDatabase addrDatabase = DatabaseFactory.getMmsAddressDatabase(context); + AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); + attachmentDatabase.deleteAttachmentsForMessage(messageId); addrDatabase.deleteAddressesForId(messageId); SQLiteDatabase database = databaseHelper.getWritableDatabase(); @@ -831,7 +816,7 @@ public class MmsDatabase extends MessagingDatabase { } public void deleteThread(long threadId) { - Set singleThreadSet = new HashSet(); + Set singleThreadSet = new HashSet<>(); singleThreadSet.add(threadId); deleteThreads(singleThreadSet); } @@ -889,7 +874,7 @@ public class MmsDatabase extends MessagingDatabase { public void deleteAllThreads() { - DatabaseFactory.getPartDatabase(context).deleteAllParts(); + DatabaseFactory.getAttachmentDatabase(context).deleteAllAttachments(); DatabaseFactory.getMmsAddressDatabase(context).deleteAllAddresses(); SQLiteDatabase database = databaseHelper.getWritableDatabase(); @@ -911,72 +896,6 @@ public class MmsDatabase extends MessagingDatabase { } } - private PduHeaders getHeadersFromCursor(Cursor cursor) throws InvalidHeaderValueException { - PduHeaders headers = new PduHeaders(); - PduHeadersBuilder phb = new PduHeadersBuilder(headers, cursor); - - phb.add(RETRIEVE_TEXT, RETRIEVE_TEXT_CS, PduHeaders.RETRIEVE_TEXT); - phb.add(SUBJECT, SUBJECT_CHARSET, PduHeaders.SUBJECT); - phb.addText(CONTENT_LOCATION, PduHeaders.CONTENT_LOCATION); - phb.addText(CONTENT_TYPE, PduHeaders.CONTENT_TYPE); - phb.addText(MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS); - phb.addText(MESSAGE_ID, PduHeaders.MESSAGE_ID); - phb.addText(RESPONSE_TEXT, PduHeaders.RESPONSE_TEXT); - phb.addText(TRANSACTION_ID, PduHeaders.TRANSACTION_ID); - phb.addOctet(CONTENT_CLASS, PduHeaders.CONTENT_CLASS); - phb.addOctet(DELIVERY_REPORT, PduHeaders.DELIVERY_REPORT); - phb.addOctet(MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE); - phb.addOctet(MMS_VERSION, PduHeaders.MMS_VERSION); - phb.addOctet(PRIORITY, PduHeaders.PRIORITY); - phb.addOctet(READ_STATUS, PduHeaders.READ_STATUS); - phb.addOctet(REPORT_ALLOWED, PduHeaders.REPORT_ALLOWED); - phb.addOctet(RETRIEVE_STATUS, PduHeaders.RETRIEVE_STATUS); - phb.addOctet(STATUS, PduHeaders.STATUS); - phb.addLong(NORMALIZED_DATE_SENT, PduHeaders.DATE); - phb.addLong(DELIVERY_TIME, PduHeaders.DELIVERY_TIME); - phb.addLong(EXPIRY, PduHeaders.EXPIRY); - phb.addLong(MESSAGE_SIZE, PduHeaders.MESSAGE_SIZE); - - headers.setLongInteger(headers.getLongInteger(PduHeaders.DATE) / 1000L, PduHeaders.DATE); - - return headers; - } - - private ContentValues getContentValuesFromHeader(PduHeaders headers) { - ContentValues contentValues = new ContentValues(); - ContentValuesBuilder cvb = new ContentValuesBuilder(contentValues); - - cvb.add(RETRIEVE_TEXT, RETRIEVE_TEXT_CS, headers.getEncodedStringValue(PduHeaders.RETRIEVE_TEXT)); - cvb.add(SUBJECT, SUBJECT_CHARSET, headers.getEncodedStringValue(PduHeaders.SUBJECT)); - cvb.add(CONTENT_LOCATION, headers.getTextString(PduHeaders.CONTENT_LOCATION)); - cvb.add(CONTENT_TYPE, headers.getTextString(PduHeaders.CONTENT_TYPE)); - cvb.add(MESSAGE_CLASS, headers.getTextString(PduHeaders.MESSAGE_CLASS)); - cvb.add(MESSAGE_ID, headers.getTextString(PduHeaders.MESSAGE_ID)); - cvb.add(RESPONSE_TEXT, headers.getTextString(PduHeaders.RESPONSE_TEXT)); - cvb.add(TRANSACTION_ID, headers.getTextString(PduHeaders.TRANSACTION_ID)); - cvb.add(CONTENT_CLASS, headers.getOctet(PduHeaders.CONTENT_CLASS)); - cvb.add(DELIVERY_REPORT, headers.getOctet(PduHeaders.DELIVERY_REPORT)); - cvb.add(MESSAGE_TYPE, headers.getOctet(PduHeaders.MESSAGE_TYPE)); - cvb.add(MMS_VERSION, headers.getOctet(PduHeaders.MMS_VERSION)); - cvb.add(PRIORITY, headers.getOctet(PduHeaders.PRIORITY)); - cvb.add(READ_REPORT, headers.getOctet(PduHeaders.READ_REPORT)); - cvb.add(READ_STATUS, headers.getOctet(PduHeaders.READ_STATUS)); - cvb.add(REPORT_ALLOWED, headers.getOctet(PduHeaders.REPORT_ALLOWED)); - cvb.add(RETRIEVE_STATUS, headers.getOctet(PduHeaders.RETRIEVE_STATUS)); - cvb.add(STATUS, headers.getOctet(PduHeaders.STATUS)); - cvb.add(DATE_SENT, headers.getLongInteger(PduHeaders.DATE) * 1000L); - cvb.add(DELIVERY_TIME, headers.getLongInteger(PduHeaders.DELIVERY_TIME)); - cvb.add(EXPIRY, headers.getLongInteger(PduHeaders.EXPIRY)); - cvb.add(MESSAGE_SIZE, headers.getLongInteger(PduHeaders.MESSAGE_SIZE)); - - if (headers.getEncodedStringValue(PduHeaders.FROM) != null) - cvb.add(ADDRESS, headers.getEncodedStringValue(PduHeaders.FROM).getTextString()); - else - cvb.add(ADDRESS, null); - - return cvb.getContentValues(); - } - public Reader readerFor(MasterSecret masterSecret, Cursor cursor) { return new Reader(masterSecret, cursor); } @@ -1171,11 +1090,16 @@ public class MmsDatabase extends MessagingDatabase { Callable task = new Callable() { @Override public SlideDeck call() throws Exception { - PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context); - PduBody body = getPartsAsBody(partDatabase.getParts(id)); - SlideDeck slideDeck = new SlideDeck(context, body); + AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); + List attachments = new LinkedList(attachmentDatabase.getAttachmentsForMessage(id)); + SlideDeck slideDeck = new SlideDeck(context, attachments); + boolean progress = false; - if (!body.containsPushInProgress()) { + for (Attachment attachment : attachments) { + if (attachment.isInProgress()) progress = true; + } + + if (!progress) { slideCache.put(timestamp + "::" + id, new SoftReference<>(slideDeck)); } @@ -1183,7 +1107,7 @@ public class MmsDatabase extends MessagingDatabase { } }; - future = new ListenableFutureTask<>(task); + future = new ListenableFutureTask<>(task, timestamp + "::" + id); slideResolver.execute(future); return future; @@ -1222,15 +1146,4 @@ public class MmsDatabase extends MessagingDatabase { final long time = System.currentTimeMillis(); return time - (time % 1000); } - - private PduBody getPartsAsBody(List parts) { - PduBody body = new PduBody(); - - for (PduPart part : parts) { - body.addPart(part); - } - - return body; - } - } diff --git a/src/org/thoughtcrime/securesms/database/PartDatabase.java b/src/org/thoughtcrime/securesms/database/PartDatabase.java deleted file mode 100644 index 9c993973d3..0000000000 --- a/src/org/thoughtcrime/securesms/database/PartDatabase.java +++ /dev/null @@ -1,713 +0,0 @@ -/** - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (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 . - */ -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.graphics.Bitmap; -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.Log; -import android.util.Pair; - -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; -import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream; -import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.crypto.MasterSecretUnion; -import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirement; -import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirementProvider; -import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData; -import org.thoughtcrime.securesms.util.Util; -import org.thoughtcrime.securesms.util.VisibleForTesting; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Serializable; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; - -import de.greenrobot.event.EventBus; -import ws.com.google.android.mms.ContentType; -import ws.com.google.android.mms.MmsException; -import ws.com.google.android.mms.pdu.PduBody; -import ws.com.google.android.mms.pdu.PduPart; - -public class PartDatabase extends Database { - private static final String TAG = PartDatabase.class.getSimpleName(); - - private static final String TABLE_NAME = "part"; - private static final String ROW_ID = "_id"; - private static final String MMS_ID = "mid"; - private static final String SEQUENCE = "seq"; - private static final String CONTENT_TYPE = "ct"; - private static final String NAME = "name"; - private static final String CHARSET = "chset"; - private static final String CONTENT_DISPOSITION = "cd"; - private static final String FILENAME = "fn"; - private static final String CONTENT_ID = "cid"; - private static final String CONTENT_LOCATION = "cl"; - private static final String CONTENT_TYPE_START = "ctt_s"; - private static final String CONTENT_TYPE_TYPE = "ctt_t"; - private static final String ENCRYPTED = "encrypted"; - private static final String DATA = "_data"; - private static final String TRANSFER_STATE = "pending_push"; - private static final String SIZE = "data_size"; - private static final String THUMBNAIL = "thumbnail"; - private static final String ASPECT_RATIO = "aspect_ratio"; - private static final String UNIQUE_ID = "unique_id"; - - public static final int TRANSFER_PROGRESS_DONE = 0; - public static final int TRANSFER_PROGRESS_STARTED = 1; - public static final int TRANSFER_PROGRESS_AUTO_PENDING = 2; - public static final int TRANSFER_PROGRESS_FAILED = 3; - - private static final String PART_ID_WHERE = ROW_ID + " = ? AND " + UNIQUE_ID + " = ?"; - - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " + - MMS_ID + " INTEGER, " + SEQUENCE + " INTEGER DEFAULT 0, " + - CONTENT_TYPE + " TEXT, " + NAME + " TEXT, " + CHARSET + " INTEGER, " + - CONTENT_DISPOSITION + " TEXT, " + FILENAME + " TEXT, " + CONTENT_ID + " TEXT, " + - CONTENT_LOCATION + " TEXT, " + CONTENT_TYPE_START + " INTEGER, " + - CONTENT_TYPE_TYPE + " TEXT, " + ENCRYPTED + " INTEGER, " + - TRANSFER_STATE + " INTEGER, "+ DATA + " TEXT, " + SIZE + " INTEGER, " + - THUMBNAIL + " TEXT, " + ASPECT_RATIO + " REAL, " + UNIQUE_ID + " INTEGER NOT NULL);"; - - public static final String[] CREATE_INDEXS = { - "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", - "CREATE INDEX IF NOT EXISTS pending_push_index ON " + TABLE_NAME + " (" + TRANSFER_STATE + ");", - }; - - private final static String IMAGES_QUERY = "SELECT " + TABLE_NAME + "." + ROW_ID + ", " - + TABLE_NAME + "." + CONTENT_TYPE + ", " - + TABLE_NAME + "." + ASPECT_RATIO + ", " - + TABLE_NAME + "." + UNIQUE_ID + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.NORMALIZED_DATE_RECEIVED + ", " - + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ADDRESS + " " - + "FROM " + TABLE_NAME + " LEFT JOIN " + MmsDatabase.TABLE_NAME - + " ON " + TABLE_NAME + "." + MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " - + "WHERE " + MMS_ID + " IN (SELECT " + MmsSmsColumns.ID - + " FROM " + MmsDatabase.TABLE_NAME - + " WHERE " + MmsDatabase.THREAD_ID + " = ?) AND " - + CONTENT_TYPE + " LIKE 'image/%' " - + "ORDER BY " + TABLE_NAME + "." + ROW_ID + " DESC"; - - - private final ExecutorService thumbnailExecutor = Util.newSingleThreadedLifoExecutor(); - - public PartDatabase(Context context, SQLiteOpenHelper databaseHelper) { - super(context, databaseHelper); - } - - public InputStream getPartStream(MasterSecret masterSecret, PartId partId) - throws FileNotFoundException - { - return getDataStream(masterSecret, partId, DATA); - } - - public void updateFailedDownloadedPart(long messageId, PartId partId, PduPart part) - throws MmsException - { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - - part.setContentDisposition(new byte[0]); - part.setTransferProgress(TRANSFER_PROGRESS_FAILED); - - ContentValues values = getContentValuesForPart(part); - - values.put(DATA, (String)null); - - database.update(TABLE_NAME, values, PART_ID_WHERE, partId.toStrings()); - notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId)); - } - - public PduPart getPart(PartId partId) { - SQLiteDatabase database = databaseHelper.getReadableDatabase(); - Cursor cursor = null; - - try { - cursor = database.query(TABLE_NAME, null, PART_ID_WHERE, partId.toStrings(), null, null, null); - - if (cursor != null && cursor.moveToFirst()) return getPart(cursor); - else return null; - - } finally { - if (cursor != null) - cursor.close(); - } - } - - public Cursor getImagesForThread(long threadId) { - SQLiteDatabase database = databaseHelper.getReadableDatabase(); - Cursor cursor = database.rawQuery(IMAGES_QUERY, new String[]{threadId+""}); - setNotifyConverationListeners(cursor, threadId); - return cursor; - } - - public List getParts(long mmsId) { - SQLiteDatabase database = databaseHelper.getReadableDatabase(); - List results = new LinkedList<>(); - Cursor cursor = null; - - try { - cursor = database.query(TABLE_NAME, null, MMS_ID + " = ?", new String[] {mmsId+""}, - null, null, null); - - while (cursor != null && cursor.moveToNext()) { - results.add(getPart(cursor)); - } - - return results; - } finally { - if (cursor != null) - cursor.close(); - } - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - public void deleteParts(long mmsId) { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - Cursor cursor = null; - - try { - cursor = database.query(TABLE_NAME, new String[] {DATA, THUMBNAIL}, MMS_ID + " = ?", - new String[] {mmsId+""}, null, null, null); - - while (cursor != null && cursor.moveToNext()) { - String data = cursor.getString(0); - String thumbnail = cursor.getString(1); - - if (!TextUtils.isEmpty(data)) { - new File(data).delete(); - } - - if (!TextUtils.isEmpty(thumbnail)) { - new File(thumbnail).delete(); - } - } - } finally { - if (cursor != null) - cursor.close(); - } - - database.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {mmsId + ""}); - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - public void deleteAllParts() { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - database.delete(TABLE_NAME, null, null); - - File partsDirectory = context.getDir("parts", Context.MODE_PRIVATE); - File[] parts = partsDirectory.listFiles(); - - for (File part : parts) { - part.delete(); - } - } - - void insertParts(MasterSecretUnion masterSecret, long mmsId, PduBody body) throws MmsException { - Log.w(TAG, "insertParts(" + body.getPartsNum() + ")"); - for (int i=0;i writePartData(MasterSecret masterSecret, PduPart part, InputStream in) - throws MmsException - { - try { - File partsDirectory = context.getDir("parts", Context.MODE_PRIVATE); - File dataFile = File.createTempFile("part", ".mms", partsDirectory); - OutputStream out = getPartOutputStream(masterSecret, dataFile, part); - long plaintextLength = Util.copy(in, out); - - return new Pair<>(dataFile, plaintextLength); - } catch (IOException e) { - throw new MmsException(e); - } - } - - private Pair writePartData(MasterSecret masterSecret, PduPart part) - throws MmsException - { - try { - if (part.getData() != null) { - Log.w(TAG, "Writing part data from buffer"); - return writePartData(masterSecret, part, new ByteArrayInputStream(part.getData())); - } else if (part.getDataUri() != null) { - Log.w(TAG, "Writing part data from URI"); - InputStream in = PartAuthority.getPartStream(context, masterSecret, part.getDataUri()); - return writePartData(masterSecret, part, in); - } else { - throw new MmsException("Part is empty!"); - } - } catch (IOException e) { - throw new MmsException(e); - } - } - - public InputStream getThumbnailStream(MasterSecret masterSecret, PartId partId) throws IOException { - Log.w(TAG, "getThumbnailStream(" + partId + ")"); - final InputStream dataStream = getDataStream(masterSecret, partId, THUMBNAIL); - if (dataStream != null) { - return dataStream; - } - - try { - return thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret, partId)).get(); - } catch (InterruptedException ie) { - throw new AssertionError("interrupted"); - } catch (ExecutionException ee) { - Log.w(TAG, ee); - throw new IOException(ee); - } - } - - private PduPart getPart(Cursor cursor) { - PduPart part = new PduPart(); - - getPartValues(part, cursor); - - return part; - } - - public @NonNull List getPendingParts() { - final SQLiteDatabase database = databaseHelper.getReadableDatabase(); - final List parts = new LinkedList<>(); - - Cursor cursor = null; - try { - cursor = database.query(TABLE_NAME, null, TRANSFER_STATE + " = ?", new String[] {String.valueOf(TRANSFER_PROGRESS_STARTED)}, null, null, null); - while (cursor != null && cursor.moveToNext()) { - parts.add(getPart(cursor)); - } - } finally { - if (cursor != null) cursor.close(); - } - - return parts; - } - - private PartId insertPart(MasterSecretUnion masterSecret, PduPart part, long mmsId, Bitmap thumbnail) throws MmsException { - Log.w(TAG, "inserting part to mms " + mmsId); - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - Pair partData = null; - - if ((part.getData() != null || part.getDataUri() != null) && masterSecret.getMasterSecret().isPresent()) { - partData = writePartData(masterSecret.getMasterSecret().get(), part); - Log.w(TAG, "Wrote part to file: " + partData.first.getAbsolutePath()); - } - - ContentValues contentValues = getContentValuesForPart(part); - contentValues.put(MMS_ID, mmsId); - - if (partData != null) { - contentValues.put(DATA, partData.first.getAbsolutePath()); - contentValues.put(SIZE, partData.second); - } - - long partRowId = database.insert(TABLE_NAME, null, contentValues); - PartId partId = new PartId(partRowId, part.getUniqueId()); - - if (thumbnail != null && masterSecret.getMasterSecret().isPresent()) { - Log.w(TAG, "inserting pre-generated thumbnail"); - ThumbnailData data = new ThumbnailData(thumbnail); - updatePartThumbnail(masterSecret.getMasterSecret().get(), partId, part, data.toDataStream(), data.getAspectRatio()); - } else if (!part.isInProgress()) { - thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret.getMasterSecret().get(), partId)); - } - - return partId; - } - - public void updateDownloadedPart(MasterSecret masterSecret, long messageId, - PartId partId, PduPart part, InputStream data) - throws MmsException - { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - Pair partData = writePartData(masterSecret, part, data); - - part.setContentDisposition(new byte[0]); - part.setTransferProgress(TRANSFER_PROGRESS_DONE); - - ContentValues values = getContentValuesForPart(part); - - if (partData != null) { - values.put(DATA, partData.first.getAbsolutePath()); - values.put(SIZE, partData.second); - } - - database.update(TABLE_NAME, values, PART_ID_WHERE, partId.toStrings()); - - thumbnailExecutor.submit(new ThumbnailFetchCallable(masterSecret, partId)); - - notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId)); - } - - public void markPartUploaded(long messageId, PduPart part) { - ContentValues values = new ContentValues(1); - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - - part.setTransferProgress(TRANSFER_PROGRESS_DONE); - values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); - database.update(TABLE_NAME, values, PART_ID_WHERE, part.getPartId().toStrings()); - - notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId)); - } - - public void setTransferState(long messageId, @NonNull PartId partId, int transferState) { - final ContentValues values = new ContentValues(1); - final SQLiteDatabase database = databaseHelper.getWritableDatabase(); - - values.put(TRANSFER_STATE, transferState); - database.update(TABLE_NAME, values, PART_ID_WHERE, partId.toStrings()); - notifyConversationListeners(DatabaseFactory.getMmsDatabase(context).getThreadIdForMessage(messageId)); - ApplicationContext.getInstance(context).notifyMediaControlEvent(); - } - - public void updatePartData(MasterSecret masterSecret, PduPart part, InputStream data) - throws MmsException - { - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - Pair partData = writePartData(masterSecret, part, data); - - if (partData == null) throw new MmsException("couldn't update part data"); - - Cursor cursor = null; - try { - cursor = database.query(TABLE_NAME, new String[]{DATA}, PART_ID_WHERE, - part.getPartId().toStrings(), null, null, null); - - if (cursor != null && cursor.moveToFirst()) { - int dataColumn = cursor.getColumnIndexOrThrow(DATA); - if (!cursor.isNull(dataColumn) && !new File(cursor.getString(dataColumn)).delete()) { - Log.w(TAG, "Couldn't delete old part file"); - } - } - } finally { - if (cursor != null) cursor.close(); - } - ContentValues values = new ContentValues(2); - values.put(DATA, partData.first.getAbsolutePath()); - values.put(SIZE, partData.second); - - part.setDataSize(partData.second); - - database.update(TABLE_NAME, values, PART_ID_WHERE, part.getPartId().toStrings()); - Log.w(TAG, "updated data for part #" + part.getPartId()); - } - - public void updatePartThumbnail(MasterSecret masterSecret, PartId partId, PduPart part, InputStream in, float aspectRatio) - throws MmsException - { - Log.w(TAG, "updating part thumbnail for #" + partId); - - Pair thumbnailFile = writePartData(masterSecret, part, in); - - SQLiteDatabase database = databaseHelper.getWritableDatabase(); - ContentValues values = new ContentValues(2); - - values.put(THUMBNAIL, thumbnailFile.first.getAbsolutePath()); - values.put(ASPECT_RATIO, aspectRatio); - - database.update(TABLE_NAME, values, PART_ID_WHERE, partId.toStrings()); - } - - public static class ImageRecord { - private PartId partId; - private String contentType; - private String address; - private long date; - - private ImageRecord(PartId partId, String contentType, String address, long date) { - this.partId = partId; - this.contentType = contentType; - this.address = address; - this.date = date; - } - - public static ImageRecord from(Cursor cursor) { - PartId partId = new PartId(cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)), - cursor.getLong(cursor.getColumnIndexOrThrow(UNIQUE_ID))); - - return new ImageRecord(partId, - cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_TYPE)), - cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.ADDRESS)), - cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.NORMALIZED_DATE_RECEIVED))); - } - - public PartId getPartId() { - return partId; - } - - public String getContentType() { - return contentType; - } - - public String getAddress() { - return address; - } - - public long getDate() { - return date; - } - - public Uri getUri() { - return PartAuthority.getPartUri(partId); - } - } - - @VisibleForTesting class ThumbnailFetchCallable implements Callable { - private final MasterSecret masterSecret; - private final PartId partId; - - public ThumbnailFetchCallable(MasterSecret masterSecret, PartId partId) { - this.masterSecret = masterSecret; - this.partId = partId; - } - - @Override - public @Nullable InputStream call() throws Exception { - final InputStream stream = getDataStream(masterSecret, partId, THUMBNAIL); - if (stream != null) { - return stream; - } - - PduPart part = getPart(partId); - if (part.isInProgress()) { - return null; - } - - ThumbnailData data = MediaUtil.generateThumbnail(context, masterSecret, part.getDataUri(), Util.toIsoString(part.getContentType())); - if (data == null) { - return null; - } - - updatePartThumbnail(masterSecret, partId, part, data.toDataStream(), data.getAspectRatio()); - - return getDataStream(masterSecret, partId, THUMBNAIL); - } - } - - public static class PartId { - - private final long rowId; - private final long uniqueId; - - public PartId(long rowId, long uniqueId) { - this.rowId = rowId; - this.uniqueId = uniqueId; - } - - public long getRowId() { - return rowId; - } - - public long getUniqueId() { - return uniqueId; - } - - public String[] toStrings() { - return new String[] {String.valueOf(rowId), String.valueOf(uniqueId)}; - } - - public String toString() { - return "(row id: " + rowId + ", unique ID: " + uniqueId + ")"; - } - - public boolean isValid() { - return rowId >= 0 && uniqueId >= 0; - } - - @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - PartId partId = (PartId)o; - - if (rowId != partId.rowId) return false; - return uniqueId == partId.uniqueId; - } - - @Override public int hashCode() { - return Util.hashCode(rowId, uniqueId); - } - } -} diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index b54aaf1ff7..b197d98275 100644 --- a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -1,20 +1,21 @@ package org.thoughtcrime.securesms.jobs; import android.content.Context; +import android.text.TextUtils; import android.util.Log; +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.crypto.MediaKey; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.PartDatabase; -import org.thoughtcrime.securesms.database.PartDatabase.PartId; +import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; import org.thoughtcrime.securesms.jobs.requirements.MediaNetworkRequirement; import org.thoughtcrime.securesms.notifications.MessageNotifier; -import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.VisibleForTesting; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.requirements.NetworkRequirement; @@ -33,7 +34,6 @@ import javax.inject.Inject; import de.greenrobot.event.EventBus; import ws.com.google.android.mms.MmsException; -import ws.com.google.android.mms.pdu.PduPart; public class AttachmentDownloadJob extends MasterSecretJob implements InjectableType { private static final long serialVersionUID = 1L; @@ -45,18 +45,18 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable private final long partRowId; private final long partUniqueId; - public AttachmentDownloadJob(Context context, long messageId, PartId partId) { + public AttachmentDownloadJob(Context context, long messageId, AttachmentId attachmentId) { super(context, JobParameters.newBuilder() .withGroupId(AttachmentDownloadJob.class.getCanonicalName()) .withRequirement(new MasterSecretRequirement(context)) .withRequirement(new NetworkRequirement(context)) - .withRequirement(new MediaNetworkRequirement(context, messageId, partId)) + .withRequirement(new MediaNetworkRequirement(context, messageId, attachmentId)) .withPersistence() .create()); this.messageId = messageId; - this.partRowId = partId.getRowId(); - this.partUniqueId = partId.getUniqueId(); + this.partRowId = attachmentId.getRowId(); + this.partUniqueId = attachmentId.getUniqueId(); } @Override @@ -65,29 +65,29 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable @Override public void onRun(MasterSecret masterSecret) throws IOException { - final PartId partId = new PartId(partRowId, partUniqueId); - final PduPart part = DatabaseFactory.getPartDatabase(context).getPart(partId); + final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId); + final Attachment attachment = DatabaseFactory.getAttachmentDatabase(context).getAttachment(attachmentId); - if (part == null) { - Log.w(TAG, "part no longer exists."); - return; - } - if (part.getDataUri() != null) { - Log.w(TAG, "part was already downloaded."); + if (attachment == null) { + Log.w(TAG, "attachment no longer exists."); return; } - Log.w(TAG, "Downloading push part " + partId); + if (!attachment.isInProgress()) { + Log.w(TAG, "Attachment was already downloaded."); + return; + } - retrievePart(masterSecret, part, messageId); + Log.w(TAG, "Downloading push part " + attachmentId); + + retrieveAttachment(masterSecret, messageId, attachmentId, attachment); MessageNotifier.updateNotification(context, masterSecret); } @Override public void onCanceled() { - final PartId partId = new PartId(partRowId, partUniqueId); - final PduPart part = DatabaseFactory.getPartDatabase(context).getPart(partId); - markFailed(messageId, part, part.getPartId()); + final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId); + markFailed(messageId, attachmentId); } @Override @@ -95,28 +95,31 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable return (exception instanceof PushNetworkException); } - private void retrievePart(MasterSecret masterSecret, PduPart part, long messageId) + private void retrieveAttachment(MasterSecret masterSecret, + long messageId, + final AttachmentId attachmentId, + final Attachment attachment) throws IOException { - PartDatabase database = DatabaseFactory.getPartDatabase(context); - File attachmentFile = null; + AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context); + File attachmentFile = null; - final PartId partId = part.getPartId(); try { attachmentFile = createTempFile(); - TextSecureAttachmentPointer pointer = createAttachmentPointer(masterSecret, part); - InputStream attachment = messageReceiver.retrieveAttachment(pointer, attachmentFile, new ProgressListener() { - @Override public void onAttachmentProgress(long total, long progress) { - EventBus.getDefault().postSticky(new PartProgressEvent(partId, total, progress)); + TextSecureAttachmentPointer pointer = createAttachmentPointer(masterSecret, attachment); + InputStream stream = messageReceiver.retrieveAttachment(pointer, attachmentFile, new ProgressListener() { + @Override + public void onAttachmentProgress(long total, long progress) { + EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress)); } }); - database.updateDownloadedPart(masterSecret, messageId, partId, part, attachment); + database.insertAttachmentsForPlaceholder(masterSecret, messageId, attachmentId, stream); } catch (InvalidPartException | NonSuccessfulResponseCodeException | InvalidMessageException | MmsException e) { Log.w(TAG, e); - markFailed(messageId, part, partId); + markFailed(messageId, attachmentId); } finally { if (attachmentFile != null) attachmentFile.delete(); @@ -124,24 +127,25 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable } @VisibleForTesting - TextSecureAttachmentPointer createAttachmentPointer(MasterSecret masterSecret, PduPart part) + TextSecureAttachmentPointer createAttachmentPointer(MasterSecret masterSecret, Attachment attachment) throws InvalidPartException { - if (part.getContentLocation() == null || part.getContentLocation().length == 0) { + if (TextUtils.isEmpty(attachment.getLocation())) { throw new InvalidPartException("empty content id"); } - if (part.getContentDisposition() == null || part.getContentDisposition().length == 0) { + + if (TextUtils.isEmpty(attachment.getKey())) { throw new InvalidPartException("empty encrypted key"); } try { AsymmetricMasterSecret asymmetricMasterSecret = MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret); - long id = Long.parseLong(Util.toIsoString(part.getContentLocation())); - byte[] key = MediaKey.getDecrypted(masterSecret, asymmetricMasterSecret, Util.toIsoString(part.getContentDisposition())); + long id = Long.parseLong(attachment.getLocation()); + byte[] key = MediaKey.getDecrypted(masterSecret, asymmetricMasterSecret, attachment.getKey()); String relay = null; - if (part.getName() != null) { - relay = Util.toIsoString(part.getName()); + if (TextUtils.isEmpty(attachment.getRelay())) { + relay = attachment.getRelay(); } return new TextSecureAttachmentPointer(id, null, key, relay); @@ -162,10 +166,10 @@ public class AttachmentDownloadJob extends MasterSecretJob implements Injectable } } - private void markFailed(long messageId, PduPart part, PartDatabase.PartId partId) { + private void markFailed(long messageId, AttachmentId attachmentId) { try { - PartDatabase database = DatabaseFactory.getPartDatabase(context); - database.updateFailedDownloadedPart(messageId, partId, part); + AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context); + database.setTransferProgressFailed(attachmentId, messageId); } catch (MmsException e) { Log.w(TAG, e); } diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index 2abada1bd0..84499f5c9a 100644 --- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -5,17 +5,22 @@ import android.net.Uri; import android.util.Log; import android.util.Pair; +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.attachments.UriAttachment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUnion; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; import org.thoughtcrime.securesms.mms.ApnUnavailableException; import org.thoughtcrime.securesms.mms.CompatMmsConnection; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsRadioException; import org.thoughtcrime.securesms.notifications.MessageNotifier; +import org.thoughtcrime.securesms.providers.SingleUseBlobProvider; import org.thoughtcrime.securesms.service.KeyCachingService; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.requirements.NetworkRequirement; import org.whispersystems.libaxolotl.DuplicateMessageException; @@ -25,10 +30,15 @@ import org.whispersystems.libaxolotl.NoSessionException; import org.whispersystems.libaxolotl.util.guava.Optional; import java.io.IOException; +import java.util.LinkedList; +import java.util.List; import java.util.concurrent.TimeUnit; +import ws.com.google.android.mms.ContentType; import ws.com.google.android.mms.MmsException; +import ws.com.google.android.mms.pdu.EncodedStringValue; import ws.com.google.android.mms.pdu.NotificationInd; +import ws.com.google.android.mms.pdu.PduPart; import ws.com.google.android.mms.pdu.RetrieveConf; public class MmsDownloadJob extends MasterSecretJob { @@ -63,8 +73,6 @@ public class MmsDownloadJob extends MasterSecretJob { @Override public void onRun(MasterSecret masterSecret) { - Log.w(TAG, "onRun()"); - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); Optional notification = database.getNotification(messageId); @@ -140,13 +148,52 @@ public class MmsDownloadJob extends MasterSecretJob { throws MmsException, NoSessionException, DuplicateMessageException, InvalidMessageException, LegacyMessageException { - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - IncomingMediaMessage message = new IncomingMediaMessage(retrieved); + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + SingleUseBlobProvider provider = SingleUseBlobProvider.getInstance(); + String from = null; + List to = new LinkedList<>(); + List cc = new LinkedList<>(); + String body = null; + List attachments = new LinkedList<>(); + + if (retrieved.getFrom() != null) { + from = Util.toIsoString(retrieved.getFrom().getTextString()); + } + + if (retrieved.getTo() != null) { + for (EncodedStringValue toValue : retrieved.getTo()) { + to.add(Util.toIsoString(toValue.getTextString())); + } + } + + if (retrieved.getCc() != null) { + for (EncodedStringValue ccValue : retrieved.getCc()) { + cc.add(Util.toIsoString(ccValue.getTextString())); + } + } + + if (retrieved.getBody() != null) { + for (int i=0;i messageAndThreadId = database.insertMessageInbox(new MasterSecretUnion(masterSecret), message, contentLocation, threadId); database.delete(messageId); - MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second); } diff --git a/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java b/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java index c119237744..6e995bfa28 100644 --- a/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MmsSendJob.java @@ -1,23 +1,23 @@ package org.thoughtcrime.securesms.jobs; import android.content.Context; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; +import android.text.TextUtils; import android.util.Log; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; +import org.thoughtcrime.securesms.database.ThreadDatabase.DistributionTypes; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; -import org.thoughtcrime.securesms.mms.ApnUnavailableException; import org.thoughtcrime.securesms.mms.CompatMmsConnection; import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MmsSendResult; -import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection; -import org.thoughtcrime.securesms.mms.OutgoingLollipopMmsConnection; -import org.thoughtcrime.securesms.mms.OutgoingMmsConnection; +import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; +import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.notifications.MessageNotifier; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; @@ -25,20 +25,29 @@ import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.NumberUtil; import org.thoughtcrime.securesms.util.SmilUtil; import org.thoughtcrime.securesms.util.TelephonyUtil; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.requirements.NetworkRequirement; import java.io.IOException; import java.util.Arrays; +import java.util.List; +import ws.com.google.android.mms.ContentType; import ws.com.google.android.mms.MmsException; +import ws.com.google.android.mms.pdu.CharacterSets; import ws.com.google.android.mms.pdu.EncodedStringValue; +import ws.com.google.android.mms.pdu.PduBody; import ws.com.google.android.mms.pdu.PduComposer; import ws.com.google.android.mms.pdu.PduHeaders; +import ws.com.google.android.mms.pdu.PduPart; import ws.com.google.android.mms.pdu.SendConf; import ws.com.google.android.mms.pdu.SendReq; public class MmsSendJob extends SendJob { + + private static final long serialVersionUID = 0L; + private static final String TAG = MmsSendJob.class.getSimpleName(); private final long messageId; @@ -62,18 +71,20 @@ public class MmsSendJob extends SendJob { @Override public void onSend(MasterSecret masterSecret) throws MmsException, NoSuchMessageException, IOException { - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - SendReq message = database.getOutgoingMessage(masterSecret, messageId); + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + OutgoingMediaMessage message = database.getOutgoingMessage(masterSecret, messageId); try { - validateDestinations(message); + SendReq pdu = constructSendPdu(masterSecret, message); - final byte[] pduBytes = getPduBytes(masterSecret, message); + validateDestinations(message, pdu); + + final byte[] pduBytes = getPduBytes(pdu); final SendConf sendConf = new CompatMmsConnection(context).send(pduBytes); - final MmsSendResult result = getSendResult(sendConf, message); + final MmsSendResult result = getSendResult(sendConf, pdu); - database.markAsSent(messageId, result.getMessageId(), result.getResponseStatus()); - markPartsUploaded(messageId, message.getBody()); + database.markAsSent(messageId); + markAttachmentsUploaded(messageId, message.getAttachments()); } catch (UndeliverableMessageException | IOException e) { Log.w(TAG, e); database.markAsSentFailed(messageId); @@ -96,21 +107,19 @@ public class MmsSendJob extends SendJob { notifyMediaMessageDeliveryFailed(context, messageId); } - private byte[] getPduBytes(MasterSecret masterSecret, SendReq message) + private byte[] getPduBytes(SendReq message) throws IOException, UndeliverableMessageException, InsecureFallbackApprovalException { String number = TelephonyUtil.getManager(context).getLine1Number(); - message = getResolvedMessage(masterSecret, message, MediaConstraints.MMS_CONSTRAINTS, true); message.setBody(SmilUtil.getSmilBody(message.getBody())); - if (MmsDatabase.Types.isSecureType(message.getDatabaseMessageBox())) { - throw new UndeliverableMessageException("Attempt to send encrypted MMS?"); - } - if (number != null && number.trim().length() != 0) { + if (!TextUtils.isEmpty(number)) { message.setFrom(new EncodedStringValue(number)); } + byte[] pduBytes = new PduComposer(context, message).make(); + if (pduBytes == null) { throw new UndeliverableMessageException("PDU composition failed, null payload"); } @@ -149,7 +158,7 @@ public class MmsSendJob extends SendJob { } } - private void validateDestinations(SendReq message) throws UndeliverableMessageException { + private void validateDestinations(OutgoingMediaMessage media, SendReq message) throws UndeliverableMessageException { validateDestinations(message.getTo()); validateDestinations(message.getCc()); validateDestinations(message.getBcc()); @@ -157,6 +166,59 @@ public class MmsSendJob extends SendJob { if (message.getTo() == null && message.getCc() == null && message.getBcc() == null) { throw new UndeliverableMessageException("No to, cc, or bcc specified!"); } + + if (media.isSecure()) { + throw new UndeliverableMessageException("Attempt to send encrypted MMS?"); + } + } + + private SendReq constructSendPdu(MasterSecret masterSecret, OutgoingMediaMessage message) + throws UndeliverableMessageException + { + SendReq sendReq = new SendReq(); + PduBody body = new PduBody(); + + for (Recipient recipient : message.getRecipients()) { + if (message.getDistributionType() == DistributionTypes.CONVERSATION) { + sendReq.addTo(new EncodedStringValue(Util.toIsoBytes(recipient.getNumber()))); + } else { + sendReq.addBcc(new EncodedStringValue(Util.toIsoBytes(recipient.getNumber()))); + } + } + + sendReq.setDate(message.getSentTimeMillis() / 1000L); + + if (!TextUtils.isEmpty(message.getBody())) { + PduPart part = new PduPart(); + part.setData(Util.toUtf8Bytes(message.getBody())); + part.setCharset(CharacterSets.UTF_8); + part.setContentType(ContentType.TEXT_PLAIN.getBytes()); + part.setContentId((System.currentTimeMillis()+"").getBytes()); + part.setName(("Text"+System.currentTimeMillis()).getBytes()); + + body.addPart(part); + } + + List scaledAttachments = scaleAttachments(masterSecret, MediaConstraints.MMS_CONSTRAINTS, message.getAttachments()); + + for (Attachment attachment : scaledAttachments) { + try { + if (attachment.getDataUri() == null) throw new IOException("Assertion failed, attachment for outgoing MMS has no data!"); + + PduPart part = new PduPart(); + part.setData(Util.readFully(PartAuthority.getAttachmentStream(context, masterSecret, attachment.getDataUri()))); + part.setContentType(Util.toIsoBytes(attachment.getContentType())); + part.setContentId((System.currentTimeMillis() + "").getBytes()); + part.setName((System.currentTimeMillis() + "").getBytes()); + + body.addPart(part); + } catch (IOException e) { + Log.w(TAG, e); + } + } + + sendReq.setBody(body); + return sendReq; } private void notifyMediaMessageDeliveryFailed(Context context, long messageId) { diff --git a/src/org/thoughtcrime/securesms/jobs/PartProgressEvent.java b/src/org/thoughtcrime/securesms/jobs/PartProgressEvent.java index 7a22aaef6b..b8addb12a9 100644 --- a/src/org/thoughtcrime/securesms/jobs/PartProgressEvent.java +++ b/src/org/thoughtcrime/securesms/jobs/PartProgressEvent.java @@ -1,15 +1,19 @@ package org.thoughtcrime.securesms.jobs; -import org.thoughtcrime.securesms.database.PartDatabase.PartId; + +import android.support.annotation.NonNull; + +import org.thoughtcrime.securesms.attachments.Attachment; public class PartProgressEvent { - public PartId partId; - public long total; - public long progress; - public PartProgressEvent(PartId partId, long total, long progress) { - this.partId = partId; - this.total = total; - this.progress = progress; + public final Attachment attachment; + public final long total; + public final long progress; + + public PartProgressEvent(@NonNull Attachment attachment, long total, long progress) { + this.attachment = attachment; + this.total = total; + this.progress = progress; } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 27fe7b2097..4597f92546 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -6,6 +6,8 @@ import android.util.Log; import android.util.Pair; import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.attachments.DatabaseAttachment; +import org.thoughtcrime.securesms.attachments.PointerAttachment; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUnion; @@ -18,6 +20,7 @@ import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.PushDatabase; +import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.groups.GroupMessageProcessor; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; @@ -62,7 +65,6 @@ import java.util.List; import java.util.concurrent.TimeUnit; import ws.com.google.android.mms.MmsException; -import ws.com.google.android.mms.pdu.PduPart; public class PushDecryptJob extends ContextJob { @@ -254,13 +256,14 @@ public class PushDecryptJob extends ContextJob { message.getGroupInfo(), message.getAttachments()); - Pair messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1); + Pair messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1); + List attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageAndThreadId.first); - List parts = DatabaseFactory.getPartDatabase(context).getParts(messageAndThreadId.first); - for (PduPart part : parts) { + for (DatabaseAttachment attachment : attachments) { ApplicationContext.getInstance(context) .getJobManager() - .add(new AttachmentDownloadJob(context, messageAndThreadId.first, part.getPartId())); + .add(new AttachmentDownloadJob(context, messageAndThreadId.first, + attachment.getAttachmentId())); } if (smsMessageId.isPresent()) { @@ -277,22 +280,22 @@ public class PushDecryptJob extends ContextJob { { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); Recipients recipients = getSyncMessageDestination(message); - OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(context, masterSecret, recipients, - message.getMessage().getAttachments().get(), - message.getMessage().getBody().orNull()); + OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(), + PointerAttachment.forPointers(masterSecret, message.getMessage().getAttachments()), + message.getTimestamp(), ThreadDatabase.DistributionTypes.DEFAULT); mediaMessage = new OutgoingSecureMediaMessage(mediaMessage); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients); - long messageId = database.insertMessageOutbox(masterSecret, mediaMessage, threadId, false, message.getTimestamp()); + long messageId = database.insertMessageOutbox(masterSecret, mediaMessage, threadId, false); - database.markAsSent(messageId, "push".getBytes(), 0); + database.markAsSent(messageId); database.markAsPush(messageId); - for (PduPart part : DatabaseFactory.getPartDatabase(context).getParts(messageId)) { + for (DatabaseAttachment attachment : DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageId)) { ApplicationContext.getInstance(context) .getJobManager() - .add(new AttachmentDownloadJob(context, messageId, part.getPartId())); + .add(new AttachmentDownloadJob(context, messageId, attachment.getAttachmentId())); } if (smsMessageId.isPresent()) { diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index b8a6cc6e64..c9ee51e9cc 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -6,27 +6,25 @@ import android.util.Log; import org.thoughtcrime.securesms.crypto.MasterSecret; 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.MediaConstraints; -import org.thoughtcrime.securesms.mms.PartParser; +import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; +import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; 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.transport.UndeliverableMessageException; -import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.jobqueue.requirements.NetworkRequirement; import org.whispersystems.textsecure.api.TextSecureMessageSender; import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException; import org.whispersystems.textsecure.api.messages.TextSecureAttachment; -import org.whispersystems.textsecure.api.messages.TextSecureGroup; import org.whispersystems.textsecure.api.messages.TextSecureDataMessage; +import org.whispersystems.textsecure.api.messages.TextSecureGroup; import org.whispersystems.textsecure.api.push.TextSecureAddress; import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException; @@ -40,7 +38,6 @@ import java.util.List; import javax.inject.Inject; import ws.com.google.android.mms.MmsException; -import ws.com.google.android.mms.pdu.SendReq; import static org.thoughtcrime.securesms.dependencies.TextSecureCommunicationModule.TextSecureMessageSenderFactory; @@ -78,16 +75,16 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { public void onSend(MasterSecret masterSecret) throws MmsException, IOException, NoSuchMessageException { - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - SendReq message = database.getOutgoingMessage(masterSecret, messageId); + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + OutgoingMediaMessage message = database.getOutgoingMessage(masterSecret, messageId); try { deliver(masterSecret, message, filterRecipientId); database.markAsPush(messageId); database.markAsSecure(messageId); - database.markAsSent(messageId, "push".getBytes(), 0); - markPartsUploaded(messageId, message.getBody()); + database.markAsSent(messageId); + markAttachmentsUploaded(messageId, message.getAttachments()); } catch (InvalidNumberException | RecipientFormattingException | UndeliverableMessageException e) { Log.w(TAG, e); database.markAsSentFailed(messageId); @@ -101,11 +98,6 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { 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()) { Recipient recipient = RecipientFactory.getRecipientsFromString(context, uie.getE164Number(), false).getPrimaryRecipient(); database.addMismatchedIdentity(messageId, recipient.getRecipientId(), uie.getIdentityKey()); @@ -130,39 +122,31 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId); } - private void deliver(MasterSecret masterSecret, SendReq message, long filterRecipientId) + private void deliver(MasterSecret masterSecret, OutgoingMediaMessage message, long filterRecipientId) throws IOException, RecipientFormattingException, InvalidNumberException, EncapsulatedExceptions, UndeliverableMessageException { - message = getResolvedMessage(masterSecret, message, MediaConstraints.PUSH_CONSTRAINTS, false); - TextSecureMessageSender messageSender = messageSenderFactory.create(); - byte[] groupId = GroupUtil.getDecodedId(message.getTo()[0].getString()); + byte[] groupId = GroupUtil.getDecodedId(message.getRecipients().getPrimaryRecipient().getNumber()); Recipients recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false); - List attachments = getAttachments(masterSecret, message); + List attachments = getAttachmentsFor(masterSecret, message.getAttachments()); List addresses; if (filterRecipientId >= 0) addresses = getPushAddresses(filterRecipientId); else addresses = getPushAddresses(recipients); - if (MmsSmsColumns.Types.isGroupUpdate(message.getDatabaseMessageBox()) || - MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox())) - { - String content = PartParser.getMessageText(message.getBody()); + if (message.isGroup()) { + OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message; + GroupContext groupContext = groupMessage.getGroupContext(); + TextSecureAttachment avatar = attachments.isEmpty() ? null : attachments.get(0); + TextSecureGroup.Type type = groupMessage.isGroupQuit() ? TextSecureGroup.Type.QUIT : TextSecureGroup.Type.UPDATE; + TextSecureGroup group = new TextSecureGroup(type, groupId, groupContext.getName(), groupContext.getMembersList(), avatar); + TextSecureDataMessage groupDataMessage = new TextSecureDataMessage(message.getSentTimeMillis(), group, null, null); - if (content != null && !content.trim().isEmpty()) { - GroupContext groupContext = GroupContext.parseFrom(Base64.decode(content)); - TextSecureAttachment avatar = attachments.isEmpty() ? null : attachments.get(0); - TextSecureGroup.Type type = MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox()) ? TextSecureGroup.Type.QUIT : TextSecureGroup.Type.UPDATE; - TextSecureGroup group = new TextSecureGroup(type, groupId, groupContext.getName(), groupContext.getMembersList(), avatar); - TextSecureDataMessage groupMessage = new TextSecureDataMessage(message.getSentTimestamp(), group, null, null); - - messageSender.sendMessage(addresses, groupMessage); - } + messageSender.sendMessage(addresses, groupDataMessage); } else { - String body = PartParser.getMessageText(message.getBody()); TextSecureGroup group = new TextSecureGroup(groupId); - TextSecureDataMessage groupMessage = new TextSecureDataMessage(message.getSentTimestamp(), group, attachments, body); + TextSecureDataMessage groupMessage = new TextSecureDataMessage(message.getSentTimeMillis(), group, attachments, message.getBody()); messageSender.sendMessage(addresses, groupMessage); } diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index 5630a593ff..3b910f7b98 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -4,14 +4,14 @@ import android.content.Context; import android.util.Log; import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; -import org.thoughtcrime.securesms.database.PartDatabase; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.mms.MediaConstraints; -import org.thoughtcrime.securesms.mms.PartParser; +import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException; @@ -31,8 +31,6 @@ import java.util.List; import javax.inject.Inject; import ws.com.google.android.mms.MmsException; -import ws.com.google.android.mms.pdu.PduBody; -import ws.com.google.android.mms.pdu.SendReq; import static org.thoughtcrime.securesms.dependencies.TextSecureCommunicationModule.TextSecureMessageSenderFactory; @@ -63,15 +61,15 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { throws RetryLaterException, MmsException, NoSuchMessageException, UndeliverableMessageException { - MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - SendReq message = database.getOutgoingMessage(masterSecret, messageId); + MmsDatabase database = DatabaseFactory.getMmsDatabase(context); + OutgoingMediaMessage message = database.getOutgoingMessage(masterSecret, messageId); try { deliver(masterSecret, message); database.markAsPush(messageId); database.markAsSecure(messageId); - database.markAsSent(messageId, "push".getBytes(), 0); - markPartsUploaded(messageId, message.getBody()); + database.markAsSent(messageId); + markAttachmentsUploaded(messageId, message.getAttachments()); } catch (InsecureFallbackApprovalException ifae) { Log.w(TAG, ifae); database.markAsPendingInsecureSmsFallback(messageId); @@ -100,24 +98,21 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { notifyMediaMessageDeliveryFailed(context, messageId); } - private void deliver(MasterSecret masterSecret, SendReq message) + private void deliver(MasterSecret masterSecret, OutgoingMediaMessage message) throws RetryLaterException, InsecureFallbackApprovalException, UntrustedIdentityException, UndeliverableMessageException { TextSecureMessageSender messageSender = messageSenderFactory.create(); - String destination = message.getTo()[0].getString(); try { - message = getResolvedMessage(masterSecret, message, MediaConstraints.PUSH_CONSTRAINTS, false); - - TextSecureAddress address = getPushAddress(destination); - List attachments = getAttachments(masterSecret, message); - String body = PartParser.getMessageText(message.getBody()); - TextSecureDataMessage mediaMessage = TextSecureDataMessage.newBuilder() - .withBody(body) - .withAttachments(attachments) - .withTimestamp(message.getSentTimestamp()) - .build(); + TextSecureAddress address = getPushAddress(message.getRecipients().getPrimaryRecipient().getNumber()); + List scaledAttachments = scaleAttachments(masterSecret, MediaConstraints.PUSH_CONSTRAINTS, message.getAttachments()); + List attachmentStreams = getAttachmentsFor(masterSecret, scaledAttachments); + TextSecureDataMessage mediaMessage = TextSecureDataMessage.newBuilder() + .withBody(message.getBody()) + .withAttachments(attachmentStreams) + .withTimestamp(message.getSentTimeMillis()) + .build(); messageSender.sendMessage(address, mediaMessage); } catch (InvalidNumberException | UnregisteredUserException e) { diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java index 72d0d82d4b..422833349f 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobs; import android.content.Context; import android.util.Log; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.TextSecureDirectory; @@ -16,7 +17,6 @@ import org.whispersystems.jobqueue.requirements.NetworkRequirement; import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.textsecure.api.messages.TextSecureAttachment; import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener; -import org.whispersystems.textsecure.api.messages.TextSecureAttachmentStream; import org.whispersystems.textsecure.api.push.TextSecureAddress; import org.whispersystems.textsecure.api.util.InvalidNumberException; @@ -27,8 +27,6 @@ import java.util.List; import de.greenrobot.event.EventBus; import ws.com.google.android.mms.ContentType; -import ws.com.google.android.mms.pdu.PduPart; -import ws.com.google.android.mms.pdu.SendReq; public abstract class PushSendJob extends SendJob { @@ -55,25 +53,25 @@ public abstract class PushSendJob extends SendJob { return new TextSecureAddress(e164number, Optional.fromNullable(relay)); } - protected List getAttachments(final MasterSecret masterSecret, final SendReq message) { + protected List getAttachmentsFor(MasterSecret masterSecret, List parts) { List attachments = new LinkedList<>(); - for (int i=0;i attachments) { + AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context); + + for (Attachment attachment : attachments) { + database.markAttachmentUploaded(messageId, attachment); + } + } + + protected List scaleAttachments(@NonNull MasterSecret masterSecret, + @NonNull MediaConstraints constraints, + @NonNull List attachments) + throws UndeliverableMessageException { - PduBody body = new PduBody(); - try { - for (int i = 0; i < message.getBody().getPartsNum(); i++) { - body.addPart(getResolvedPart(masterSecret, constraints, message.getBody().getPart(i), toMemory)); + AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); + List results = new LinkedList<>(); + + for (Attachment attachment : attachments) { + try { + if (constraints.isSatisfied(context, masterSecret, attachment)) { + results.add(attachment); + } else if (constraints.canResize(attachment)) { + InputStream resized = constraints.getResizedMedia(context, masterSecret, attachment); + results.add(attachmentDatabase.updateAttachmentData(masterSecret, attachment, resized)); + } else { + throw new UndeliverableMessageException("Size constraints could not be met!"); + } + } catch (IOException | MmsException e) { + throw new UndeliverableMessageException(e); } - } catch (MmsException me) { - throw new UndeliverableMessageException(me); - } - return new SendReq(message.getPduHeaders(), - body, - message.getDatabaseMessageId(), - message.getDatabaseMessageBox(), - message.getSentTimestamp()); - } - - private PduPart getResolvedPart(MasterSecret masterSecret, MediaConstraints constraints, - PduPart part, boolean toMemory) - throws IOException, MmsException, UndeliverableMessageException - { - byte[] resizedData = null; - - if (!constraints.isSatisfied(context, masterSecret, part)) { - if (!constraints.canResize(part)) { - throw new UndeliverableMessageException("Size constraints could not be satisfied."); - } - resizedData = getResizedPartData(masterSecret, constraints, part); } - if (toMemory && part.getDataUri() != null) { - part.setData(resizedData != null ? resizedData : MediaUtil.getPartData(context, masterSecret, part)); - } - - if (resizedData != null) { - part.setDataSize(resizedData.length); - } - return part; - } - - protected void markPartsUploaded(long messageId, PduBody body) { - if (body == null) return; - PartDatabase database = DatabaseFactory.getPartDatabase(context); - for (int i = 0; i < body.getPartsNum(); i++) { - database.markPartUploaded(messageId, body.getPart(i)); - } - } - - private byte[] getResizedPartData(MasterSecret masterSecret, MediaConstraints constraints, - PduPart part) - throws IOException, MmsException - { - Log.w(TAG, "resizing part " + part.getPartId()); - - final long oldSize = part.getDataSize(); - final byte[] data = constraints.getResizedMedia(context, masterSecret, part); - - DatabaseFactory.getPartDatabase(context).updatePartData(masterSecret, part, new ByteArrayInputStream(data)); - Log.w(TAG, String.format("Resized part %.1fkb => %.1fkb", oldSize / 1024.0, part.getDataSize() / 1024.0)); - - return data; + return results; } } diff --git a/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirement.java b/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirement.java index 61469f22b4..7e361599b2 100644 --- a/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirement.java +++ b/src/org/thoughtcrime/securesms/jobs/requirements/MediaNetworkRequirement.java @@ -6,9 +6,10 @@ import android.net.NetworkInfo; import android.support.annotation.NonNull; import android.util.Log; +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.PartDatabase; -import org.thoughtcrime.securesms.database.PartDatabase.PartId; +import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -18,8 +19,6 @@ import org.whispersystems.jobqueue.requirements.Requirement; import java.util.Collections; import java.util.Set; -import ws.com.google.android.mms.pdu.PduPart; - public class MediaNetworkRequirement implements Requirement, ContextDependent { private static final long serialVersionUID = 0L; private static final String TAG = MediaNetworkRequirement.class.getSimpleName(); @@ -30,14 +29,15 @@ public class MediaNetworkRequirement implements Requirement, ContextDependent { private final long partRowId; private final long partUniqueId; - public MediaNetworkRequirement(Context context, long messageId, PartId partId) { + public MediaNetworkRequirement(Context context, long messageId, AttachmentId attachmentId) { this.context = context; this.messageId = messageId; - this.partRowId = partId.getRowId(); - this.partUniqueId = partId.getUniqueId(); + this.partRowId = attachmentId.getRowId(); + this.partUniqueId = attachmentId.getUniqueId(); } - @Override public void setContext(Context context) { + @Override + public void setContext(Context context) { this.context = context; } @@ -74,23 +74,26 @@ public class MediaNetworkRequirement implements Requirement, ContextDependent { @Override public boolean isPresent() { - final PartId partId = new PartId(partRowId, partUniqueId); - final PartDatabase db = DatabaseFactory.getPartDatabase(context); - final PduPart part = db.getPart(partId); - if (part == null) { - Log.w(TAG, "part was null, returning vacuous true"); + final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId); + final AttachmentDatabase db = DatabaseFactory.getAttachmentDatabase(context); + final Attachment attachment = db.getAttachment(attachmentId); + + if (attachment == null) { + Log.w(TAG, "attachment was null, returning vacuous true"); return true; } - Log.w(TAG, "part transfer progress is " + part.getTransferProgress()); - switch (part.getTransferProgress()) { - case PartDatabase.TRANSFER_PROGRESS_STARTED: + Log.w(TAG, "part transfer progress is " + attachment.getTransferState()); + switch (attachment.getTransferState()) { + case AttachmentDatabase.TRANSFER_PROGRESS_STARTED: return true; - case PartDatabase.TRANSFER_PROGRESS_AUTO_PENDING: + case AttachmentDatabase.TRANSFER_PROGRESS_AUTO_PENDING: final Set allowedTypes = getAllowedAutoDownloadTypes(); - final boolean isAllowed = allowedTypes.contains(MediaUtil.getDiscreteMimeType(part)); + final boolean isAllowed = allowedTypes.contains(MediaUtil.getDiscreteMimeType(attachment.getContentType())); - if (isAllowed) db.setTransferState(messageId, partId, PartDatabase.TRANSFER_PROGRESS_STARTED); + /// XXX WTF -- This is *hella* gross. A requirement shouldn't have the side effect of + // *modifying the database* just by calling isPresent(). + if (isAllowed) db.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_STARTED); return isAllowed; default: return false; diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index c88eccd5e0..6b6b9cb2a1 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -135,7 +135,7 @@ public class AttachmentManager { } else { slideDeck.addSlide(slide); attachmentView.setVisibility(View.VISIBLE); - thumbnail.setImageResource(slide, masterSecret); + thumbnail.setImageResource(masterSecret, slide, false, true); attachmentListener.onAttachmentChanged(); } } @@ -213,9 +213,9 @@ public class AttachmentManager { final @Nullable Slide slide, final @NonNull MediaConstraints constraints) { - return slide == null || - constraints.isSatisfied(context, masterSecret, slide.getPart()) || - constraints.canResize(slide.getPart()); + return slide == null || + constraints.isSatisfied(context, masterSecret, slide.asAttachment()) || + constraints.canResize(slide.asAttachment()); } private class RemoveButtonListener implements View.OnClickListener { diff --git a/src/org/thoughtcrime/securesms/mms/AudioSlide.java b/src/org/thoughtcrime/securesms/mms/AudioSlide.java index ef4850a7dd..cf225353d0 100644 --- a/src/org/thoughtcrime/securesms/mms/AudioSlide.java +++ b/src/org/thoughtcrime/securesms/mms/AudioSlide.java @@ -21,8 +21,10 @@ import android.content.res.Resources.Theme; import android.net.Uri; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.util.ResUtil; import java.io.IOException; @@ -33,11 +35,22 @@ import ws.com.google.android.mms.pdu.PduPart; public class AudioSlide extends Slide { public AudioSlide(Context context, Uri uri, long dataSize) throws IOException { - super(context, constructPartFromUri(context, uri, ContentType.AUDIO_UNSPECIFIED, dataSize)); + super(context, constructAttachmentFromUri(context, uri, ContentType.AUDIO_UNSPECIFIED, dataSize)); } - public AudioSlide(Context context, PduPart part) { - super(context, part); + public AudioSlide(Context context, Attachment attachment) { + super(context, attachment); + } + + @Override + @Nullable + public Uri getThumbnailUri() { + return null; + } + + @Override + public boolean hasPlaceholder() { + return true; } @Override @@ -50,7 +63,9 @@ public class AudioSlide extends Slide { return true; } - @NonNull @Override public String getContentDescription() { + @NonNull + @Override + public String getContentDescription() { return context.getString(R.string.Slide_audio); } diff --git a/src/org/thoughtcrime/securesms/mms/CompatMmsConnection.java b/src/org/thoughtcrime/securesms/mms/CompatMmsConnection.java index 4bd3aae014..c93415f751 100644 --- a/src/org/thoughtcrime/securesms/mms/CompatMmsConnection.java +++ b/src/org/thoughtcrime/securesms/mms/CompatMmsConnection.java @@ -1,8 +1,6 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.support.annotation.NonNull; @@ -12,11 +10,6 @@ import android.util.Log; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import java.io.IOException; -import java.net.Inet6Address; -import java.net.InterfaceAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.util.Enumeration; import ws.com.google.android.mms.MmsException; import ws.com.google.android.mms.pdu.RetrieveConf; @@ -31,7 +24,9 @@ public class CompatMmsConnection implements OutgoingMmsConnection, IncomingMmsCo this.context = context; } - @Nullable @Override public SendConf send(@NonNull byte[] pduBytes) + @Nullable + @Override + public SendConf send(@NonNull byte[] pduBytes) throws UndeliverableMessageException { try { @@ -47,8 +42,10 @@ public class CompatMmsConnection implements OutgoingMmsConnection, IncomingMmsCo } } - @Nullable @Override public RetrieveConf retrieve(@NonNull String contentLocation, - byte[] transactionId) + @Nullable + @Override + public RetrieveConf retrieve(@NonNull String contentLocation, + byte[] transactionId) throws MmsException, MmsRadioException, ApnUnavailableException, IOException { try { diff --git a/src/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java b/src/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java index e258fdf9a6..f5296c93f4 100644 --- a/src/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java +++ b/src/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java @@ -14,20 +14,22 @@ import java.io.IOException; import java.io.InputStream; public class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher { + private static final String TAG = DecryptableStreamLocalUriFetcher.class.getSimpleName(); - private Context context; + + private Context context; private MasterSecret masterSecret; public DecryptableStreamLocalUriFetcher(Context context, MasterSecret masterSecret, Uri uri) { super(context, uri); - this.context = context; + this.context = context; this.masterSecret = masterSecret; } @Override protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException { try { - return PartAuthority.getPartStream(context, masterSecret, uri); + return PartAuthority.getAttachmentStream(context, masterSecret, uri); } catch (IOException ioe) { Log.w(TAG, ioe); throw new FileNotFoundException("PartAuthority couldn't load Uri resource."); diff --git a/src/org/thoughtcrime/securesms/mms/GifSlide.java b/src/org/thoughtcrime/securesms/mms/GifSlide.java index 9fcbb65e86..44fc838f7f 100644 --- a/src/org/thoughtcrime/securesms/mms/GifSlide.java +++ b/src/org/thoughtcrime/securesms/mms/GifSlide.java @@ -2,21 +2,29 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; import android.net.Uri; +import android.support.annotation.Nullable; + +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.crypto.MasterSecret; import java.io.IOException; +import java.io.InputStream; import ws.com.google.android.mms.pdu.PduPart; public class GifSlide extends ImageSlide { - public GifSlide(Context context, PduPart part) { - super(context, part); + + public GifSlide(Context context, Attachment attachment) { + super(context, attachment); } public GifSlide(Context context, Uri uri, long dataSize) throws IOException { super(context, uri, dataSize); } - @Override public Uri getThumbnailUri() { - return getPart().getDataUri(); + @Override + @Nullable + public Uri getThumbnailUri() { + return getUri(); } } diff --git a/src/org/thoughtcrime/securesms/mms/ImageSlide.java b/src/org/thoughtcrime/securesms/mms/ImageSlide.java index 875943dc3a..e7d8eb46a1 100644 --- a/src/org/thoughtcrime/securesms/mms/ImageSlide.java +++ b/src/org/thoughtcrime/securesms/mms/ImageSlide.java @@ -23,32 +23,22 @@ import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.attachments.Attachment; import java.io.IOException; import ws.com.google.android.mms.ContentType; -import ws.com.google.android.mms.pdu.PduPart; public class ImageSlide extends Slide { + private static final String TAG = ImageSlide.class.getSimpleName(); - public ImageSlide(Context context, PduPart part) { - super(context, part); + public ImageSlide(@NonNull Context context, @NonNull Attachment attachment) { + super(context, attachment); } public ImageSlide(Context context, Uri uri, long size) throws IOException { - super(context, constructPartFromUri(context, uri, ContentType.IMAGE_JPEG, size)); - } - - @Override - public Uri getThumbnailUri() { - if (getPart().getDataUri() != null) { - return isDraft() - ? getPart().getDataUri() - : PartAuthority.getThumbnailUri(getPart().getPartId()); - } - - return null; + super(context, constructAttachmentFromUri(context, uri, ContentType.IMAGE_JPEG, size)); } @Override diff --git a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java index f5862e506e..b9b384d109 100644 --- a/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java @@ -1,38 +1,42 @@ package org.thoughtcrime.securesms.mms; -import android.text.TextUtils; -import android.util.Log; - +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.attachments.PointerAttachment; import org.thoughtcrime.securesms.crypto.MasterSecretUnion; -import org.thoughtcrime.securesms.crypto.MediaKey; -import org.thoughtcrime.securesms.database.PartDatabase; +import org.thoughtcrime.securesms.database.MmsAddresses; import org.thoughtcrime.securesms.util.GroupUtil; -import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.textsecure.api.messages.TextSecureAttachment; import org.whispersystems.textsecure.api.messages.TextSecureGroup; +import java.util.LinkedList; import java.util.List; -import ws.com.google.android.mms.pdu.CharacterSets; -import ws.com.google.android.mms.pdu.EncodedStringValue; -import ws.com.google.android.mms.pdu.PduBody; -import ws.com.google.android.mms.pdu.PduHeaders; -import ws.com.google.android.mms.pdu.PduPart; -import ws.com.google.android.mms.pdu.RetrieveConf; - public class IncomingMediaMessage { - private final PduHeaders headers; - private final PduBody body; - private final String groupId; - private final boolean push; + private final String from; + private final String body; + private final String groupId; + private final boolean push; + private final long sentTimeMillis; - public IncomingMediaMessage(RetrieveConf retrieved) { - this.headers = retrieved.getPduHeaders(); - this.body = retrieved.getBody(); - this.groupId = null; - this.push = false; + private final List to = new LinkedList<>(); + private final List cc = new LinkedList<>(); + private final List attachments = new LinkedList<>(); + + public IncomingMediaMessage(String from, List to, List cc, + String body, long sentTimeMillis, + List attachments) + { + this.from = from; + this.sentTimeMillis = sentTimeMillis; + this.body = body; + this.groupId = null; + this.push = false; + + this.to.addAll(to); + this.cc.addAll(cc); + this.attachments.addAll(attachments); } public IncomingMediaMessage(MasterSecretUnion masterSecret, @@ -44,59 +48,30 @@ public class IncomingMediaMessage { Optional group, Optional> attachments) { - this.headers = new PduHeaders(); - this.body = new PduBody(); - this.push = true; + this.push = true; + this.from = from; + this.sentTimeMillis = sentTimeMillis; + this.body = body.orNull(); - if (group.isPresent()) { - this.groupId = GroupUtil.getEncodedId(group.get().getGroupId()); - } else { - this.groupId = null; - } + if (group.isPresent()) this.groupId = GroupUtil.getEncodedId(group.get().getGroupId()); + else this.groupId = null; - this.headers.setEncodedStringValue(new EncodedStringValue(from), PduHeaders.FROM); - this.headers.appendEncodedStringValue(new EncodedStringValue(to), PduHeaders.TO); - this.headers.setLongInteger(sentTimeMillis / 1000, PduHeaders.DATE); - - - if (body.isPresent() && !TextUtils.isEmpty(body.get())) { - PduPart text = new PduPart(); - text.setData(Util.toUtf8Bytes(body.get())); - text.setContentType(Util.toIsoBytes("text/plain")); - text.setCharset(CharacterSets.UTF_8); - this.body.addPart(text); - } - - if (attachments.isPresent()) { - for (TextSecureAttachment attachment : attachments.get()) { - if (attachment.isPointer()) { - PduPart media = new PduPart(); - String encryptedKey = MediaKey.getEncrypted(masterSecret, attachment.asPointer().getKey()); - - media.setContentType(Util.toIsoBytes(attachment.getContentType())); - media.setContentLocation(Util.toIsoBytes(String.valueOf(attachment.asPointer().getId()))); - media.setContentDisposition(Util.toIsoBytes(encryptedKey)); - - if (relay.isPresent()) { - media.setName(Util.toIsoBytes(relay.get())); - } - - media.setTransferProgress(PartDatabase.TRANSFER_PROGRESS_AUTO_PENDING); - - this.body.addPart(media); - } - } - } + this.to.add(to); + this.attachments.addAll(PointerAttachment.forPointers(masterSecret, attachments)); } - public PduHeaders getPduHeaders() { - return headers; - } - - public PduBody getBody() { + public String getBody() { return body; } + public MmsAddresses getAddresses() { + return new MmsAddresses(from, to, cc, new LinkedList()); + } + + public List getAttachments() { + return attachments; + } + public String getGroupId() { return groupId; } @@ -105,10 +80,11 @@ public class IncomingMediaMessage { return push; } + public long getSentTimeMillis() { + return sentTimeMillis; + } + public boolean isGroupMessage() { - return groupId != null || - !Util.isEmpty(headers.getEncodedStringValues(PduHeaders.CC)) || - (headers.getEncodedStringValues(PduHeaders.TO) != null && - headers.getEncodedStringValues(PduHeaders.TO).length > 1); + return groupId != null || to.size() > 1 || cc.size() > 0; } } diff --git a/src/org/thoughtcrime/securesms/mms/LegacyMmsConnection.java b/src/org/thoughtcrime/securesms/mms/LegacyMmsConnection.java index f7ae4a2611..43c9fd192f 100644 --- a/src/org/thoughtcrime/securesms/mms/LegacyMmsConnection.java +++ b/src/org/thoughtcrime/securesms/mms/LegacyMmsConnection.java @@ -199,7 +199,6 @@ public abstract class LegacyMmsConnection { protected List
getBaseHeaders() { final String number = TelephonyUtil.getManager(context).getLine1Number(); ; - final Optional mdnHeader = getVerizonMdnHeader(number); return new LinkedList
() {{ add(new BasicHeader("Accept", "*/*, application/vnd.wap.mms-message, application/vnd.wap.sic")); @@ -209,28 +208,10 @@ public abstract class LegacyMmsConnection { if (!TextUtils.isEmpty(number)) { add(new BasicHeader("x-up-calling-line-id", number)); add(new BasicHeader("X-MDN", number)); - if (mdnHeader.isPresent()) add(mdnHeader.get()); } }}; } - private Optional getVerizonMdnHeader(@Nullable String number) { - if (TextUtils.isEmpty(number)) return Optional.absent(); - - try { - PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); - PhoneNumber phoneNumber = phoneNumberUtil.parse(number, null); - String mdnNumber = phoneNumberUtil.getNationalSignificantNumber(phoneNumber); - - return Optional.of(new BasicHeader("x-vzw-mdn", mdnNumber)); - } catch (NumberParseException e) { - Log.w(TAG, e); - return Optional.absent(); - } - - - } - public static class Apn { public static Apn EMPTY = new Apn("", "", "", "", ""); diff --git a/src/org/thoughtcrime/securesms/mms/MediaConstraints.java b/src/org/thoughtcrime/securesms/mms/MediaConstraints.java index c59ea2b2ee..f71cd6c02c 100644 --- a/src/org/thoughtcrime/securesms/mms/MediaConstraints.java +++ b/src/org/thoughtcrime/securesms/mms/MediaConstraints.java @@ -1,25 +1,23 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; -import android.graphics.Bitmap.CompressFormat; import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; -import com.bumptech.glide.Glide; - +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; -import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.MediaUtil; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.ExecutionException; -import ws.com.google.android.mms.pdu.PduPart; - public abstract class MediaConstraints { private static final String TAG = MediaConstraints.class.getSimpleName(); @@ -36,13 +34,13 @@ public abstract class MediaConstraints { public abstract int getAudioMaxSize(); - public boolean isSatisfied(Context context, MasterSecret masterSecret, PduPart part) { + public boolean isSatisfied(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Attachment attachment) { try { - return (MediaUtil.isGif(part) && part.getDataSize() <= getGifMaxSize() && isWithinBounds(context, masterSecret, part.getDataUri())) || - (MediaUtil.isImage(part) && part.getDataSize() <= getImageMaxSize() && isWithinBounds(context, masterSecret, part.getDataUri())) || - (MediaUtil.isAudio(part) && part.getDataSize() <= getAudioMaxSize()) || - (MediaUtil.isVideo(part) && part.getDataSize() <= getVideoMaxSize()) || - (!MediaUtil.isImage(part) && !MediaUtil.isAudio(part) && !MediaUtil.isVideo(part)); + return (MediaUtil.isGif(attachment) && attachment.getSize() <= getGifMaxSize() && isWithinBounds(context, masterSecret, attachment.getDataUri())) || + (MediaUtil.isImage(attachment) && attachment.getSize() <= getImageMaxSize() && isWithinBounds(context, masterSecret, attachment.getDataUri())) || + (MediaUtil.isAudio(attachment) && attachment.getSize() <= getAudioMaxSize()) || + (MediaUtil.isVideo(attachment) && attachment.getSize() <= getVideoMaxSize()) || + (!MediaUtil.isImage(attachment) && !MediaUtil.isAudio(attachment) && !MediaUtil.isVideo(attachment)); } catch (IOException ioe) { Log.w(TAG, "Failed to determine if media's constraints are satisfied.", ioe); return false; @@ -50,24 +48,28 @@ public abstract class MediaConstraints { } public boolean isWithinBounds(Context context, MasterSecret masterSecret, Uri uri) throws IOException { - InputStream is = PartAuthority.getPartStream(context, masterSecret, uri); + InputStream is = PartAuthority.getAttachmentStream(context, masterSecret, uri); Pair dimensions = BitmapUtil.getDimensions(is); return dimensions.first > 0 && dimensions.first <= getImageMaxWidth(context) && dimensions.second > 0 && dimensions.second <= getImageMaxHeight(context); } - public boolean canResize(PduPart part) { - return part != null && MediaUtil.isImage(part) && !MediaUtil.isGif(part); + public boolean canResize(@Nullable Attachment attachment) { + return attachment != null && MediaUtil.isImage(attachment) && !MediaUtil.isGif(attachment); } - public byte[] getResizedMedia(Context context, MasterSecret masterSecret, PduPart part) + public InputStream getResizedMedia(@NonNull Context context, + @NonNull MasterSecret masterSecret, + @NonNull Attachment attachment) throws IOException { - if (!canResize(part) || part.getDataUri() == null) { + if (!canResize(attachment)) { throw new UnsupportedOperationException("Cannot resize this content type"); } + try { - return BitmapUtil.createScaledBytes(context, new DecryptableUri(masterSecret, part.getDataUri()), this); + // XXX - This is loading everything into memory! We want the send path to be stream-like. + return new ByteArrayInputStream(BitmapUtil.createScaledBytes(context, new DecryptableUri(masterSecret, attachment.getDataUri()), this)); } catch (ExecutionException ee) { throw new IOException(ee); } diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java index 37b50c3474..b221ad7446 100644 --- a/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java @@ -1,36 +1,44 @@ package org.thoughtcrime.securesms.mms; -import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import org.thoughtcrime.redphone.util.Base64; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.recipients.Recipients; -import org.thoughtcrime.securesms.util.Base64; import org.whispersystems.textsecure.internal.push.TextSecureProtos.GroupContext; -import ws.com.google.android.mms.ContentType; -import ws.com.google.android.mms.pdu.PduBody; -import ws.com.google.android.mms.pdu.PduPart; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { private final GroupContext group; - public OutgoingGroupMediaMessage(Context context, Recipients recipients, - GroupContext group, byte[] avatar) + public OutgoingGroupMediaMessage(@NonNull Recipients recipients, + @NonNull String encodedGroupContext, + @NonNull List avatar, + long sentTimeMillis) + throws IOException { - super(context, recipients, new PduBody(), Base64.encodeBytes(group.toByteArray()), + super(recipients, encodedGroupContext, avatar, sentTimeMillis, + ThreadDatabase.DistributionTypes.CONVERSATION); + + this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext)); + } + + public OutgoingGroupMediaMessage(@NonNull Recipients recipients, + @NonNull GroupContext group, + @Nullable final Attachment avatar) + { + super(recipients, Base64.encodeBytes(group.toByteArray()), + new LinkedList() {{if (avatar != null) add(avatar);}}, + System.currentTimeMillis(), ThreadDatabase.DistributionTypes.CONVERSATION); this.group = group; - - if (avatar != null) { - PduPart part = new PduPart(); - part.setData(avatar); - part.setContentType(ContentType.IMAGE_PNG.getBytes()); - part.setContentId((System.currentTimeMillis()+"").getBytes()); - part.setName(("Image" + System.currentTimeMillis()).getBytes()); - body.addPart(part); - } } @Override @@ -45,4 +53,8 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage { public boolean isGroupQuit() { return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE; } + + public GroupContext getGroupContext() { + return group; + } } diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java index 374f9ab678..c10850e662 100644 --- a/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java @@ -1,66 +1,54 @@ package org.thoughtcrime.securesms.mms; -import android.content.Context; -import android.text.TextUtils; - -import org.thoughtcrime.securesms.crypto.MasterSecretUnion; -import org.thoughtcrime.securesms.crypto.MediaKey; -import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.recipients.Recipients; -import org.thoughtcrime.securesms.util.Util; -import org.whispersystems.textsecure.api.messages.TextSecureAttachment; import java.util.List; -import ws.com.google.android.mms.pdu.PduBody; -import ws.com.google.android.mms.pdu.PduPart; - public class OutgoingMediaMessage { - private final Recipients recipients; - protected final PduBody body; - private final int distributionType; + private final Recipients recipients; + protected final String body; + protected final List attachments; + private final long sentTimeMillis; + private final int distributionType; - public OutgoingMediaMessage(Context context, Recipients recipients, PduBody body, - String message, int distributionType) + public OutgoingMediaMessage(Recipients recipients, String message, + List attachments, long sentTimeMillis, + int distributionType) { this.recipients = recipients; - this.body = body; + this.body = message; + this.sentTimeMillis = sentTimeMillis; this.distributionType = distributionType; - - if (!TextUtils.isEmpty(message)) { - this.body.addPart(new TextSlide(context, message).getPart()); - } + this.attachments = attachments; } - public OutgoingMediaMessage(Context context, Recipients recipients, SlideDeck slideDeck, - String message, int distributionType) + public OutgoingMediaMessage(Recipients recipients, SlideDeck slideDeck, String message, long sentTimeMillis, int distributionType) { - this(context, recipients, slideDeck.toPduBody(), message, distributionType); - } - - public OutgoingMediaMessage(Context context, MasterSecretUnion masterSecret, - Recipients recipients, List attachments, - String message) - { - this(context, recipients, pduBodyFor(masterSecret, attachments), message, - ThreadDatabase.DistributionTypes.CONVERSATION); + this(recipients, message, slideDeck.asAttachments(), sentTimeMillis, distributionType); } public OutgoingMediaMessage(OutgoingMediaMessage that) { this.recipients = that.getRecipients(); this.body = that.body; this.distributionType = that.distributionType; + this.attachments = that.attachments; + this.sentTimeMillis = that.sentTimeMillis; } public Recipients getRecipients() { return recipients; } - public PduBody getPduBody() { + public String getBody() { return body; } + public List getAttachments() { + return attachments; + } + public int getDistributionType() { return distributionType; } @@ -73,23 +61,8 @@ public class OutgoingMediaMessage { return false; } - private static PduBody pduBodyFor(MasterSecretUnion masterSecret, List attachments) { - PduBody body = new PduBody(); - - for (TextSecureAttachment attachment : attachments) { - if (attachment.isPointer()) { - PduPart media = new PduPart(); - String encryptedKey = MediaKey.getEncrypted(masterSecret, attachment.asPointer().getKey()); - - media.setContentType(Util.toIsoBytes(attachment.getContentType())); - media.setContentLocation(Util.toIsoBytes(String.valueOf(attachment.asPointer().getId()))); - media.setContentDisposition(Util.toIsoBytes(encryptedKey)); - - body.addPart(media); - } - } - - return body; + public long getSentTimeMillis() { + return sentTimeMillis; } } diff --git a/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java b/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java index cfa01db42b..084579db62 100644 --- a/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java +++ b/src/org/thoughtcrime/securesms/mms/OutgoingSecureMediaMessage.java @@ -2,16 +2,21 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.recipients.Recipients; +import java.util.List; + import ws.com.google.android.mms.pdu.PduBody; public class OutgoingSecureMediaMessage extends OutgoingMediaMessage { - public OutgoingSecureMediaMessage(Context context, Recipients recipients, PduBody body, - String message, int distributionType) + public OutgoingSecureMediaMessage(Recipients recipients, String body, + List attachments, + long sentTimeMillis, + int distributionType) { - super(context, recipients, body, message, distributionType); + super(recipients, body, attachments, sentTimeMillis, distributionType); } public OutgoingSecureMediaMessage(OutgoingMediaMessage base) { diff --git a/src/org/thoughtcrime/securesms/mms/PartAuthority.java b/src/org/thoughtcrime/securesms/mms/PartAuthority.java index 1a6029f592..55b8b9888b 100644 --- a/src/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/src/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -4,12 +4,14 @@ import android.content.ContentUris; import android.content.Context; import android.content.UriMatcher; import android.net.Uri; +import android.support.annotation.NonNull; +import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.PartDatabase; import org.thoughtcrime.securesms.providers.CaptureProvider; import org.thoughtcrime.securesms.providers.PartProvider; +import org.thoughtcrime.securesms.providers.SingleUseBlobProvider; import java.io.IOException; import java.io.InputStream; @@ -21,9 +23,10 @@ public class PartAuthority { private static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING); private static final Uri THUMB_CONTENT_URI = Uri.parse(THUMB_URI_STRING); - private static final int PART_ROW = 1; - private static final int THUMB_ROW = 2; - private static final int CAPTURE_ROW = 3; + private static final int PART_ROW = 1; + private static final int THUMB_ROW = 2; + private static final int CAPTURE_ROW = 3; + private static final int SINGLE_USE_ROW = 4; private static final UriMatcher uriMatcher; @@ -32,9 +35,10 @@ public class PartAuthority { uriMatcher.addURI("org.thoughtcrime.securesms", "part/*/#", PART_ROW); uriMatcher.addURI("org.thoughtcrime.securesms", "thumb/*/#", THUMB_ROW); uriMatcher.addURI(CaptureProvider.AUTHORITY, CaptureProvider.EXPECTED_PATH, CAPTURE_ROW); + uriMatcher.addURI(SingleUseBlobProvider.AUTHORITY, SingleUseBlobProvider.PATH, SINGLE_USE_ROW); } - public static InputStream getPartStream(Context context, MasterSecret masterSecret, Uri uri) + public static InputStream getAttachmentStream(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull Uri uri) throws IOException { int match = uriMatcher.match(uri); @@ -42,12 +46,14 @@ public class PartAuthority { switch (match) { case PART_ROW: PartUriParser partUri = new PartUriParser(uri); - return DatabaseFactory.getPartDatabase(context).getPartStream(masterSecret, partUri.getPartId()); + return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(masterSecret, partUri.getPartId()); case THUMB_ROW: partUri = new PartUriParser(uri); - return DatabaseFactory.getPartDatabase(context).getThumbnailStream(masterSecret, partUri.getPartId()); + return DatabaseFactory.getAttachmentDatabase(context).getThumbnailStream(masterSecret, partUri.getPartId()); case CAPTURE_ROW: return CaptureProvider.getInstance(context).getStream(masterSecret, ContentUris.parseId(uri)); + case SINGLE_USE_ROW: + return SingleUseBlobProvider.getInstance().getStream(ContentUris.parseId(uri)); default: return context.getContentResolver().openInputStream(uri); } @@ -56,18 +62,18 @@ public class PartAuthority { } } - public static Uri getPublicPartUri(Uri uri) { + public static Uri getAttachmentPublicUri(Uri uri) { PartUriParser partUri = new PartUriParser(uri); return PartProvider.getContentUri(partUri.getPartId()); } - public static Uri getPartUri(PartDatabase.PartId partId) { - Uri uri = Uri.withAppendedPath(PART_CONTENT_URI, String.valueOf(partId.getUniqueId())); - return ContentUris.withAppendedId(uri, partId.getRowId()); + public static Uri getAttachmentDataUri(AttachmentId attachmentId) { + Uri uri = Uri.withAppendedPath(PART_CONTENT_URI, String.valueOf(attachmentId.getUniqueId())); + return ContentUris.withAppendedId(uri, attachmentId.getRowId()); } - public static Uri getThumbnailUri(PartDatabase.PartId partId) { - Uri uri = Uri.withAppendedPath(THUMB_CONTENT_URI, String.valueOf(partId.getUniqueId())); - return ContentUris.withAppendedId(uri, partId.getRowId()); + public static Uri getAttachmentThumbnailUri(AttachmentId attachmentId) { + Uri uri = Uri.withAppendedPath(THUMB_CONTENT_URI, String.valueOf(attachmentId.getUniqueId())); + return ContentUris.withAppendedId(uri, attachmentId.getRowId()); } } diff --git a/src/org/thoughtcrime/securesms/mms/PartUriParser.java b/src/org/thoughtcrime/securesms/mms/PartUriParser.java index c830ef421f..ca22393265 100644 --- a/src/org/thoughtcrime/securesms/mms/PartUriParser.java +++ b/src/org/thoughtcrime/securesms/mms/PartUriParser.java @@ -3,10 +3,7 @@ package org.thoughtcrime.securesms.mms; import android.content.ContentUris; import android.net.Uri; -import org.thoughtcrime.securesms.database.PartDatabase; -import org.thoughtcrime.securesms.util.Hex; - -import java.io.IOException; +import org.thoughtcrime.securesms.attachments.AttachmentId; public class PartUriParser { @@ -16,8 +13,8 @@ public class PartUriParser { this.uri = uri; } - public PartDatabase.PartId getPartId() { - return new PartDatabase.PartId(getId(), getUniqueId()); + public AttachmentId getPartId() { + return new AttachmentId(getId(), getUniqueId()); } private long getId() { diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java index 2dd09a136f..f68e8b4b3d 100644 --- a/src/org/thoughtcrime/securesms/mms/Slide.java +++ b/src/org/thoughtcrime/securesms/mms/Slide.java @@ -23,32 +23,38 @@ import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.attachments.UriAttachment; +import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.util.MediaUtil; -import org.thoughtcrime.securesms.database.PartDatabase; import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.libaxolotl.util.guava.Optional; import java.io.IOException; -import java.io.InputStream; - -import ws.com.google.android.mms.pdu.PduPart; public abstract class Slide { - protected final PduPart part; - protected final Context context; + protected final Attachment attachment; + protected final Context context; + + public Slide(@NonNull Context context, @NonNull Attachment attachment) { + this.context = context; + this.attachment = attachment; - public Slide(Context context, @NonNull PduPart part) { - this.part = part; - this.context = context; } public String getContentType() { - return new String(part.getContentType()); + return attachment.getContentType(); } + @Nullable public Uri getUri() { - return part.getDataUri(); + return attachment.getDataUri(); + } + + @Nullable + public Uri getThumbnailUri() { + return attachment.getThumbnailUri(); } public boolean hasImage() { @@ -65,53 +71,39 @@ public abstract class Slide { public @NonNull String getContentDescription() { return ""; } - public PduPart getPart() { - return part; - } - - public Uri getThumbnailUri() { - return null; + public Attachment asAttachment() { + return attachment; } public boolean isInProgress() { - return part.isInProgress(); + return attachment.isInProgress(); } public boolean isPendingDownload() { - return getTransferProgress() == PartDatabase.TRANSFER_PROGRESS_FAILED || - getTransferProgress() == PartDatabase.TRANSFER_PROGRESS_AUTO_PENDING; + return getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_FAILED || + getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_AUTO_PENDING; } - public long getTransferProgress() { - return part.getTransferProgress(); + public long getTransferState() { + return attachment.getTransferState(); } public @DrawableRes int getPlaceholderRes(Theme theme) { throw new AssertionError("getPlaceholderRes() called for non-drawable slide"); } - public boolean isDraft() { - return !getPart().getPartId().isValid(); + public boolean hasPlaceholder() { + return false; } - - protected static PduPart constructPartFromUri(@NonNull Context context, - @NonNull Uri uri, - @NonNull String defaultMime, - long dataSize) - throws IOException + protected static Attachment constructAttachmentFromUri(@NonNull Context context, + @NonNull Uri uri, + @NonNull String defaultMime, + long size) + throws IOException { - final PduPart part = new PduPart(); - final String mimeType = MediaUtil.getMimeType(context, uri); - final String derivedMimeType = mimeType != null ? mimeType : defaultMime; - - part.setDataSize(dataSize); - part.setDataUri(uri); - part.setContentType(derivedMimeType.getBytes()); - part.setContentId((System.currentTimeMillis()+"").getBytes()); - part.setName((MediaUtil.getDiscreteMimeType(derivedMimeType) + System.currentTimeMillis()).getBytes()); - - return part; + Optional resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)); + return new UriAttachment(uri, resolvedType.or(defaultMime), AttachmentDatabase.TRANSFER_PROGRESS_STARTED, size); } @Override @@ -124,8 +116,7 @@ public abstract class Slide { this.hasAudio() == that.hasAudio() && this.hasImage() == that.hasImage() && this.hasVideo() == that.hasVideo() && - this.isDraft() == that.isDraft() && - this.getTransferProgress() == that.getTransferProgress() && + this.getTransferState() == that.getTransferState() && Util.equals(this.getUri(), that.getUri()) && Util.equals(this.getThumbnailUri(), that.getThumbnailUri()); } @@ -133,6 +124,6 @@ public abstract class Slide { @Override public int hashCode() { return Util.hashCode(getContentType(), hasAudio(), hasImage(), - hasVideo(), isDraft(), getUri(), getThumbnailUri(), getTransferProgress()); + hasVideo(), getUri(), getThumbnailUri(), getTransferState()); } } diff --git a/src/org/thoughtcrime/securesms/mms/SlideDeck.java b/src/org/thoughtcrime/securesms/mms/SlideDeck.java index 66afee40be..2c0a03624e 100644 --- a/src/org/thoughtcrime/securesms/mms/SlideDeck.java +++ b/src/org/thoughtcrime/securesms/mms/SlideDeck.java @@ -22,6 +22,7 @@ import android.support.annotation.Nullable; import android.util.Pair; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.dom.smil.parser.SmilXmlSerializer; import org.thoughtcrime.securesms.util.ListenableFutureTask; @@ -43,14 +44,9 @@ public class SlideDeck { private final List slides = new LinkedList<>(); - public SlideDeck(SlideDeck copy) { - this.slides.addAll(copy.getSlides()); - } - - public SlideDeck(Context context, PduBody body) { - for (int i=0;i attachments) { + for (Attachment attachment : attachments) { + Slide slide = MediaUtil.getSlideForAttachment(context, attachment); if (slide != null) slides.add(slide); } } @@ -62,15 +58,14 @@ public class SlideDeck { slides.clear(); } - public PduBody toPduBody() { - PduBody body = new PduBody(); + public List asAttachments() { + List attachments = new LinkedList<>(); for (Slide slide : slides) { - PduPart part = slide.getPart(); - body.addPart(part); + attachments.add(slide.asAttachment()); } - return body; + return attachments; } public void addSlide(Slide slide) { diff --git a/src/org/thoughtcrime/securesms/mms/TextSlide.java b/src/org/thoughtcrime/securesms/mms/TextSlide.java deleted file mode 100644 index 085ed8b2f0..0000000000 --- a/src/org/thoughtcrime/securesms/mms/TextSlide.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (C) 2011 Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (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 . - */ -package org.thoughtcrime.securesms.mms; - -import android.content.Context; -import android.util.Log; - -import java.io.UnsupportedEncodingException; - -import ws.com.google.android.mms.ContentType; -import ws.com.google.android.mms.pdu.CharacterSets; -import ws.com.google.android.mms.pdu.PduPart; - -public class TextSlide extends Slide { - - public TextSlide(Context context, String message) { - super(context, getPartForMessage(message)); - } - - private static PduPart getPartForMessage(String message) { - PduPart part = new PduPart(); - - try { - part.setData(message.getBytes(CharacterSets.MIMENAME_UTF_8)); - - if (part.getData().length == 0) - throw new AssertionError("Part data should not be zero!"); - - } catch (UnsupportedEncodingException e) { - Log.w("TextSlide", "ISO_8859_1 must be supported!", e); - part.setData("Unsupported character set!".getBytes()); - } - - part.setCharset(CharacterSets.UTF_8); - part.setContentType(ContentType.TEXT_PLAIN.getBytes()); - part.setContentId((System.currentTimeMillis()+"").getBytes()); - part.setName(("Text"+System.currentTimeMillis()).getBytes()); - - return part; - } -} diff --git a/src/org/thoughtcrime/securesms/mms/VideoSlide.java b/src/org/thoughtcrime/securesms/mms/VideoSlide.java index d243d0c307..63be2dcc79 100644 --- a/src/org/thoughtcrime/securesms/mms/VideoSlide.java +++ b/src/org/thoughtcrime/securesms/mms/VideoSlide.java @@ -21,8 +21,10 @@ import android.content.res.Resources.Theme; import android.net.Uri; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.util.ResUtil; import java.io.IOException; @@ -33,11 +35,22 @@ import ws.com.google.android.mms.pdu.PduPart; public class VideoSlide extends Slide { public VideoSlide(Context context, Uri uri, long dataSize) throws IOException { - super(context, constructPartFromUri(context, uri, ContentType.VIDEO_UNSPECIFIED, dataSize)); + super(context, constructAttachmentFromUri(context, uri, ContentType.VIDEO_UNSPECIFIED, dataSize)); } - public VideoSlide(Context context, PduPart part) { - super(context, part); + public VideoSlide(Context context, Attachment attachment) { + super(context, attachment); + } + + @Override + @Nullable + public Uri getThumbnailUri() { + return null; + } + + @Override + public boolean hasPlaceholder() { + return true; } @Override diff --git a/src/org/thoughtcrime/securesms/notifications/WearReplyReceiver.java b/src/org/thoughtcrime/securesms/notifications/WearReplyReceiver.java index 302f547a8f..5ca13526d4 100644 --- a/src/org/thoughtcrime/securesms/notifications/WearReplyReceiver.java +++ b/src/org/thoughtcrime/securesms/notifications/WearReplyReceiver.java @@ -24,6 +24,7 @@ import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.RemoteInput; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; @@ -32,6 +33,8 @@ import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; +import java.util.LinkedList; + import ws.com.google.android.mms.pdu.PduBody; /** @@ -64,7 +67,7 @@ public class WearReplyReceiver extends MasterSecretBroadcastReceiver { long threadId; if (recipients.isGroupRecipient()) { - OutgoingMediaMessage reply = new OutgoingMediaMessage(context, recipients, new PduBody(), responseText.toString(), 0); + OutgoingMediaMessage reply = new OutgoingMediaMessage(recipients, responseText.toString(), new LinkedList(), System.currentTimeMillis(), 0); threadId = MessageSender.send(context, masterSecret, reply, -1, false); } else { OutgoingTextMessage reply = new OutgoingTextMessage(recipients, responseText.toString()); diff --git a/src/org/thoughtcrime/securesms/providers/CaptureProvider.java b/src/org/thoughtcrime/securesms/providers/CaptureProvider.java index 685914ae31..1211de6685 100644 --- a/src/org/thoughtcrime/securesms/providers/CaptureProvider.java +++ b/src/org/thoughtcrime/securesms/providers/CaptureProvider.java @@ -98,7 +98,7 @@ public class CaptureProvider { } } - public InputStream getStream(MasterSecret masterSecret, long id) throws IOException { + public @NonNull InputStream getStream(MasterSecret masterSecret, long id) throws IOException { final byte[] cached = cache.get((int)id); return cached != null ? new ByteArrayInputStream(cached) : new DecryptingPartInputStream(getFile(id), masterSecret); diff --git a/src/org/thoughtcrime/securesms/providers/PartProvider.java b/src/org/thoughtcrime/securesms/providers/PartProvider.java index 2cddc37043..6e134ada43 100644 --- a/src/org/thoughtcrime/securesms/providers/PartProvider.java +++ b/src/org/thoughtcrime/securesms/providers/PartProvider.java @@ -23,11 +23,12 @@ import android.content.UriMatcher; import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; +import android.support.annotation.NonNull; import android.util.Log; +import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.PartDatabase; import org.thoughtcrime.securesms.mms.PartUriParser; import org.thoughtcrime.securesms.service.KeyCachingService; @@ -57,13 +58,14 @@ public class PartProvider extends ContentProvider { return true; } - public static Uri getContentUri(PartDatabase.PartId partId) { - Uri uri = Uri.withAppendedPath(CONTENT_URI, String.valueOf(partId.getUniqueId())); - return ContentUris.withAppendedId(uri, partId.getRowId()); + public static Uri getContentUri(AttachmentId attachmentId) { + Uri uri = Uri.withAppendedPath(CONTENT_URI, String.valueOf(attachmentId.getUniqueId())); + return ContentUris.withAppendedId(uri, attachmentId.getRowId()); } - private File copyPartToTemporaryFile(MasterSecret masterSecret, PartDatabase.PartId partId) throws IOException { - InputStream in = DatabaseFactory.getPartDatabase(getContext()).getPartStream(masterSecret, partId); + @SuppressWarnings("ConstantConditions") + private File copyPartToTemporaryFile(MasterSecret masterSecret, AttachmentId attachmentId) throws IOException { + InputStream in = DatabaseFactory.getAttachmentDatabase(getContext()).getAttachmentStream(masterSecret, attachmentId); File tmpDir = getContext().getDir("tmp", 0); File tmpFile = File.createTempFile("test", ".jpg", tmpDir); FileOutputStream fout = new FileOutputStream(tmpFile); @@ -80,7 +82,7 @@ public class PartProvider extends ContentProvider { } @Override - public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { MasterSecret masterSecret = KeyCachingService.getMasterSecret(getContext()); Log.w(TAG, "openFile() called!"); @@ -112,27 +114,27 @@ public class PartProvider extends ContentProvider { } @Override - public int delete(Uri arg0, String arg1, String[] arg2) { + public int delete(@NonNull Uri arg0, String arg1, String[] arg2) { return 0; } @Override - public String getType(Uri arg0) { + public String getType(@NonNull Uri arg0) { return null; } @Override - public Uri insert(Uri arg0, ContentValues arg1) { + public Uri insert(@NonNull Uri arg0, ContentValues arg1) { return null; } @Override - public Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) { + public Cursor query(@NonNull Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) { return null; } @Override - public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) { + public int update(@NonNull Uri arg0, ContentValues arg1, String arg2, String[] arg3) { return 0; } } diff --git a/src/org/thoughtcrime/securesms/providers/SingleUseBlobProvider.java b/src/org/thoughtcrime/securesms/providers/SingleUseBlobProvider.java new file mode 100644 index 0000000000..9f2368c272 --- /dev/null +++ b/src/org/thoughtcrime/securesms/providers/SingleUseBlobProvider.java @@ -0,0 +1,67 @@ +package org.thoughtcrime.securesms.providers; + +import android.content.ContentUris; +import android.content.Context; +import android.content.UriMatcher; +import android.net.Uri; +import android.os.AsyncTask; +import android.support.annotation.NonNull; +import android.util.Log; + +import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; +import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.Util; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class SingleUseBlobProvider { + + private static final String TAG = CaptureProvider.class.getSimpleName(); + + public static final String AUTHORITY = "org.thoughtcrime.securesms"; + public static final String PATH = "memory/*/#"; + private static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/memory"); + + private final Map cache = new HashMap<>(); + + private static final SingleUseBlobProvider instance = new SingleUseBlobProvider(); + + public static SingleUseBlobProvider getInstance() { + return instance; + } + + private SingleUseBlobProvider() {} + + public synchronized Uri createUri(@NonNull byte[] blob) { + try { + long id = Math.abs(SecureRandom.getInstance("SHA1PRNG").nextLong()); + cache.put(id, blob); + + Uri uniqueUri = Uri.withAppendedPath(CONTENT_URI, String.valueOf(System.currentTimeMillis())); + return ContentUris.withAppendedId(uniqueUri, id); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + public synchronized @NonNull InputStream getStream(long id) throws IOException { + byte[] cached = cache.get(id); + cache.remove(id); + + if (cached != null) return new ByteArrayInputStream(cached); + else throw new IOException("ID not found: " + id); + + } + +} diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java b/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java index dffb9bc22e..19c8255a11 100644 --- a/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java +++ b/src/org/thoughtcrime/securesms/recipients/RecipientFactory.java @@ -81,6 +81,20 @@ public class RecipientFactory { return getRecipientsForIds(context, ids, asynchronous); } + public static Recipients getRecipientsFromStrings(@NonNull Context context, @NonNull List numbers, boolean asynchronous) { + List ids = new LinkedList<>(); + + for (String number : numbers) { + Optional id = getRecipientIdFromNumber(context, number); + + if (id.isPresent()) { + ids.add(String.valueOf(id.get())); + } + } + + return getRecipientsForIds(context, ids, asynchronous); + } + private static Recipients getRecipientsForIds(Context context, List idStrings, boolean asynchronous) { long[] ids = new long[idStrings.size()]; int i = 0; diff --git a/src/org/thoughtcrime/securesms/recipients/Recipients.java b/src/org/thoughtcrime/securesms/recipients/Recipients.java index 4408cecb7f..0f1e51b39b 100644 --- a/src/org/thoughtcrime/securesms/recipients/Recipients.java +++ b/src/org/thoughtcrime/securesms/recipients/Recipients.java @@ -257,7 +257,7 @@ public class Recipients implements Iterable, RecipientModifiedListene return Util.join(recipientArray, " "); } - public String[] toNumberStringArray(boolean scrub) { + public @NonNull String[] toNumberStringArray(boolean scrub) { String[] recipientsArray = new String[recipients.size()]; Iterator iterator = recipients.iterator(); int i = 0; @@ -278,6 +278,13 @@ public class Recipients implements Iterable, RecipientModifiedListene return recipientsArray; } + public @NonNull List toNumberStringList(boolean scrub) { + List results = new LinkedList<>(); + Collections.addAll(results, toNumberStringArray(scrub)); + + return results; + } + public String toShortString() { String fromString = ""; diff --git a/src/org/thoughtcrime/securesms/service/QuickResponseService.java b/src/org/thoughtcrime/securesms/service/QuickResponseService.java index d6d32b49f0..77e544ccac 100644 --- a/src/org/thoughtcrime/securesms/service/QuickResponseService.java +++ b/src/org/thoughtcrime/securesms/service/QuickResponseService.java @@ -55,7 +55,7 @@ public class QuickResponseService extends MasterSecretIntentService { if (recipients.isSingleRecipient()) { MessageSender.send(this, masterSecret, new OutgoingTextMessage(recipients, content), -1, false); } else { - MessageSender.send(this, masterSecret, new OutgoingMediaMessage(this, recipients, new SlideDeck(), content, + MessageSender.send(this, masterSecret, new OutgoingMediaMessage(recipients, new SlideDeck(), content, System.currentTimeMillis(), ThreadDatabase.DistributionTypes.DEFAULT), -1, false); } } diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index 0c3879c235..8e94d583c1 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -100,7 +100,7 @@ public class MessageSender { } Recipients recipients = message.getRecipients(); - long messageId = database.insertMessageOutbox(new MasterSecretUnion(masterSecret), message, allocatedThreadId, forceSms, System.currentTimeMillis()); + long messageId = database.insertMessageOutbox(new MasterSecretUnion(masterSecret), message, allocatedThreadId, forceSms); sendMediaMessage(context, masterSecret, recipients, forceSms, messageId); @@ -177,7 +177,7 @@ public class MessageSender { throws MmsException { MmsDatabase database = DatabaseFactory.getMmsDatabase(context); - database.markAsSent(messageId, "self-send".getBytes(), 0); + database.markAsSent(messageId); database.markAsPush(messageId); long newMessageId = database.copyMessageInbox(masterSecret, messageId); diff --git a/src/org/thoughtcrime/securesms/util/GroupUtil.java b/src/org/thoughtcrime/securesms/util/GroupUtil.java index cc27c7d4c3..440267cebc 100644 --- a/src/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/src/org/thoughtcrime/securesms/util/GroupUtil.java @@ -30,7 +30,7 @@ public class GroupUtil { return Hex.fromStringCondensed(groupId.split("!", 2)[1]); } - public static boolean isEncodedGroup(String groupId) { + public static boolean isEncodedGroup(@NonNull String groupId) { return groupId.startsWith(ENCODED_GROUP_PREFIX); } diff --git a/src/org/thoughtcrime/securesms/util/ListenableFutureTask.java b/src/org/thoughtcrime/securesms/util/ListenableFutureTask.java index 3fe7ba13ba..04e43e8344 100644 --- a/src/org/thoughtcrime/securesms/util/ListenableFutureTask.java +++ b/src/org/thoughtcrime/securesms/util/ListenableFutureTask.java @@ -16,6 +16,8 @@ */ package org.thoughtcrime.securesms.util; +import android.support.annotation.Nullable; + import java.util.LinkedList; import java.util.List; import java.util.concurrent.Callable; @@ -26,17 +28,30 @@ public class ListenableFutureTask extends FutureTask { private final List> listeners = new LinkedList<>(); + @Nullable + private final Object identifier; + public ListenableFutureTask(Callable callable) { + this(callable, null); + } + + public ListenableFutureTask(Callable callable, @Nullable Object identifier) { super(callable); + this.identifier = identifier; } public ListenableFutureTask(final V result) { + this(result, null); + } + + public ListenableFutureTask(final V result, @Nullable Object identifier) { super(new Callable() { @Override public V call() throws Exception { return result; } }); + this.identifier = identifier; this.run(); } @@ -74,4 +89,19 @@ public class ListenableFutureTask extends FutureTask { } } } + + @Override + public boolean equals(Object other) { + if (other != null && other instanceof ListenableFutureTask && this.identifier != null) { + return identifier.equals(other); + } else { + return super.equals(other); + } + } + + @Override + public int hashCode() { + if (identifier != null) return identifier.hashCode(); + else return super.hashCode(); + } } diff --git a/src/org/thoughtcrime/securesms/util/MediaUtil.java b/src/org/thoughtcrime/securesms/util/MediaUtil.java index 9af08d3b81..ddd72247af 100644 --- a/src/org/thoughtcrime/securesms/util/MediaUtil.java +++ b/src/org/thoughtcrime/securesms/util/MediaUtil.java @@ -9,9 +9,8 @@ import android.text.TextUtils; import android.util.Log; import android.webkit.MimeTypeMap; -import com.bumptech.glide.Glide; - import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; @@ -21,24 +20,24 @@ import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.VideoSlide; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.ExecutionException; import ws.com.google.android.mms.ContentType; -import ws.com.google.android.mms.pdu.PduPart; public class MediaUtil { private static final String TAG = MediaUtil.class.getSimpleName(); - public static ThumbnailData generateThumbnail(Context context, MasterSecret masterSecret, Uri uri, String type) + public static @Nullable ThumbnailData generateThumbnail(Context context, MasterSecret masterSecret, String contentType, Uri uri) throws ExecutionException { long startMillis = System.currentTimeMillis(); - ThumbnailData data; - if (ContentType.isImageType(type)) data = new ThumbnailData(generateImageThumbnail(context, masterSecret, uri)); - else data = null; + ThumbnailData data = null; + + if (ContentType.isImageType(contentType)) { + data = new ThumbnailData(generateImageThumbnail(context, masterSecret, uri)); + } if (data != null) { Log.w(TAG, String.format("generated thumbnail for part, %dx%d (%.3f:1) in %dms", @@ -49,16 +48,6 @@ public class MediaUtil { return data; } - public static byte[] getPartData(Context context, MasterSecret masterSecret, PduPart part) - throws IOException - { - ByteArrayOutputStream os = part.getDataSize() > 0 && part.getDataSize() < Integer.MAX_VALUE - ? new ByteArrayOutputStream((int) part.getDataSize()) - : new ByteArrayOutputStream(); - Util.copy(PartAuthority.getPartStream(context, masterSecret, part.getDataUri()), os); - return os.toByteArray(); - } - private static Bitmap generateImageThumbnail(Context context, MasterSecret masterSecret, Uri uri) throws ExecutionException { @@ -66,22 +55,22 @@ public class MediaUtil { return BitmapUtil.createScaledBitmap(context, new DecryptableUri(masterSecret, uri), maxSize, maxSize); } - public static Slide getSlideForPart(Context context, PduPart part, String contentType) { + public static Slide getSlideForAttachment(Context context, Attachment attachment) { Slide slide = null; - if (isGif(contentType)) { - slide = new GifSlide(context, part); - } else if (ContentType.isImageType(contentType)) { - slide = new ImageSlide(context, part); - } else if (ContentType.isVideoType(contentType)) { - slide = new VideoSlide(context, part); - } else if (ContentType.isAudioType(contentType)) { - slide = new AudioSlide(context, part); + if (isGif(attachment.getContentType())) { + slide = new GifSlide(context, attachment); + } else if (ContentType.isImageType(attachment.getContentType())) { + slide = new ImageSlide(context, attachment); + } else if (ContentType.isVideoType(attachment.getContentType())) { + slide = new VideoSlide(context, attachment); + } else if (ContentType.isAudioType(attachment.getContentType())) { + slide = new AudioSlide(context, attachment); } return slide; } - public static String getMimeType(Context context, Uri uri) { + public static @Nullable String getMimeType(Context context, Uri uri) { String type = context.getContentResolver().getType(uri); if (type == null) { final String extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()); @@ -91,7 +80,7 @@ public class MediaUtil { } public static long getMediaSize(Context context, MasterSecret masterSecret, Uri uri) throws IOException { - InputStream in = PartAuthority.getPartStream(context, masterSecret, uri); + InputStream in = PartAuthority.getAttachmentStream(context, masterSecret, uri); if (in == null) throw new IOException("Couldn't obtain input stream."); long size = 0; @@ -110,24 +99,20 @@ public class MediaUtil { return !TextUtils.isEmpty(contentType) && contentType.trim().equals("image/gif"); } - public static boolean isGif(PduPart part) { - return isGif(Util.toIsoString(part.getContentType())); + public static boolean isGif(Attachment attachment) { + return isGif(attachment.getContentType()); } - public static boolean isImage(PduPart part) { - return ContentType.isImageType(Util.toIsoString(part.getContentType())); + public static boolean isImage(Attachment attachment) { + return ContentType.isImageType(attachment.getContentType()); } - public static boolean isAudio(PduPart part) { - return ContentType.isAudioType(Util.toIsoString(part.getContentType())); + public static boolean isAudio(Attachment attachment) { + return ContentType.isAudioType(attachment.getContentType()); } - public static boolean isVideo(PduPart part) { - return ContentType.isVideoType(Util.toIsoString(part.getContentType())); - } - - public static @Nullable String getDiscreteMimeType(@NonNull PduPart part) { - return getDiscreteMimeType(Util.toIsoString(part.getContentType())); + public static boolean isVideo(Attachment attachment) { + return ContentType.isVideoType(attachment.getContentType()); } public static @Nullable String getDiscreteMimeType(@NonNull String mimeType) { diff --git a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java index 0d710aaaf1..3ff6980883 100644 --- a/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java +++ b/src/org/thoughtcrime/securesms/util/SaveAttachmentTask.java @@ -35,8 +35,8 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask(context); - this.masterSecretReference = new WeakReference(masterSecret); + this.contextReference = new WeakReference<>(context); + this.masterSecretReference = new WeakReference<>(masterSecret); } @Override @@ -59,7 +59,7 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask(); } - public boolean containsPushInProgress() { - for (int i=0;i(); - setUniqueId(System.currentTimeMillis()); - } - - public void setEncrypted(boolean isEncrypted) { - this.isEncrypted = isEncrypted; - } - - public boolean getEncrypted() { - return isEncrypted; - } - - public void setDataSize(long dataSize) { - this.dataSize = dataSize; - } - - public long getDataSize() { - return this.dataSize; - } - - public boolean isInProgress() { - return transferProgress != PartDatabase.TRANSFER_PROGRESS_DONE && - transferProgress != PartDatabase.TRANSFER_PROGRESS_FAILED; - } - - public void setTransferProgress(int transferProgress) { - this.transferProgress = transferProgress; - } - - public int getTransferProgress() { - return transferProgress; } /** @@ -447,46 +398,5 @@ public class PduPart { return new String(location); } } - - public PartDatabase.PartId getPartId() { - return new PartDatabase.PartId(rowId, uniqueId); - } - - public void setPartId(PartDatabase.PartId partId) { - this.rowId = partId.getRowId(); - this.uniqueId = partId.getUniqueId(); - } - - public long getRowId() { - return rowId; - } - - public void setRowId(long rowId) { - this.rowId = rowId; - } - - public Bitmap getThumbnail() { - return thumbnail; - } - - public void setThumbnail(Bitmap thumbnail) { - this.thumbnail = thumbnail; - } - - public long getUniqueId() { - return uniqueId; - } - - public void setUniqueId(long uniqueId) { - this.uniqueId = uniqueId; - } - - public long getMmsId() { - return mmsId; - } - - public void setMmsId(long mmsId) { - this.mmsId = mmsId; - } } diff --git a/src/ws/com/google/android/mms/pdu/SendReq.java b/src/ws/com/google/android/mms/pdu/SendReq.java index 60a5d47aa8..ab0a70bf21 100644 --- a/src/ws/com/google/android/mms/pdu/SendReq.java +++ b/src/ws/com/google/android/mms/pdu/SendReq.java @@ -23,9 +23,6 @@ import ws.com.google.android.mms.InvalidHeaderValueException; public class SendReq extends MultimediaMessagePdu { private static final String TAG = "SendReq"; - private long databaseMessageId; - private long messageBox; - private long timestamp; public SendReq() { super(); @@ -91,26 +88,6 @@ public class SendReq extends MultimediaMessagePdu { super(headers, body); } - public SendReq(PduHeaders headers, PduBody body, long messageId, long messageBox, long timestamp) - { - super(headers, body); - this.databaseMessageId = messageId; - this.messageBox = messageBox; - this.timestamp = timestamp; - } - - public long getDatabaseMessageBox() { - return this.messageBox; - } - - public long getDatabaseMessageId() { - return databaseMessageId; - } - - public long getSentTimestamp() { - return timestamp; - } - /** * Get Bcc value. * diff --git a/test/androidTest/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTest.java b/test/androidTest/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTest.java new file mode 100644 index 0000000000..94e8763c51 --- /dev/null +++ b/test/androidTest/java/org/thoughtcrime/securesms/database/AttachmentDatabaseTest.java @@ -0,0 +1,69 @@ +package org.thoughtcrime.securesms.database; + +import android.net.Uri; + +import org.thoughtcrime.securesms.TextSecureTestCase; +import org.thoughtcrime.securesms.attachments.AttachmentId; +import org.thoughtcrime.securesms.attachments.DatabaseAttachment; +import org.thoughtcrime.securesms.crypto.MasterSecret; + +import java.io.FileNotFoundException; +import java.io.InputStream; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyFloat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AttachmentDatabaseTest extends TextSecureTestCase { + private static final long ROW_ID = 1L; + private static final long UNIQUE_ID = 2L; + + private AttachmentDatabase database; + + @Override + public void setUp() { + database = spy(DatabaseFactory.getAttachmentDatabase(getInstrumentation().getTargetContext())); + } + + public void testTaskNotRunWhenThumbnailExists() throws Exception { + final AttachmentId attachmentId = new AttachmentId(ROW_ID, UNIQUE_ID); + + when(database.getAttachment(attachmentId)).thenReturn(getMockAttachment("x/x")); + + doReturn(mock(InputStream.class)).when(database).getDataStream(any(MasterSecret.class), any(AttachmentId.class), eq("thumbnail")); + database.getThumbnailStream(mock(MasterSecret.class), attachmentId); + + // XXX - I don't think this is testing anything? The thumbnail would be updated asynchronously. + verify(database, never()).updateAttachmentThumbnail(any(MasterSecret.class), any(AttachmentId.class), any(InputStream.class), anyFloat()); + } + + public void testTaskRunWhenThumbnailMissing() throws Exception { + final AttachmentId attachmentId = new AttachmentId(ROW_ID, UNIQUE_ID); + + when(database.getAttachment(attachmentId)).thenReturn(getMockAttachment("image/png")); + doReturn(null).when(database).getDataStream(any(MasterSecret.class), any(AttachmentId.class), eq("thumbnail")); + doNothing().when(database).updateAttachmentThumbnail(any(MasterSecret.class), any(AttachmentId.class), any(InputStream.class), anyFloat()); + + try { + database.new ThumbnailFetchCallable(mock(MasterSecret.class), attachmentId).call(); + throw new AssertionError("didn't try to generate thumbnail"); + } catch (FileNotFoundException fnfe) { + // success + } + } + + private DatabaseAttachment getMockAttachment(String contentType) { + DatabaseAttachment attachment = mock(DatabaseAttachment.class); + when(attachment.getContentType()).thenReturn(contentType); + when(attachment.getDataUri()).thenReturn(Uri.EMPTY); + + return attachment; + } +} diff --git a/test/androidTest/java/org/thoughtcrime/securesms/database/PartDatabaseTest.java b/test/androidTest/java/org/thoughtcrime/securesms/database/PartDatabaseTest.java deleted file mode 100644 index a33e80de3d..0000000000 --- a/test/androidTest/java/org/thoughtcrime/securesms/database/PartDatabaseTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.thoughtcrime.securesms.database; - -import android.net.Uri; - -import org.thoughtcrime.securesms.TextSecureTestCase; -import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.database.PartDatabase.PartId; - -import java.io.FileNotFoundException; -import java.io.InputStream; - -import ws.com.google.android.mms.pdu.PduPart; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyFloat; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class PartDatabaseTest extends TextSecureTestCase { - private static final long ROW_ID = 1L; - private static final long UNIQUE_ID = 2L; - - private PartDatabase database; - - @Override - public void setUp() { - database = spy(DatabaseFactory.getPartDatabase(getInstrumentation().getTargetContext())); - } - - public void testTaskNotRunWhenThumbnailExists() throws Exception { - final PartId partId = new PartId(ROW_ID, UNIQUE_ID); - - when(database.getPart(partId)).thenReturn(getPduPartSkeleton("x/x")); - doReturn(mock(InputStream.class)).when(database).getDataStream(any(MasterSecret.class), any(PartId.class), eq("thumbnail")); - database.getThumbnailStream(null, partId); - - verify(database, never()).updatePartThumbnail(any(MasterSecret.class), any(PartId.class), any(PduPart.class), any(InputStream.class), anyFloat()); - } - - public void testTaskRunWhenThumbnailMissing() throws Exception { - final PartId partId = new PartId(ROW_ID, UNIQUE_ID); - - when(database.getPart(partId)).thenReturn(getPduPartSkeleton("image/png")); - doReturn(null).when(database).getDataStream(any(MasterSecret.class), any(PartId.class), eq("thumbnail")); - doNothing().when(database).updatePartThumbnail(any(MasterSecret.class), any(PartId.class), any(PduPart.class), any(InputStream.class), anyFloat()); - - try { - database.new ThumbnailFetchCallable(mock(MasterSecret.class), partId).call(); - throw new AssertionError("didn't try to generate thumbnail"); - } catch (FileNotFoundException fnfe) { - // success - } - } - - private PduPart getPduPartSkeleton(String contentType) { - PduPart part = new PduPart(); - part.setContentType(contentType.getBytes()); - part.setDataUri(Uri.EMPTY); - return part; - } -} diff --git a/test/unitTest/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJobTest.java b/test/unitTest/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJobTest.java index 5f040d78d4..1c07b81848 100644 --- a/test/unitTest/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJobTest.java +++ b/test/unitTest/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJobTest.java @@ -5,7 +5,7 @@ import android.content.Context; import org.junit.Before; import org.junit.Test; import org.thoughtcrime.securesms.BaseUnitTest; -import org.thoughtcrime.securesms.database.PartDatabase.PartId; +import org.thoughtcrime.securesms.database.AttachmentDatabase.AttachmentId; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob.InvalidPartException; import org.thoughtcrime.securesms.util.Util; @@ -20,7 +20,7 @@ public class AttachmentDownloadJobTest extends BaseUnitTest { @Override public void setUp() throws Exception { super.setUp(); - job = new AttachmentDownloadJob(mock(Context.class), 1L, new PartId(1L, 1L)); + job = new AttachmentDownloadJob(mock(Context.class), 1L, new AttachmentId(1L, 1L)); } @Test(expected = InvalidPartException.class)