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

@@ -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<File> 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<Avatar> 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<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) {
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;
}
}
}

View File

@@ -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<byte[]> 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;
}