diff --git a/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java b/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java index f5a827f64e..fec6746565 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/GroupCreateActivity.java @@ -20,6 +20,7 @@ package org.thoughtcrime.securesms; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; @@ -63,6 +64,7 @@ import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFrag import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.GlideApp; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.BitmapUtil; @@ -76,6 +78,7 @@ import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.util.InvalidNumberException; +import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; @@ -102,7 +105,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity private static final short REQUEST_CODE_SELECT_AVATAR = 26165; private static final int PICK_CONTACT = 1; - public static final int AVATAR_SIZE = 210; private EditText groupName; private ListView lv; @@ -321,7 +323,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity .skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.NONE) .centerCrop() - .override(AVATAR_SIZE, AVATAR_SIZE) + .override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS) .into(new SimpleTarget() { @Override public void onResourceReady(@NonNull Bitmap resource, Transition transition) { @@ -554,10 +556,16 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity existingContacts.addAll(recipients); if (group.isPresent()) { + Bitmap avatar = null; + try { + avatar = BitmapFactory.decodeStream(AvatarHelper.getAvatar(getContext(), group.get().getRecipientId())); + } catch (IOException e) { + Log.w(TAG, "Failed to read avatar."); + } return Optional.of(new GroupData(groupIds[0], existingContacts, - BitmapUtil.fromByteArray(group.get().getAvatar()), - group.get().getAvatar(), + avatar, + BitmapUtil.toByteArray(avatar), group.get().getTitle())); } else { return Optional.absent(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java index 863ab2d5bb..b7f7189f8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java @@ -21,7 +21,6 @@ import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; import org.thoughtcrime.securesms.database.AttachmentDatabase; -import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupReceiptDatabase; import org.thoughtcrime.securesms.database.JobDatabase; import org.thoughtcrime.securesms.database.KeyValueDatabase; @@ -49,7 +48,6 @@ import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -118,9 +116,11 @@ public class FullBackupExporter extends FullBackupBase { stopwatch.split("prefs"); - for (File avatar : AvatarHelper.getAvatarFiles(context)) { - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); - outputStream.write(avatar.getName(), new FileInputStream(avatar), avatar.length()); + for (AvatarHelper.Avatar avatar : AvatarHelper.getAvatars(context)) { + if (avatar != null) { + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); + outputStream.write(avatar.getFilename(), avatar.getInputStream(), avatar.getLength()); + } } stopwatch.split("avatars"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java index 3d24f2e19a..578eb6d5a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java @@ -176,7 +176,7 @@ public class FullBackupImporter extends FullBackupBase { private static void processAvatar(@NonNull Context context, @NonNull SQLiteDatabase db, @NonNull BackupProtos.Avatar avatar, @NonNull BackupRecordInputStream inputStream) throws IOException { if (avatar.hasRecipientId()) { RecipientId recipientId = RecipientId.from(avatar.getRecipientId()); - inputStream.readAttachmentTo(new FileOutputStream(AvatarHelper.getAvatarFile(context, recipientId)), avatar.getLength()); + inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId), avatar.getLength()); } else { if (avatar.hasName() && SqlUtil.tableExists(db, "recipient_preferences")) { Log.w(TAG, "Avatar is missing a recipientId. Clearing signal_profile_avatar (legacy) so it can be fetched later."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GroupRecordContactPhoto.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GroupRecordContactPhoto.java index f360fba5c3..c806068d20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GroupRecordContactPhoto.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GroupRecordContactPhoto.java @@ -10,6 +10,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.util.Conversions; import org.whispersystems.libsignal.util.guava.Optional; @@ -33,11 +34,11 @@ public final class GroupRecordContactPhoto implements ContactPhoto { GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); Optional groupRecord = groupDatabase.getGroup(groupId); - if (groupRecord.isPresent() && groupRecord.get().getAvatar() != null) { - return new ByteArrayInputStream(groupRecord.get().getAvatar()); + if (!groupRecord.isPresent() || !AvatarHelper.hasAvatar(context, groupRecord.get().getRecipientId())) { + throw new IOException("No avatar for group: " + groupId); } - throw new IOException("Couldn't load avatar for group: " + groupId); + return AvatarHelper.getAvatar(context, groupRecord.get().getRecipientId()); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java index b6daaa8191..35b5125044 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ProfileContactPhoto.java @@ -31,13 +31,12 @@ public class ProfileContactPhoto implements ContactPhoto { @Override public @NonNull InputStream openInputStream(Context context) throws IOException { - return AvatarHelper.getInputStreamFor(context, recipient.getId()); + return AvatarHelper.getAvatar(context, recipient.getId()); } @Override public @Nullable Uri getUri(@NonNull Context context) { - File avatarFile = AvatarHelper.getAvatarFile(context, recipient.getId()); - return avatarFile.exists() ? Uri.fromFile(avatarFile) : null; + return null; } @Override @@ -72,12 +71,6 @@ public class ProfileContactPhoto implements ContactPhoto { return 0; } - File avatarFile = AvatarHelper.getAvatarFile(ApplicationDependencies.getApplication(), recipient.getId()); - - if (avatarFile.exists()) { - return avatarFile.lastModified(); - } else { - return 0; - } + return AvatarHelper.getLastModified(ApplicationDependencies.getApplication(), recipient.getId()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/ModernEncryptingPartOutputStream.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/ModernEncryptingPartOutputStream.java index ab4efe84a9..0affeaf2e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/ModernEncryptingPartOutputStream.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/ModernEncryptingPartOutputStream.java @@ -54,4 +54,7 @@ public class ModernEncryptingPartOutputStream { } } + public static long getPlaintextLength(long cipherTextLength) { + return cipherTextLength - 32; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index ddc9296124..0f4dbdb54a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -42,7 +42,6 @@ public class GroupDatabase extends Database { static final String RECIPIENT_ID = "recipient_id"; private static final String TITLE = "title"; private static final String MEMBERS = "members"; - private static final String AVATAR = "avatar"; private static final String AVATAR_ID = "avatar_id"; private static final String AVATAR_KEY = "avatar_key"; private static final String AVATAR_CONTENT_TYPE = "avatar_content_type"; @@ -59,7 +58,6 @@ public class GroupDatabase extends Database { RECIPIENT_ID + " INTEGER, " + TITLE + " TEXT, " + MEMBERS + " TEXT, " + - AVATAR + " BLOB, " + AVATAR_ID + " INTEGER, " + AVATAR_KEY + " BLOB, " + AVATAR_CONTENT_TYPE + " TEXT, " + @@ -75,7 +73,7 @@ public class GroupDatabase extends Database { }; private static final String[] GROUP_PROJECTION = { - GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, + GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, TIMESTAMP, ACTIVE, MMS }; @@ -120,7 +118,7 @@ public class GroupDatabase extends Database { return true; } - boolean noMetadata = group.get().getAvatar() == null && TextUtils.isEmpty(group.get().getTitle()); + boolean noMetadata = !group.get().hasAvatar() && TextUtils.isEmpty(group.get().getTitle()); boolean noMembers = group.get().getMembers().isEmpty() || (group.get().getMembers().size() == 1 && group.get().getMembers().contains(Recipient.self().getId())); return noMetadata && noMembers; @@ -228,6 +226,8 @@ public class GroupDatabase extends Database { contentValues.put(AVATAR_KEY, avatar.getKey()); contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType()); contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull()); + } else { + contentValues.put(AVATAR_ID, 0); } contentValues.put(AVATAR_RELAY, relay); @@ -252,6 +252,8 @@ public class GroupDatabase extends Database { contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType()); contentValues.put(AVATAR_KEY, avatar.getKey()); contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull()); + } else { + contentValues.put(AVATAR_ID, 0); } databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, @@ -274,20 +276,12 @@ public class GroupDatabase extends Database { Recipient.live(groupRecipient).refresh(); } - public void updateAvatar(@NonNull GroupId groupId, @Nullable Bitmap avatar) { - updateAvatar(groupId, BitmapUtil.toByteArray(avatar)); - } - - public void updateAvatar(@NonNull GroupId groupId, @Nullable byte[] avatar) { - long avatarId; - - if (avatar != null) avatarId = Math.abs(new SecureRandom().nextLong()); - else avatarId = 0; - - - ContentValues contentValues = new ContentValues(2); - contentValues.put(AVATAR, avatar); - contentValues.put(AVATAR_ID, avatarId); + /** + * Used to bust the Glide cache when an avatar changes. + */ + public void onAvatarUpdated(@NonNull GroupId groupId, boolean hasAvatar) { + ContentValues contentValues = new ContentValues(1); + contentValues.put(AVATAR_ID, hasAvatar ? Math.abs(new SecureRandom().nextLong()) : 0); databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", new String[] {groupId.toString()}); @@ -388,7 +382,6 @@ public class GroupDatabase extends Database { RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))), cursor.getString(cursor.getColumnIndexOrThrow(TITLE)), cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)), - cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR)), cursor.getLong(cursor.getColumnIndexOrThrow(AVATAR_ID)), cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_KEY)), cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_CONTENT_TYPE)), @@ -411,7 +404,6 @@ public class GroupDatabase extends Database { private final RecipientId recipientId; private final String title; private final List members; - private final byte[] avatar; private final long avatarId; private final byte[] avatarKey; private final byte[] avatarDigest; @@ -420,14 +412,13 @@ public class GroupDatabase extends Database { private final boolean active; private final boolean mms; - public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, String title, String members, byte[] avatar, + public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, String title, String members, long avatarId, byte[] avatarKey, String avatarContentType, String relay, boolean active, byte[] avatarDigest, boolean mms) { this.id = id; this.recipientId = recipientId; this.title = title; - this.avatar = avatar; this.avatarId = avatarId; this.avatarKey = avatarKey; this.avatarDigest = avatarDigest; @@ -456,8 +447,8 @@ public class GroupDatabase extends Database { return members; } - public byte[] getAvatar() { - return avatar; + public boolean hasAvatar() { + return avatarId != 0; } public long getAvatarId() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index f4ae251b54..b1895111fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -21,7 +21,9 @@ import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabaseHook; import net.sqlcipher.database.SQLiteOpenHelper; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.ProfileName; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.crypto.DatabaseSecret; import org.thoughtcrime.securesms.crypto.MasterSecret; @@ -51,14 +53,17 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.service.KeyCachingService; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.Base64; +import org.thoughtcrime.securesms.util.FileUtils; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; +import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.util.List; public class SQLCipherOpenHelper extends SQLiteOpenHelper { @@ -118,8 +123,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int GROUPS_V2_RECIPIENT_CAPABILITY = 51; private static final int TRANSFER_FILE_CLEANUP = 52; private static final int PROFILE_DATA_MIGRATION = 53; + private static final int AVATAR_LOCATION_MIGRATION = 54; - private static final int DATABASE_VERSION = 53; + private static final int DATABASE_VERSION = 54; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -802,6 +808,49 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } } + if (oldVersion < AVATAR_LOCATION_MIGRATION) { + File oldAvatarDirectory = new File(context.getFilesDir(), "avatars"); + File[] results = oldAvatarDirectory.listFiles(); + + if (results != null) { + Log.i(TAG, "Preparing to migrate " + results.length + " avatars."); + + for (File file : results) { + if (Util.isLong(file.getName())) { + try { + AvatarHelper.setAvatar(context, RecipientId.from(file.getName()), new FileInputStream(file)); + } catch(IOException e) { + Log.w(TAG, "Failed to copy file " + file.getName() + "! Skipping."); + } + } else { + Log.w(TAG, "Invalid avatar name '" + file.getName() + "'! Skipping."); + } + } + } else { + Log.w(TAG, "No avatar directory files found."); + } + + if (!FileUtils.deleteDirectory(oldAvatarDirectory)) { + Log.w(TAG, "Failed to delete avatar directory."); + } + + try (Cursor cursor = db.rawQuery("SELECT recipient_id, avatar FROM groups", null)) { + while (cursor != null && cursor.moveToNext()) { + RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow("recipient_id"))); + byte[] avatar = cursor.getBlob(cursor.getColumnIndexOrThrow("avatar")); + + try { + AvatarHelper.setAvatar(context, recipientId, avatar != null ? new ByteArrayInputStream(avatar) : null); + } catch (IOException e) { + Log.w(TAG, "Failed to copy avatar for " + recipientId + "! Skipping.", e); + } + } + } + + db.execSQL("UPDATE groups SET avatar_id = 0 WHERE avatar IS NULL"); + db.execSQL("UPDATE groups SET avatar = NULL"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java index 451ea29a3e..6abee264f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/V1GroupManager.java @@ -19,7 +19,9 @@ import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult; import org.thoughtcrime.securesms.jobs.LeaveGroupJob; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -32,6 +34,8 @@ import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -39,6 +43,8 @@ import java.util.Set; final class V1GroupManager { + private static final String TAG = Log.tag(V1GroupManager.class); + static @NonNull GroupActionResult createGroup(@NonNull Context context, @NonNull Set memberIds, @Nullable Bitmap avatar, @@ -55,7 +61,12 @@ final class V1GroupManager { groupDatabase.create(groupId, name, new LinkedList<>(memberIds), null, null); if (!mms) { - groupDatabase.updateAvatar(groupId, avatarBytes); + try { + AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null); + } catch (IOException e) { + Log.w(TAG, "Failed to save avatar!", e); + } + groupDatabase.onAvatarUpdated(groupId, avatarBytes != null); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true); return sendGroupUpdate(context, groupId, memberIds, name, avatarBytes); } else { @@ -71,18 +82,23 @@ final class V1GroupManager { @Nullable String name) throws InvalidNumberException { - final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); + final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId); + final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); memberAddresses.add(Recipient.self().getId()); groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses)); groupDatabase.updateTitle(groupId, name); - groupDatabase.updateAvatar(groupId, avatarBytes); + groupDatabase.onAvatarUpdated(groupId, avatarBytes != null); if (!groupId.isMmsGroup()) { + try { + AvatarHelper.setAvatar(context, groupRecipientId, avatarBytes != null ? new ByteArrayInputStream(avatarBytes) : null); + } catch (IOException e) { + Log.w(TAG, "Failed to save avatar!", e); + } return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes); } else { - RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId); Recipient groupRecipient = Recipient.resolved(groupRecipientId); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); return new GroupActionResult(groupRecipient, threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java index 90b113d863..7a5ace5d73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.Hex; @@ -90,13 +91,14 @@ public class AvatarDownloadJob extends BaseJob { SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), Optional.absent()); - InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE); - Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500); + InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); + + AvatarHelper.setAvatar(context, record.get().getRecipientId(), inputStream); + DatabaseFactory.getGroupDatabase(context).onAvatarUpdated(groupId, true); - database.updateAvatar(groupId, avatar); inputStream.close(); } - } catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) { + } catch (NonSuccessfulResponseCodeException | InvalidMessageException e) { Log.w(TAG, e); } finally { if (attachment != null) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java index ca4405acc9..bf52919e2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -100,7 +101,7 @@ public class MultiDeviceGroupUpdateJob extends BaseJob { out.write(new DeviceGroup(record.getId().getDecodedId(), Optional.fromNullable(record.getTitle()), members, - getAvatar(record.getAvatar()), + getAvatar(record.getRecipientId()), record.isActive(), expirationTimer, Optional.of(recipient.getColor().serialize()), @@ -151,13 +152,13 @@ public class MultiDeviceGroupUpdateJob extends BaseJob { } - private Optional getAvatar(@Nullable byte[] avatar) { - if (avatar == null) return Optional.absent(); + private Optional getAvatar(@NonNull RecipientId recipientId) throws IOException { + if (!AvatarHelper.hasAvatar(context, recipientId)) return Optional.absent(); return Optional.of(SignalServiceAttachment.newStreamBuilder() - .withStream(new ByteArrayInputStream(avatar)) + .withStream(AvatarHelper.getAvatar(context, recipientId)) .withContentType("image/*") - .withLength(avatar.length) + .withLength(AvatarHelper.getAvatarLength(context, recipientId)) .build()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java index 54cb01b52f..4fb269e551 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -80,16 +81,16 @@ public class PushGroupUpdateJob extends BaseJob { Optional record = groupDatabase.getGroup(groupId); SignalServiceAttachment avatar = null; - if (record == null) { + if (record == null || !record.isPresent()) { Log.w(TAG, "No information for group record info request: " + groupId.toString()); return; } - if (record.get().getAvatar() != null) { + if (AvatarHelper.hasAvatar(context, record.get().getRecipientId())) { avatar = SignalServiceAttachmentStream.newStreamBuilder() .withContentType("image/jpeg") - .withStream(new ByteArrayInputStream(record.get().getAvatar())) - .withLength(record.get().getAvatar().length) + .withStream(AvatarHelper.getAvatar(context, record.get().getRecipientId())) + .withLength(AvatarHelper.getAvatarLength(context, record.get().getRecipientId())) .build(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java index 96bf2fdc78..087b9a015d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -97,17 +97,10 @@ public class RetrieveProfileAvatarJob extends BaseJob { File downloadDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); try { - SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); - InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES); - File decryptDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); + SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); + InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); - try { - Util.copy(avatarStream, new FileOutputStream(decryptDestination)); - } catch (AssertionError e) { - throw new IOException("Failed to copy stream. Likely a Conscrypt issue.", e); - } - - decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.getId())); + AvatarHelper.setAvatar(context, recipient.getId(), avatarStream); } catch (PushNetworkException e) { if (e.getCause() instanceof NonSuccessfulResponseCodeException) { Log.w(TAG, "Removing profile avatar (no image available) for: " + recipient.getId().serialize()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java index 37343f5b87..2acb0bc4fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java @@ -17,6 +17,7 @@ import androidx.lifecycle.ViewModelProviders; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.TransportOptions; import org.thoughtcrime.securesms.imageeditor.model.EditorModel; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.scribbles.ImageEditorFragment; import org.thoughtcrime.securesms.util.MediaUtil; @@ -27,7 +28,7 @@ import java.util.Collections; public class AvatarSelectionActivity extends AppCompatActivity implements CameraFragment.Controller, ImageEditorFragment.Controller, MediaPickerFolderFragment.Controller, MediaPickerItemFragment.Controller { - private static final Point AVATAR_DIMENSIONS = new Point(1024, 1024); + private static final Point AVATAR_DIMENSIONS = new Point(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS); private static final String IMAGE_CAPTURE = "IMAGE_CAPTURE"; private static final String IMAGE_EDITOR = "IMAGE_EDITOR"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarMigrationJob.java index e1966f70a1..4396df8316 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/AvatarMigrationJob.java @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.Util; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -64,7 +65,7 @@ public class AvatarMigrationJob extends MigrationJob { Recipient recipient = Recipient.external(context, file.getName()); byte[] data = Util.readFully(new FileInputStream(file)); - AvatarHelper.setAvatar(context, recipient.getId(), data); + AvatarHelper.setAvatar(context, recipient.getId(), new ByteArrayInputStream(data)); } else { Log.w(TAG, "Invalid file name! Can't migrate this file. It'll just get deleted."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java index 93db338add..d919517482 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/AvatarHelper.java @@ -6,81 +6,191 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.annimon.stream.Stream; - +import org.thoughtcrime.securesms.crypto.AttachmentSecret; +import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; +import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; +import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.ByteUnit; import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.util.StreamDetails; -import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.LinkedList; -import java.util.List; +import java.io.OutputStream; +import java.util.Collections; +import java.util.Iterator; public class AvatarHelper { + private static final String TAG = Log.tag(AvatarHelper.class); + + public static int AVATAR_DIMENSIONS = 1024; + public static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = ByteUnit.MEGABYTES.toBytes(10); + private static final String AVATAR_DIRECTORY = "avatars"; - public static InputStream getInputStreamFor(@NonNull Context context, @NonNull RecipientId recipientId) - throws IOException - { - return new FileInputStream(getAvatarFile(context, recipientId)); - } - - public static List getAvatarFiles(@NonNull Context context) { - File avatarDirectory = new File(context.getFilesDir(), AVATAR_DIRECTORY); + /** + * Retrieves an iterable set of avatars. Only intended to be used during backup. + */ + public static Iterable getAvatars(@NonNull Context context) { + File avatarDirectory = context.getDir(AVATAR_DIRECTORY, Context.MODE_PRIVATE); File[] results = avatarDirectory.listFiles(); - if (results == null) return new LinkedList<>(); - else return Stream.of(results).toList(); + if (results == null) { + return Collections.emptyList(); + } + + return () -> { + return new Iterator() { + int i = 0; + @Override + public boolean hasNext() { + return i < results.length; + } + + @Override + public Avatar next() { + File file = results[i]; + try { + return new Avatar(getAvatar(context, RecipientId.from(file.getName())), + file.getName(), + ModernEncryptingPartOutputStream.getPlaintextLength(file.length())); + } catch (IOException e) { + return null; + } finally { + i++; + } + } + }; + }; } + /** + * Deletes and avatar. + */ public static void delete(@NonNull Context context, @NonNull RecipientId recipientId) { getAvatarFile(context, recipientId).delete(); } - public static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId) { - File avatarDirectory = new File(context.getFilesDir(), AVATAR_DIRECTORY); - avatarDirectory.mkdirs(); - - return new File(avatarDirectory, new File(recipientId.serialize()).getName()); + /** + * Whether or not an avatar is present for the given recipient. + */ + public static boolean hasAvatar(@NonNull Context context, @NonNull RecipientId recipientId) { + return getAvatarFile(context, recipientId).exists(); } - public static void setAvatar(@NonNull Context context, @NonNull RecipientId recipientId, @Nullable byte[] data) - throws IOException + /** + * Retrieves a stream for an avatar. If there is no avatar, the stream will likely throw an + * IOException. It is recommended to call {@link #hasAvatar(Context, RecipientId)} first. + */ + public static @NonNull InputStream getAvatar(@NonNull Context context, @NonNull RecipientId recipientId) throws IOException { + AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); + File avatarFile = getAvatarFile(context, recipientId); + + return ModernDecryptingPartInputStream.createFor(attachmentSecret, avatarFile, 0); + } + + /** + * Returns the size of the avatar on disk. + */ + public static long getAvatarLength(@NonNull Context context, @NonNull RecipientId recipientId) { + return ModernEncryptingPartOutputStream.getPlaintextLength(getAvatarFile(context, recipientId).length()); + } + + /** + * Saves the contents of the input stream as the avatar for the specified recipient. If you pass + * in null for the stream, the avatar will be deleted. + */ + public static void setAvatar(@NonNull Context context, @NonNull RecipientId recipientId, @Nullable InputStream inputStream) + throws IOException { - if (data == null) { + if (inputStream == null) { delete(context, recipientId); - } else { - FileOutputStream out = new FileOutputStream(getAvatarFile(context, recipientId)); - out.write(data); - out.close(); + return; + } + + OutputStream outputStream = null; + try { + outputStream = getOutputStream(context, recipientId); + Util.copy(inputStream, outputStream); + } finally { + Util.close(outputStream); } } - public static @NonNull StreamDetails avatarStream(@NonNull byte[] data) { - return new StreamDetails(new ByteArrayInputStream(data), MediaUtil.IMAGE_JPEG, data.length); + /** + * Retrieves an output stream you can write to that will be saved as the avatar for the specified + * recipient. Only intended to be used for backup. Otherwise, use {@link #setAvatar(Context, RecipientId, InputStream)}. + */ + public static @NonNull OutputStream getOutputStream(@NonNull Context context, @NonNull RecipientId recipientId) throws IOException { + AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); + File targetFile = getAvatarFile(context, recipientId); + return ModernEncryptingPartOutputStream.createFor(attachmentSecret, targetFile, true).second; } - public static @Nullable StreamDetails getSelfProfileAvatarStream(@NonNull Context context) { - File avatarFile = getAvatarFile(context, Recipient.self().getId()); + /** + * Returns the timestamp of when the avatar was last modified, or zero if the avatar doesn't exist. + */ + public static long getLastModified(@NonNull Context context, @NonNull RecipientId recipientId) { + File file = getAvatarFile(context, recipientId); - if (avatarFile.exists() && avatarFile.length() > 0) { - try { - FileInputStream stream = new FileInputStream(avatarFile); - - return new StreamDetails(stream, MediaUtil.IMAGE_JPEG, avatarFile.length()); - } catch (FileNotFoundException e) { - throw new AssertionError(e); - } + if (file.exists()) { + return file.lastModified(); } else { + return 0; + } + } + + /** + * Returns a {@link StreamDetails} for the local user's own avatar, or null if one does not exist. + */ + public static @Nullable StreamDetails getSelfProfileAvatarStream(@NonNull Context context) { + RecipientId selfId = Recipient.self().getId(); + + if (!hasAvatar(context, selfId)) { + return null; + } + + try { + InputStream stream = getAvatar(context, selfId); + return new StreamDetails(stream, MediaUtil.IMAGE_JPEG, getAvatarLength(context, selfId)); + } catch (IOException e) { + Log.w(TAG, "Failed to read own avatar!", e); return null; } } + + private static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId) { + File directory = context.getDir(AVATAR_DIRECTORY, Context.MODE_PRIVATE); + return new File(directory, recipientId.serialize()); + } + + public static class Avatar { + private final InputStream inputStream; + private final String filename; + private final long length; + + public Avatar(@NonNull InputStream inputStream, @NonNull String filename, long length) { + this.inputStream = inputStream; + this.filename = filename; + this.length = length; + } + + public @NonNull InputStream getInputStream() { + return inputStream; + } + + public @NonNull String getFilename() { + return filename; + } + + public long getLength() { + return length; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java index dbfcce1aaf..26ee62ca08 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.SecureRandom; import java.util.Arrays; @@ -76,10 +77,10 @@ class EditProfileRepository { void getCurrentAvatar(@NonNull Consumer avatarConsumer) { RecipientId selfId = Recipient.self().getId(); - if (AvatarHelper.getAvatarFile(context, selfId).exists() && AvatarHelper.getAvatarFile(context, selfId).length() > 0) { + if (AvatarHelper.hasAvatar(context, selfId)) { SimpleTask.run(() -> { try { - return Util.readFully(AvatarHelper.getInputStreamFor(context, selfId)); + return Util.readFully(AvatarHelper.getAvatar(context, selfId)); } catch (IOException e) { Log.w(TAG, e); return null; @@ -106,7 +107,7 @@ class EditProfileRepository { DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), profileName); try { - AvatarHelper.setAvatar(context, Recipient.self().getId(), avatar); + AvatarHelper.setAvatar(context, Recipient.self().getId(), avatar != null ? new ByteArrayInputStream(avatar) : null); } catch (IOException e) { return UploadResult.ERROR_FILE_IO; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java index 8b584f81be..2c8775a1e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java @@ -201,7 +201,7 @@ public final class LiveRecipient { title = unnamedGroupName; } - if (groupRecord.get().getAvatar() != null && groupRecord.get().getAvatar().length > 0) { + if (groupRecord.get().hasAvatar()) { avatarId = Optional.of(groupRecord.get().getAvatarId()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FileUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/FileUtils.java index 1937296c49..865325b5b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FileUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FileUtils.java @@ -49,13 +49,13 @@ public final class FileUtils { } } - public static void deleteDirectory(@Nullable File directory) { + public static boolean deleteDirectory(@Nullable File directory) { if (directory == null || !directory.exists() || !directory.isDirectory()) { - return; + return false; } deleteDirectoryContents(directory); - directory.delete(); + return directory.delete(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index 01ed441726..c064198cd6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -224,7 +224,9 @@ public class Util { } } - public static void close(Closeable closeable) { + public static void close(@Nullable Closeable closeable) { + if (closeable == null) return; + try { closeable.close(); } catch (IOException e) { @@ -607,4 +609,12 @@ public class Util { return concat; } + public static boolean isLong(String value) { + try { + Long.parseLong(value); + return true; + } catch (NumberFormatException e) { + return false; + } + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index d863b2aa2b..74961c334e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -113,7 +113,7 @@ public class SignalServiceMessageReceiver { * @throws IOException * @throws InvalidMessageException */ - public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes) + public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes) throws IOException, InvalidMessageException { return retrieveAttachment(pointer, destination, maxSizeBytes, null); @@ -142,7 +142,7 @@ public class SignalServiceMessageReceiver { return socket.retrieveProfileByUsername(username, unidentifiedAccess); } - public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, int maxSizeBytes) + public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, long maxSizeBytes) throws IOException { socket.retrieveProfileAvatar(path, destination, maxSizeBytes); @@ -162,7 +162,7 @@ public class SignalServiceMessageReceiver { * @throws IOException * @throws InvalidMessageException */ - public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes, ProgressListener listener) + public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes, ProgressListener listener) throws IOException, InvalidMessageException { if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!"); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index eb74f0c72f..eaa7e6ea5f 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -498,7 +498,7 @@ public class PushServiceSocket { makeServiceRequest(SIGNED_PREKEY_PATH, "PUT", JsonUtil.toJson(signedPreKeyEntity)); } - public void retrieveAttachment(long attachmentId, File destination, int maxSizeBytes, ProgressListener listener) + public void retrieveAttachment(long attachmentId, File destination, long maxSizeBytes, ProgressListener listener) throws NonSuccessfulResponseCodeException, PushNetworkException { downloadFromCdn(destination, String.format(Locale.US, ATTACHMENT_DOWNLOAD_PATH, attachmentId), maxSizeBytes, listener); @@ -590,7 +590,7 @@ public class PushServiceSocket { } } - public void retrieveProfileAvatar(String path, File destination, int maxSizeBytes) + public void retrieveProfileAvatar(String path, File destination, long maxSizeBytes) throws NonSuccessfulResponseCodeException, PushNetworkException { downloadFromCdn(destination, path, maxSizeBytes, null); @@ -874,7 +874,7 @@ public class PushServiceSocket { return new Pair<>(id, digest); } - private void downloadFromCdn(File destination, String path, int maxSizeBytes, ProgressListener listener) + private void downloadFromCdn(File destination, String path, long maxSizeBytes, ProgressListener listener) throws PushNetworkException, NonSuccessfulResponseCodeException { try (FileOutputStream outputStream = new FileOutputStream(destination, true)) { @@ -884,7 +884,7 @@ public class PushServiceSocket { } } - private void downloadFromCdn(OutputStream outputStream, long offset, String path, int maxSizeBytes, ProgressListener listener) + private void downloadFromCdn(OutputStream outputStream, long offset, String path, long maxSizeBytes, ProgressListener listener) throws PushNetworkException, NonSuccessfulResponseCodeException { ConnectionHolder connectionHolder = getRandom(cdnClients, random);