2017-08-16 06:03:31 +02:00
|
|
|
package org.thoughtcrime.securesms.profiles;
|
|
|
|
|
|
|
|
|
|
|
|
import android.content.Context;
|
2020-02-11 00:40:22 +01:00
|
|
|
|
2019-06-05 21:47:14 +02:00
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import androidx.annotation.Nullable;
|
2017-08-16 06:03:31 +02:00
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
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;
|
2020-02-11 00:40:22 +01:00
|
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
2019-09-23 17:37:01 +02:00
|
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
2020-03-26 20:38:27 +01:00
|
|
|
import org.thoughtcrime.securesms.util.ByteUnit;
|
2020-02-11 00:40:22 +01:00
|
|
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
2020-03-26 20:38:27 +01:00
|
|
|
import org.thoughtcrime.securesms.util.Util;
|
2020-02-11 00:40:22 +01:00
|
|
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
2017-08-16 06:03:31 +02:00
|
|
|
|
|
|
|
import java.io.File;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.InputStream;
|
2020-03-26 20:38:27 +01:00
|
|
|
import java.io.OutputStream;
|
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.Iterator;
|
2017-08-16 06:03:31 +02:00
|
|
|
|
|
|
|
public class AvatarHelper {
|
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
private static final String TAG = Log.tag(AvatarHelper.class);
|
2017-08-16 06:03:31 +02:00
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
public static int AVATAR_DIMENSIONS = 1024;
|
|
|
|
public static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = ByteUnit.MEGABYTES.toBytes(10);
|
2017-08-16 06:03:31 +02:00
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
private static final String AVATAR_DIRECTORY = "avatars";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
2018-03-19 22:10:21 +01:00
|
|
|
File[] results = avatarDirectory.listFiles();
|
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
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++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
2018-03-19 22:10:21 +01:00
|
|
|
}
|
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
/**
|
|
|
|
* Deletes and avatar.
|
|
|
|
*/
|
2019-09-23 17:37:01 +02:00
|
|
|
public static void delete(@NonNull Context context, @NonNull RecipientId recipientId) {
|
|
|
|
getAvatarFile(context, recipientId).delete();
|
2017-08-16 06:03:31 +02:00
|
|
|
}
|
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
/**
|
|
|
|
* 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();
|
|
|
|
}
|
2017-08-16 06:03:31 +02:00
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
/**
|
|
|
|
* 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());
|
2017-08-16 06:03:31 +02:00
|
|
|
}
|
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
/**
|
|
|
|
* 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
|
2017-08-16 06:03:31 +02:00
|
|
|
{
|
2020-03-26 20:38:27 +01:00
|
|
|
if (inputStream == null) {
|
2019-09-23 17:37:01 +02:00
|
|
|
delete(context, recipientId);
|
2020-03-26 20:38:27 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
OutputStream outputStream = null;
|
|
|
|
try {
|
|
|
|
outputStream = getOutputStream(context, recipientId);
|
|
|
|
Util.copy(inputStream, outputStream);
|
|
|
|
} finally {
|
|
|
|
Util.close(outputStream);
|
2017-08-16 06:03:31 +02:00
|
|
|
}
|
|
|
|
}
|
2020-02-11 00:40:22 +01:00
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
/**
|
|
|
|
* 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;
|
2020-02-11 00:40:22 +01:00
|
|
|
}
|
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
/**
|
|
|
|
* 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 (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.
|
|
|
|
*/
|
2020-02-11 00:40:22 +01:00
|
|
|
public static @Nullable StreamDetails getSelfProfileAvatarStream(@NonNull Context context) {
|
2020-03-26 20:38:27 +01:00
|
|
|
RecipientId selfId = Recipient.self().getId();
|
2020-02-11 00:40:22 +01:00
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
if (!hasAvatar(context, selfId)) {
|
|
|
|
return null;
|
|
|
|
}
|
2020-02-11 00:40:22 +01:00
|
|
|
|
2020-03-26 20:38:27 +01:00
|
|
|
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);
|
2020-02-11 00:40:22 +01:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2020-03-26 20:38:27 +01:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2017-08-16 06:03:31 +02:00
|
|
|
}
|