From e7f1c52eb24c87e315f7bbcfc91f4b794d8ab8fb Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Thu, 25 Jun 2015 16:04:16 -0700 Subject: [PATCH] Add progress listener for attachments. // FREEBIE --- .../api/TextSecureMessageReceiver.java | 6 ++- .../api/TextSecureMessageSender.java | 1 + .../api/messages/TextSecureAttachment.java | 18 ++++++-- .../messages/TextSecureAttachmentStream.java | 12 ++++-- .../DeviceContactsInputStream.java | 2 +- .../multidevice/DeviceGroupsInputStream.java | 2 +- .../internal/push/PushAttachmentData.java | 20 ++++++--- .../internal/push/PushServiceSocket.java | 42 ++++++++++++++----- .../textsecure/internal/util/Util.java | 16 ------- 9 files changed, 76 insertions(+), 43 deletions(-) diff --git a/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageReceiver.java b/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageReceiver.java index c1b239abfb..6684eb2991 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageReceiver.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageReceiver.java @@ -18,6 +18,8 @@ package org.whispersystems.textsecure.api; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.textsecure.api.crypto.AttachmentCipherInputStream; +import org.whispersystems.textsecure.api.messages.TextSecureAttachment; +import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener; import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer; import org.whispersystems.textsecure.api.messages.TextSecureDataMessage; import org.whispersystems.textsecure.api.messages.TextSecureEnvelope; @@ -88,10 +90,10 @@ public class TextSecureMessageReceiver { * @throws IOException * @throws InvalidMessageException */ - public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File destination) + public InputStream retrieveAttachment(TextSecureAttachmentPointer pointer, File destination, ProgressListener listener) throws IOException, InvalidMessageException { - socket.retrieveAttachment(pointer.getRelay().orNull(), pointer.getId(), destination); + socket.retrieveAttachment(pointer.getRelay().orNull(), pointer.getId(), destination, listener); return new AttachmentCipherInputStream(destination, pointer.getKey()); } diff --git a/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageSender.java b/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageSender.java index 00f0cd9353..959d7570fc 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageSender.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/TextSecureMessageSender.java @@ -335,6 +335,7 @@ public class TextSecureMessageSender { PushAttachmentData attachmentData = new PushAttachmentData(attachment.getContentType(), attachment.getInputStream(), attachment.getLength(), + attachment.getListener(), attachmentKey); long attachmentId = socket.sendAttachment(attachmentData); diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachment.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachment.java index 03b6437a6e..cb1cab82aa 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachment.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachment.java @@ -47,9 +47,10 @@ public abstract class TextSecureAttachment { public static class Builder { - private InputStream inputStream; - private String contentType; - private long length; + private InputStream inputStream; + private String contentType; + private long length; + private ProgressListener listener; private Builder() {} @@ -68,12 +69,21 @@ public abstract class TextSecureAttachment { return this; } + public Builder withListener(ProgressListener listener) { + this.listener = listener; + return this; + } + public TextSecureAttachmentStream build() { if (inputStream == null) throw new IllegalArgumentException("Must specify stream!"); if (contentType == null) throw new IllegalArgumentException("No content type specified!"); if (length == 0) throw new IllegalArgumentException("No length specified!"); - return new TextSecureAttachmentStream(inputStream, contentType, length); + return new TextSecureAttachmentStream(inputStream, contentType, length, listener); } } + + public interface ProgressListener { + public void onAttachmentProgress(long total, long progress); + } } diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachmentStream.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachmentStream.java index 2f57a1bcc8..81fae9ed54 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachmentStream.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/TextSecureAttachmentStream.java @@ -23,13 +23,15 @@ import java.io.InputStream; */ public class TextSecureAttachmentStream extends TextSecureAttachment { - private final InputStream inputStream; - private final long length; + private final InputStream inputStream; + private final long length; + private final ProgressListener listener; - public TextSecureAttachmentStream(InputStream inputStream, String contentType, long length) { + public TextSecureAttachmentStream(InputStream inputStream, String contentType, long length, ProgressListener listener) { super(contentType); this.inputStream = inputStream; this.length = length; + this.listener = listener; } @Override @@ -49,4 +51,8 @@ public class TextSecureAttachmentStream extends TextSecureAttachment { public long getLength() { return length; } + + public ProgressListener getListener() { + return listener; + } } diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceContactsInputStream.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceContactsInputStream.java index c1ef46868c..8cf8dca07d 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceContactsInputStream.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceContactsInputStream.java @@ -29,7 +29,7 @@ public class DeviceContactsInputStream extends ChunkedInputStream { InputStream avatarStream = new LimitedInputStream(in, avatarLength); String avatarContentType = details.getAvatar().getContentType(); - avatar = Optional.of(new TextSecureAttachmentStream(avatarStream, avatarContentType, avatarLength)); + avatar = Optional.of(new TextSecureAttachmentStream(avatarStream, avatarContentType, avatarLength, null)); } return new DeviceContact(number, name, avatar); diff --git a/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceGroupsInputStream.java b/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceGroupsInputStream.java index 1f82df2bf2..f15fa82b8e 100644 --- a/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceGroupsInputStream.java +++ b/java/src/main/java/org/whispersystems/textsecure/api/messages/multidevice/DeviceGroupsInputStream.java @@ -36,7 +36,7 @@ public class DeviceGroupsInputStream extends ChunkedInputStream{ InputStream avatarStream = new ChunkedInputStream.LimitedInputStream(in, avatarLength); String avatarContentType = details.getAvatar().getContentType(); - avatar = Optional.of(new TextSecureAttachmentStream(avatarStream, avatarContentType, avatarLength)); + avatar = Optional.of(new TextSecureAttachmentStream(avatarStream, avatarContentType, avatarLength, null)); } return new DeviceGroup(id, name, members, avatar); diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/push/PushAttachmentData.java b/java/src/main/java/org/whispersystems/textsecure/internal/push/PushAttachmentData.java index 11b0bcf15c..59c5c172f5 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/push/PushAttachmentData.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/push/PushAttachmentData.java @@ -16,20 +16,26 @@ */ package org.whispersystems.textsecure.internal.push; +import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener; + import java.io.InputStream; public class PushAttachmentData { - private final String contentType; - private final InputStream data; - private final long dataSize; - private final byte[] key; + private final String contentType; + private final InputStream data; + private final long dataSize; + private final byte[] key; + private final ProgressListener listener; - public PushAttachmentData(String contentType, InputStream data, long dataSize, byte[] key) { + public PushAttachmentData(String contentType, InputStream data, long dataSize, + ProgressListener listener, byte[] key) + { this.contentType = contentType; this.data = data; this.dataSize = dataSize; this.key = key; + this.listener = listener; } public String getContentType() { @@ -47,4 +53,8 @@ public class PushAttachmentData { public byte[] getKey() { return key; } + + public ProgressListener getListener() { + return listener; + } } diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java b/java/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java index d62c9b7b72..ebe7f35de0 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java @@ -27,10 +27,11 @@ import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.textsecure.api.crypto.AttachmentCipherOutputStream; +import org.whispersystems.textsecure.api.messages.TextSecureAttachment.ProgressListener; import org.whispersystems.textsecure.api.messages.multidevice.DeviceInfo; import org.whispersystems.textsecure.api.push.ContactTokenDetails; -import org.whispersystems.textsecure.api.push.TextSecureAddress; import org.whispersystems.textsecure.api.push.SignedPreKeyEntity; +import org.whispersystems.textsecure.api.push.TextSecureAddress; import org.whispersystems.textsecure.api.push.TrustStore; import org.whispersystems.textsecure.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.textsecure.api.push.exceptions.ExpectationFailedException; @@ -335,12 +336,12 @@ public class PushServiceSocket { Log.w(TAG, "Got attachment content location: " + attachmentKey.getLocation()); uploadAttachment("PUT", attachmentKey.getLocation(), attachment.getData(), - attachment.getDataSize(), attachment.getKey()); + attachment.getDataSize(), attachment.getKey(), attachment.getListener()); return attachmentKey.getId(); } - public void retrieveAttachment(String relay, long attachmentId, File destination) throws IOException { + public void retrieveAttachment(String relay, long attachmentId, File destination, ProgressListener listener) throws IOException { String path = String.format(ATTACHMENT_PATH, String.valueOf(attachmentId)); if (!Util.isEmpty(relay)) { @@ -352,7 +353,7 @@ public class PushServiceSocket { Log.w(TAG, "Attachment: " + attachmentId + " is at: " + descriptor.getLocation()); - downloadExternalFile(descriptor.getLocation(), destination); + downloadExternalFile(descriptor.getLocation(), destination, listener); } public List retrieveDirectory(Set contactTokens) @@ -374,7 +375,7 @@ public class PushServiceSocket { } } - private void downloadExternalFile(String url, File localDestination) + private void downloadExternalFile(String url, File localDestination, ProgressListener listener) throws IOException { URL downloadUrl = new URL(url); @@ -388,13 +389,19 @@ public class PushServiceSocket { throw new NonSuccessfulResponseCodeException("Bad response: " + connection.getResponseCode()); } - OutputStream output = new FileOutputStream(localDestination); - InputStream input = connection.getInputStream(); - byte[] buffer = new byte[4096]; - int read; + OutputStream output = new FileOutputStream(localDestination); + InputStream input = connection.getInputStream(); + byte[] buffer = new byte[4096]; + int contentLength = connection.getContentLength(); + int read,totalRead = 0; while ((read = input.read(buffer)) != -1) { output.write(buffer, 0, read); + totalRead += read; + + if (listener != null) { + listener.onAttachmentProgress(contentLength, totalRead); + } } output.close(); @@ -406,7 +413,8 @@ public class PushServiceSocket { } } - private void uploadAttachment(String method, String url, InputStream data, long dataSize, byte[] key) + private void uploadAttachment(String method, String url, InputStream data, + long dataSize, byte[] key, ProgressListener listener) throws IOException { URL uploadUrl = new URL(url); @@ -427,9 +435,21 @@ public class PushServiceSocket { try { OutputStream stream = connection.getOutputStream(); AttachmentCipherOutputStream out = new AttachmentCipherOutputStream(key, stream); + byte[] buffer = new byte[4096]; + int read, written = 0; - Util.copy(data, out); + while ((read = data.read(buffer)) != -1) { + out.write(buffer, 0, read); + written += read; + + if (listener != null) { + listener.onAttachmentProgress(dataSize, written); + } + } + + data.close(); out.flush(); + out.close(); if (connection.getResponseCode() != 200) { throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage()); diff --git a/java/src/main/java/org/whispersystems/textsecure/internal/util/Util.java b/java/src/main/java/org/whispersystems/textsecure/internal/util/Util.java index 15917f987a..d08d033f94 100644 --- a/java/src/main/java/org/whispersystems/textsecure/internal/util/Util.java +++ b/java/src/main/java/org/whispersystems/textsecure/internal/util/Util.java @@ -96,7 +96,6 @@ public class Util { } } - public static void copy(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[4096]; int read; @@ -124,19 +123,4 @@ public class Util { throw new AssertionError(e); } } - - public static byte[] toVarint64(long value) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - while (true) { - if ((value & ~0x7FL) == 0) { - out.write((int) value); - return out.toByteArray(); - } else { - out.write(((int) value & 0x7F) | 0x80); - value >>>= 7; - } - } - } - }