Migrate avatars and group avatars.

This commit is contained in:
Greyson Parrelli 2020-03-26 15:38:27 -04:00
parent 9848599807
commit 10bfc8a753
22 changed files with 317 additions and 136 deletions

View File

@ -20,6 +20,7 @@ package org.thoughtcrime.securesms;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
@ -63,6 +64,7 @@ import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFrag
import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BitmapUtil; 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.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; 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 short REQUEST_CODE_SELECT_AVATAR = 26165;
private static final int PICK_CONTACT = 1; private static final int PICK_CONTACT = 1;
public static final int AVATAR_SIZE = 210;
private EditText groupName; private EditText groupName;
private ListView lv; private ListView lv;
@ -321,7 +323,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
.skipMemoryCache(true) .skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.centerCrop() .centerCrop()
.override(AVATAR_SIZE, AVATAR_SIZE) .override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS)
.into(new SimpleTarget<Bitmap>() { .into(new SimpleTarget<Bitmap>() {
@Override @Override
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) { public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
@ -554,10 +556,16 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
existingContacts.addAll(recipients); existingContacts.addAll(recipients);
if (group.isPresent()) { 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], return Optional.of(new GroupData(groupIds[0],
existingContacts, existingContacts,
BitmapUtil.fromByteArray(group.get().getAvatar()), avatar,
group.get().getAvatar(), BitmapUtil.toByteArray(avatar),
group.get().getTitle())); group.get().getTitle()));
} else { } else {
return Optional.absent(); return Optional.absent();

View File

@ -21,7 +21,6 @@ import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase; import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.JobDatabase; import org.thoughtcrime.securesms.database.JobDatabase;
import org.thoughtcrime.securesms.database.KeyValueDatabase; import org.thoughtcrime.securesms.database.KeyValueDatabase;
@ -49,7 +48,6 @@ import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -118,9 +116,11 @@ public class FullBackupExporter extends FullBackupBase {
stopwatch.split("prefs"); stopwatch.split("prefs");
for (File avatar : AvatarHelper.getAvatarFiles(context)) { for (AvatarHelper.Avatar avatar : AvatarHelper.getAvatars(context)) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); if (avatar != null) {
outputStream.write(avatar.getName(), new FileInputStream(avatar), avatar.length()); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
outputStream.write(avatar.getFilename(), avatar.getInputStream(), avatar.getLength());
}
} }
stopwatch.split("avatars"); stopwatch.split("avatars");

View File

@ -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 { private static void processAvatar(@NonNull Context context, @NonNull SQLiteDatabase db, @NonNull BackupProtos.Avatar avatar, @NonNull BackupRecordInputStream inputStream) throws IOException {
if (avatar.hasRecipientId()) { if (avatar.hasRecipientId()) {
RecipientId recipientId = RecipientId.from(avatar.getRecipientId()); 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 { } else {
if (avatar.hasName() && SqlUtil.tableExists(db, "recipient_preferences")) { 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."); Log.w(TAG, "Avatar is missing a recipientId. Clearing signal_profile_avatar (legacy) so it can be fetched later.");

View File

@ -10,6 +10,7 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.util.Conversions; import org.thoughtcrime.securesms.util.Conversions;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
@ -33,11 +34,11 @@ public final class GroupRecordContactPhoto implements ContactPhoto {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
Optional<GroupDatabase.GroupRecord> groupRecord = groupDatabase.getGroup(groupId); Optional<GroupDatabase.GroupRecord> groupRecord = groupDatabase.getGroup(groupId);
if (groupRecord.isPresent() && groupRecord.get().getAvatar() != null) { if (!groupRecord.isPresent() || !AvatarHelper.hasAvatar(context, groupRecord.get().getRecipientId())) {
return new ByteArrayInputStream(groupRecord.get().getAvatar()); 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 @Override

View File

@ -31,13 +31,12 @@ public class ProfileContactPhoto implements ContactPhoto {
@Override @Override
public @NonNull InputStream openInputStream(Context context) throws IOException { public @NonNull InputStream openInputStream(Context context) throws IOException {
return AvatarHelper.getInputStreamFor(context, recipient.getId()); return AvatarHelper.getAvatar(context, recipient.getId());
} }
@Override @Override
public @Nullable Uri getUri(@NonNull Context context) { public @Nullable Uri getUri(@NonNull Context context) {
File avatarFile = AvatarHelper.getAvatarFile(context, recipient.getId()); return null;
return avatarFile.exists() ? Uri.fromFile(avatarFile) : null;
} }
@Override @Override
@ -72,12 +71,6 @@ public class ProfileContactPhoto implements ContactPhoto {
return 0; return 0;
} }
File avatarFile = AvatarHelper.getAvatarFile(ApplicationDependencies.getApplication(), recipient.getId()); return AvatarHelper.getLastModified(ApplicationDependencies.getApplication(), recipient.getId());
if (avatarFile.exists()) {
return avatarFile.lastModified();
} else {
return 0;
}
} }
} }

View File

@ -54,4 +54,7 @@ public class ModernEncryptingPartOutputStream {
} }
} }
public static long getPlaintextLength(long cipherTextLength) {
return cipherTextLength - 32;
}
} }

View File

@ -42,7 +42,6 @@ public class GroupDatabase extends Database {
static final String RECIPIENT_ID = "recipient_id"; static final String RECIPIENT_ID = "recipient_id";
private static final String TITLE = "title"; private static final String TITLE = "title";
private static final String MEMBERS = "members"; 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_ID = "avatar_id";
private static final String AVATAR_KEY = "avatar_key"; private static final String AVATAR_KEY = "avatar_key";
private static final String AVATAR_CONTENT_TYPE = "avatar_content_type"; private static final String AVATAR_CONTENT_TYPE = "avatar_content_type";
@ -59,7 +58,6 @@ public class GroupDatabase extends Database {
RECIPIENT_ID + " INTEGER, " + RECIPIENT_ID + " INTEGER, " +
TITLE + " TEXT, " + TITLE + " TEXT, " +
MEMBERS + " TEXT, " + MEMBERS + " TEXT, " +
AVATAR + " BLOB, " +
AVATAR_ID + " INTEGER, " + AVATAR_ID + " INTEGER, " +
AVATAR_KEY + " BLOB, " + AVATAR_KEY + " BLOB, " +
AVATAR_CONTENT_TYPE + " TEXT, " + AVATAR_CONTENT_TYPE + " TEXT, " +
@ -75,7 +73,7 @@ public class GroupDatabase extends Database {
}; };
private static final String[] GROUP_PROJECTION = { 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 TIMESTAMP, ACTIVE, MMS
}; };
@ -120,7 +118,7 @@ public class GroupDatabase extends Database {
return true; 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())); boolean noMembers = group.get().getMembers().isEmpty() || (group.get().getMembers().size() == 1 && group.get().getMembers().contains(Recipient.self().getId()));
return noMetadata && noMembers; return noMetadata && noMembers;
@ -228,6 +226,8 @@ public class GroupDatabase extends Database {
contentValues.put(AVATAR_KEY, avatar.getKey()); contentValues.put(AVATAR_KEY, avatar.getKey());
contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType()); contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType());
contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull()); contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull());
} else {
contentValues.put(AVATAR_ID, 0);
} }
contentValues.put(AVATAR_RELAY, relay); contentValues.put(AVATAR_RELAY, relay);
@ -252,6 +252,8 @@ public class GroupDatabase extends Database {
contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType()); contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType());
contentValues.put(AVATAR_KEY, avatar.getKey()); contentValues.put(AVATAR_KEY, avatar.getKey());
contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull()); contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull());
} else {
contentValues.put(AVATAR_ID, 0);
} }
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,
@ -274,20 +276,12 @@ public class GroupDatabase extends Database {
Recipient.live(groupRecipient).refresh(); Recipient.live(groupRecipient).refresh();
} }
public void updateAvatar(@NonNull GroupId groupId, @Nullable Bitmap avatar) { /**
updateAvatar(groupId, BitmapUtil.toByteArray(avatar)); * Used to bust the Glide cache when an avatar changes.
} */
public void onAvatarUpdated(@NonNull GroupId groupId, boolean hasAvatar) {
public void updateAvatar(@NonNull GroupId groupId, @Nullable byte[] avatar) { ContentValues contentValues = new ContentValues(1);
long avatarId; contentValues.put(AVATAR_ID, hasAvatar ? Math.abs(new SecureRandom().nextLong()) : 0);
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);
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?",
new String[] {groupId.toString()}); new String[] {groupId.toString()});
@ -388,7 +382,6 @@ public class GroupDatabase extends Database {
RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))), RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))),
cursor.getString(cursor.getColumnIndexOrThrow(TITLE)), cursor.getString(cursor.getColumnIndexOrThrow(TITLE)),
cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)), cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)),
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR)),
cursor.getLong(cursor.getColumnIndexOrThrow(AVATAR_ID)), cursor.getLong(cursor.getColumnIndexOrThrow(AVATAR_ID)),
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_KEY)), cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_KEY)),
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_CONTENT_TYPE)), cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_CONTENT_TYPE)),
@ -411,7 +404,6 @@ public class GroupDatabase extends Database {
private final RecipientId recipientId; private final RecipientId recipientId;
private final String title; private final String title;
private final List<RecipientId> members; private final List<RecipientId> members;
private final byte[] avatar;
private final long avatarId; private final long avatarId;
private final byte[] avatarKey; private final byte[] avatarKey;
private final byte[] avatarDigest; private final byte[] avatarDigest;
@ -420,14 +412,13 @@ public class GroupDatabase extends Database {
private final boolean active; private final boolean active;
private final boolean mms; 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, long avatarId, byte[] avatarKey, String avatarContentType,
String relay, boolean active, byte[] avatarDigest, boolean mms) String relay, boolean active, byte[] avatarDigest, boolean mms)
{ {
this.id = id; this.id = id;
this.recipientId = recipientId; this.recipientId = recipientId;
this.title = title; this.title = title;
this.avatar = avatar;
this.avatarId = avatarId; this.avatarId = avatarId;
this.avatarKey = avatarKey; this.avatarKey = avatarKey;
this.avatarDigest = avatarDigest; this.avatarDigest = avatarDigest;
@ -456,8 +447,8 @@ public class GroupDatabase extends Database {
return members; return members;
} }
public byte[] getAvatar() { public boolean hasAvatar() {
return avatar; return avatarId != 0;
} }
public long getAvatarId() { public long getAvatarId() {

View File

@ -21,7 +21,9 @@ import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteDatabaseHook; import net.sqlcipher.database.SQLiteDatabaseHook;
import net.sqlcipher.database.SQLiteOpenHelper; import net.sqlcipher.database.SQLiteOpenHelper;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.crypto.DatabaseSecret; import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.crypto.MasterSecret; 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.notifications.NotificationChannels;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.FileUtils;
import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.List; import java.util.List;
public class SQLCipherOpenHelper extends SQLiteOpenHelper { 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 GROUPS_V2_RECIPIENT_CAPABILITY = 51;
private static final int TRANSFER_FILE_CLEANUP = 52; private static final int TRANSFER_FILE_CLEANUP = 52;
private static final int PROFILE_DATA_MIGRATION = 53; 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 static final String DATABASE_NAME = "signal.db";
private final Context context; 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(); db.setTransactionSuccessful();
} finally { } finally {
db.endTransaction(); db.endTransaction();

View File

@ -19,7 +19,9 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult; import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
import org.thoughtcrime.securesms.jobs.LeaveGroupJob; import org.thoughtcrime.securesms.jobs.LeaveGroupJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; 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.api.util.InvalidNumberException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -39,6 +43,8 @@ import java.util.Set;
final class V1GroupManager { final class V1GroupManager {
private static final String TAG = Log.tag(V1GroupManager.class);
static @NonNull GroupActionResult createGroup(@NonNull Context context, static @NonNull GroupActionResult createGroup(@NonNull Context context,
@NonNull Set<RecipientId> memberIds, @NonNull Set<RecipientId> memberIds,
@Nullable Bitmap avatar, @Nullable Bitmap avatar,
@ -55,7 +61,12 @@ final class V1GroupManager {
groupDatabase.create(groupId, name, new LinkedList<>(memberIds), null, null); groupDatabase.create(groupId, name, new LinkedList<>(memberIds), null, null);
if (!mms) { 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); DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient.getId(), true);
return sendGroupUpdate(context, groupId, memberIds, name, avatarBytes); return sendGroupUpdate(context, groupId, memberIds, name, avatarBytes);
} else { } else {
@ -71,18 +82,23 @@ final class V1GroupManager {
@Nullable String name) @Nullable String name)
throws InvalidNumberException throws InvalidNumberException
{ {
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar); final RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
memberAddresses.add(Recipient.self().getId()); memberAddresses.add(Recipient.self().getId());
groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses)); groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses));
groupDatabase.updateTitle(groupId, name); groupDatabase.updateTitle(groupId, name);
groupDatabase.updateAvatar(groupId, avatarBytes); groupDatabase.onAvatarUpdated(groupId, avatarBytes != null);
if (!groupId.isMmsGroup()) { 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); return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes);
} else { } else {
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
Recipient groupRecipient = Recipient.resolved(groupRecipientId); Recipient groupRecipient = Recipient.resolved(groupRecipientId);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
return new GroupActionResult(groupRecipient, threadId); return new GroupActionResult(groupRecipient, threadId);

View File

@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.Hex;
@ -90,13 +91,14 @@ public class AvatarDownloadJob extends BaseJob {
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); 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()); 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); InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500);
AvatarHelper.setAvatar(context, record.get().getRecipientId(), inputStream);
DatabaseFactory.getGroupDatabase(context).onAvatarUpdated(groupId, true);
database.updateAvatar(groupId, avatar);
inputStream.close(); inputStream.close();
} }
} catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) { } catch (NonSuccessfulResponseCodeException | InvalidMessageException e) {
Log.w(TAG, e); Log.w(TAG, e);
} finally { } finally {
if (attachment != null) if (attachment != null)

View File

@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
@ -100,7 +101,7 @@ public class MultiDeviceGroupUpdateJob extends BaseJob {
out.write(new DeviceGroup(record.getId().getDecodedId(), out.write(new DeviceGroup(record.getId().getDecodedId(),
Optional.fromNullable(record.getTitle()), Optional.fromNullable(record.getTitle()),
members, members,
getAvatar(record.getAvatar()), getAvatar(record.getRecipientId()),
record.isActive(), record.isActive(),
expirationTimer, expirationTimer,
Optional.of(recipient.getColor().serialize()), Optional.of(recipient.getColor().serialize()),
@ -151,13 +152,13 @@ public class MultiDeviceGroupUpdateJob extends BaseJob {
} }
private Optional<SignalServiceAttachmentStream> getAvatar(@Nullable byte[] avatar) { private Optional<SignalServiceAttachmentStream> getAvatar(@NonNull RecipientId recipientId) throws IOException {
if (avatar == null) return Optional.absent(); if (!AvatarHelper.hasAvatar(context, recipientId)) return Optional.absent();
return Optional.of(SignalServiceAttachment.newStreamBuilder() return Optional.of(SignalServiceAttachment.newStreamBuilder()
.withStream(new ByteArrayInputStream(avatar)) .withStream(AvatarHelper.getAvatar(context, recipientId))
.withContentType("image/*") .withContentType("image/*")
.withLength(avatar.length) .withLength(AvatarHelper.getAvatarLength(context, recipientId))
.build()); .build());
} }

View File

@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.recipients.RecipientUtil;
@ -80,16 +81,16 @@ public class PushGroupUpdateJob extends BaseJob {
Optional<GroupRecord> record = groupDatabase.getGroup(groupId); Optional<GroupRecord> record = groupDatabase.getGroup(groupId);
SignalServiceAttachment avatar = null; SignalServiceAttachment avatar = null;
if (record == null) { if (record == null || !record.isPresent()) {
Log.w(TAG, "No information for group record info request: " + groupId.toString()); Log.w(TAG, "No information for group record info request: " + groupId.toString());
return; return;
} }
if (record.get().getAvatar() != null) { if (AvatarHelper.hasAvatar(context, record.get().getRecipientId())) {
avatar = SignalServiceAttachmentStream.newStreamBuilder() avatar = SignalServiceAttachmentStream.newStreamBuilder()
.withContentType("image/jpeg") .withContentType("image/jpeg")
.withStream(new ByteArrayInputStream(record.get().getAvatar())) .withStream(AvatarHelper.getAvatar(context, record.get().getRecipientId()))
.withLength(record.get().getAvatar().length) .withLength(AvatarHelper.getAvatarLength(context, record.get().getRecipientId()))
.build(); .build();
} }

View File

@ -97,17 +97,10 @@ public class RetrieveProfileAvatarJob extends BaseJob {
File downloadDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); File downloadDestination = File.createTempFile("avatar", "jpg", context.getCacheDir());
try { try {
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();
InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, MAX_PROFILE_SIZE_BYTES); InputStream avatarStream = receiver.retrieveProfileAvatar(profileAvatar, downloadDestination, profileKey, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
File decryptDestination = File.createTempFile("avatar", "jpg", context.getCacheDir());
try { AvatarHelper.setAvatar(context, recipient.getId(), avatarStream);
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()));
} catch (PushNetworkException e) { } catch (PushNetworkException e) {
if (e.getCause() instanceof NonSuccessfulResponseCodeException) { if (e.getCause() instanceof NonSuccessfulResponseCodeException) {
Log.w(TAG, "Removing profile avatar (no image available) for: " + recipient.getId().serialize()); Log.w(TAG, "Removing profile avatar (no image available) for: " + recipient.getId().serialize());

View File

@ -17,6 +17,7 @@ import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOptions; import org.thoughtcrime.securesms.TransportOptions;
import org.thoughtcrime.securesms.imageeditor.model.EditorModel; import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment; import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
import org.thoughtcrime.securesms.util.MediaUtil; 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 { 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_CAPTURE = "IMAGE_CAPTURE";
private static final String IMAGE_EDITOR = "IMAGE_EDITOR"; private static final String IMAGE_EDITOR = "IMAGE_EDITOR";

View File

@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
@ -64,7 +65,7 @@ public class AvatarMigrationJob extends MigrationJob {
Recipient recipient = Recipient.external(context, file.getName()); Recipient recipient = Recipient.external(context, file.getName());
byte[] data = Util.readFully(new FileInputStream(file)); byte[] data = Util.readFully(new FileInputStream(file));
AvatarHelper.setAvatar(context, recipient.getId(), data); AvatarHelper.setAvatar(context, recipient.getId(), new ByteArrayInputStream(data));
} else { } else {
Log.w(TAG, "Invalid file name! Can't migrate this file. It'll just get deleted."); Log.w(TAG, "Invalid file name! Can't migrate this file. It'll just get deleted.");
} }

View File

@ -6,81 +6,191 @@ import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.ByteUnit;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.LinkedList; import java.io.OutputStream;
import java.util.List; import java.util.Collections;
import java.util.Iterator;
public class AvatarHelper { 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"; private static final String AVATAR_DIRECTORY = "avatars";
public static InputStream getInputStreamFor(@NonNull Context context, @NonNull RecipientId recipientId) /**
throws IOException * Retrieves an iterable set of avatars. Only intended to be used during backup.
{ */
return new FileInputStream(getAvatarFile(context, recipientId)); public static Iterable<Avatar> getAvatars(@NonNull Context context) {
} File avatarDirectory = context.getDir(AVATAR_DIRECTORY, Context.MODE_PRIVATE);
public static List<File> getAvatarFiles(@NonNull Context context) {
File avatarDirectory = new File(context.getFilesDir(), AVATAR_DIRECTORY);
File[] results = avatarDirectory.listFiles(); File[] results = avatarDirectory.listFiles();
if (results == null) return new LinkedList<>(); if (results == null) {
else return Stream.of(results).toList(); return Collections.emptyList();
}
return () -> {
return new Iterator<Avatar>() {
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) { public static void delete(@NonNull Context context, @NonNull RecipientId recipientId) {
getAvatarFile(context, recipientId).delete(); getAvatarFile(context, recipientId).delete();
} }
public static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId) { /**
File avatarDirectory = new File(context.getFilesDir(), AVATAR_DIRECTORY); * Whether or not an avatar is present for the given recipient.
avatarDirectory.mkdirs(); */
public static boolean hasAvatar(@NonNull Context context, @NonNull RecipientId recipientId) {
return new File(avatarDirectory, new File(recipientId.serialize()).getName()); 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); delete(context, recipientId);
} else { return;
FileOutputStream out = new FileOutputStream(getAvatarFile(context, recipientId)); }
out.write(data);
out.close(); 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) { if (file.exists()) {
try { return file.lastModified();
FileInputStream stream = new FileInputStream(avatarFile);
return new StreamDetails(stream, MediaUtil.IMAGE_JPEG, avatarFile.length());
} catch (FileNotFoundException e) {
throw new AssertionError(e);
}
} else { } 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; 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;
}
}
} }

View File

@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays; import java.util.Arrays;
@ -76,10 +77,10 @@ class EditProfileRepository {
void getCurrentAvatar(@NonNull Consumer<byte[]> avatarConsumer) { void getCurrentAvatar(@NonNull Consumer<byte[]> avatarConsumer) {
RecipientId selfId = Recipient.self().getId(); RecipientId selfId = Recipient.self().getId();
if (AvatarHelper.getAvatarFile(context, selfId).exists() && AvatarHelper.getAvatarFile(context, selfId).length() > 0) { if (AvatarHelper.hasAvatar(context, selfId)) {
SimpleTask.run(() -> { SimpleTask.run(() -> {
try { try {
return Util.readFully(AvatarHelper.getInputStreamFor(context, selfId)); return Util.readFully(AvatarHelper.getAvatar(context, selfId));
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
return null; return null;
@ -106,7 +107,7 @@ class EditProfileRepository {
DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), profileName); DatabaseFactory.getRecipientDatabase(context).setProfileName(Recipient.self().getId(), profileName);
try { try {
AvatarHelper.setAvatar(context, Recipient.self().getId(), avatar); AvatarHelper.setAvatar(context, Recipient.self().getId(), avatar != null ? new ByteArrayInputStream(avatar) : null);
} catch (IOException e) { } catch (IOException e) {
return UploadResult.ERROR_FILE_IO; return UploadResult.ERROR_FILE_IO;
} }

View File

@ -201,7 +201,7 @@ public final class LiveRecipient {
title = unnamedGroupName; title = unnamedGroupName;
} }
if (groupRecord.get().getAvatar() != null && groupRecord.get().getAvatar().length > 0) { if (groupRecord.get().hasAvatar()) {
avatarId = Optional.of(groupRecord.get().getAvatarId()); avatarId = Optional.of(groupRecord.get().getAvatarId());
} }

View File

@ -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()) { if (directory == null || !directory.exists() || !directory.isDirectory()) {
return; return false;
} }
deleteDirectoryContents(directory); deleteDirectoryContents(directory);
directory.delete(); return directory.delete();
} }
} }

View File

@ -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 { try {
closeable.close(); closeable.close();
} catch (IOException e) { } catch (IOException e) {
@ -607,4 +609,12 @@ public class Util {
return concat; return concat;
} }
public static boolean isLong(String value) {
try {
Long.parseLong(value);
return true;
} catch (NumberFormatException e) {
return false;
}
}
} }

View File

@ -113,7 +113,7 @@ public class SignalServiceMessageReceiver {
* @throws IOException * @throws IOException
* @throws InvalidMessageException * @throws InvalidMessageException
*/ */
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, int maxSizeBytes) public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes)
throws IOException, InvalidMessageException throws IOException, InvalidMessageException
{ {
return retrieveAttachment(pointer, destination, maxSizeBytes, null); return retrieveAttachment(pointer, destination, maxSizeBytes, null);
@ -142,7 +142,7 @@ public class SignalServiceMessageReceiver {
return socket.retrieveProfileByUsername(username, unidentifiedAccess); 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 throws IOException
{ {
socket.retrieveProfileAvatar(path, destination, maxSizeBytes); socket.retrieveProfileAvatar(path, destination, maxSizeBytes);
@ -162,7 +162,7 @@ public class SignalServiceMessageReceiver {
* @throws IOException * @throws IOException
* @throws InvalidMessageException * @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 throws IOException, InvalidMessageException
{ {
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!"); if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");

View File

@ -498,7 +498,7 @@ public class PushServiceSocket {
makeServiceRequest(SIGNED_PREKEY_PATH, "PUT", JsonUtil.toJson(signedPreKeyEntity)); 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 throws NonSuccessfulResponseCodeException, PushNetworkException
{ {
downloadFromCdn(destination, String.format(Locale.US, ATTACHMENT_DOWNLOAD_PATH, attachmentId), maxSizeBytes, listener); 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 throws NonSuccessfulResponseCodeException, PushNetworkException
{ {
downloadFromCdn(destination, path, maxSizeBytes, null); downloadFromCdn(destination, path, maxSizeBytes, null);
@ -874,7 +874,7 @@ public class PushServiceSocket {
return new Pair<>(id, digest); 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 throws PushNetworkException, NonSuccessfulResponseCodeException
{ {
try (FileOutputStream outputStream = new FileOutputStream(destination, true)) { 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 throws PushNetworkException, NonSuccessfulResponseCodeException
{ {
ConnectionHolder connectionHolder = getRandom(cdnClients, random); ConnectionHolder connectionHolder = getRandom(cdnClients, random);