That fuck shit the fascists are using
1package org.tm.archive.profiles;
2
3
4import android.content.Context;
5
6import androidx.annotation.NonNull;
7import androidx.annotation.Nullable;
8
9import org.signal.core.util.StreamUtil;
10import org.signal.core.util.logging.Log;
11import org.tm.archive.crypto.AttachmentSecret;
12import org.tm.archive.crypto.AttachmentSecretProvider;
13import org.tm.archive.crypto.ModernDecryptingPartInputStream;
14import org.tm.archive.crypto.ModernEncryptingPartOutputStream;
15import org.tm.archive.database.model.ProfileAvatarFileDetails;
16import org.tm.archive.keyvalue.SignalStore;
17import org.tm.archive.recipients.Recipient;
18import org.tm.archive.recipients.RecipientId;
19import org.tm.archive.util.ByteUnit;
20import org.tm.archive.util.MediaUtil;
21import org.whispersystems.signalservice.api.util.StreamDetails;
22
23import java.io.File;
24import java.io.IOException;
25import java.io.InputStream;
26import java.io.OutputStream;
27import java.util.Collections;
28import java.util.Iterator;
29
30public class AvatarHelper {
31
32 private static final String TAG = Log.tag(AvatarHelper.class);
33
34 public static int AVATAR_DIMENSIONS = 1024;
35 public static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = ByteUnit.MEGABYTES.toBytes(10);
36
37 private static final String AVATAR_DIRECTORY = "avatars";
38
39 private static File avatarDirectory;
40
41 private static File getAvatarDirectory(@NonNull Context context) {
42 if (avatarDirectory == null) {
43 avatarDirectory = context.getDir(AVATAR_DIRECTORY, Context.MODE_PRIVATE);
44 }
45 return avatarDirectory;
46 }
47
48 public static long getAvatarCount(@NonNull Context context) {
49 File avatarDirectory = getAvatarDirectory(context);
50 String[] results = avatarDirectory.list();
51
52 return results == null ? 0 : results.length;
53 }
54
55 /**
56 * Retrieves an iterable set of avatars. Only intended to be used during backup.
57 */
58 public static Iterable<Avatar> getAvatars(@NonNull Context context) {
59 File avatarDirectory = getAvatarDirectory(context);
60 File[] results = avatarDirectory.listFiles();
61
62 if (results == null) {
63 return Collections.emptyList();
64 }
65
66 return () -> {
67 return new Iterator<Avatar>() {
68 int i = 0;
69 @Override
70 public boolean hasNext() {
71 return i < results.length;
72 }
73
74 @Override
75 public Avatar next() {
76 File file = results[i];
77 try {
78 return new Avatar(getAvatar(context, RecipientId.from(file.getName())),
79 file.getName(),
80 ModernEncryptingPartOutputStream.getPlaintextLength(file.length()));
81 } catch (IOException e) {
82 return null;
83 } finally {
84 i++;
85 }
86 }
87 };
88 };
89 }
90
91 /**
92 * Deletes and avatar.
93 */
94 public static void delete(@NonNull Context context, @NonNull RecipientId recipientId) {
95 getAvatarFile(context, recipientId).delete();
96 Recipient.live(recipientId).refresh();
97 }
98
99 /**
100 * Whether or not an avatar is present for the given recipient.
101 */
102 public static boolean hasAvatar(@NonNull Context context, @NonNull RecipientId recipientId) {
103 File avatarFile = getAvatarFile(context, recipientId);
104 return avatarFile.exists() && avatarFile.length() > 0;
105 }
106
107 public static @NonNull ProfileAvatarFileDetails getAvatarFileDetails(@NonNull Context context, @NonNull RecipientId recipientId) {
108 File avatarFile = getAvatarFile(context, recipientId);
109 if (avatarFile.exists() && avatarFile.length() > 0) {
110 return new ProfileAvatarFileDetails(avatarFile.hashCode(), avatarFile.lastModified());
111 }
112 return ProfileAvatarFileDetails.NO_DETAILS;
113 }
114
115 /**
116 * Retrieves a stream for an avatar. If there is no avatar, the stream will likely throw an
117 * IOException. It is recommended to call {@link #hasAvatar(Context, RecipientId)} first.
118 */
119 public static @NonNull InputStream getAvatar(@NonNull Context context, @NonNull RecipientId recipientId) throws IOException {
120 AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
121 File avatarFile = getAvatarFile(context, recipientId);
122
123 return ModernDecryptingPartInputStream.createFor(attachmentSecret, avatarFile, 0);
124 }
125
126 public static byte[] getAvatarBytes(@NonNull Context context, @NonNull RecipientId recipientId) throws IOException {
127 return hasAvatar(context, recipientId) ? StreamUtil.readFully(getAvatar(context, recipientId))
128 : null;
129 }
130
131 /**
132 * Returns the size of the avatar on disk.
133 */
134 public static long getAvatarLength(@NonNull Context context, @NonNull RecipientId recipientId) {
135 return ModernEncryptingPartOutputStream.getPlaintextLength(getAvatarFile(context, recipientId).length());
136 }
137
138 /**
139 * Saves the contents of the input stream as the avatar for the specified recipient. If you pass
140 * in null for the stream, the avatar will be deleted.
141 */
142 public static void setAvatar(@NonNull Context context, @NonNull RecipientId recipientId, @Nullable InputStream inputStream)
143 throws IOException
144 {
145 if (inputStream == null) {
146 delete(context, recipientId);
147 return;
148 }
149
150 OutputStream outputStream = null;
151 try {
152 outputStream = getOutputStream(context, recipientId, false);
153 StreamUtil.copy(inputStream, outputStream);
154 } finally {
155 StreamUtil.close(outputStream);
156 }
157 }
158
159 public static void setSyncAvatar(@NonNull Context context, @NonNull RecipientId recipientId, @Nullable InputStream inputStream)
160 throws IOException
161 {
162 if (inputStream == null) {
163 delete(context, recipientId);
164 return;
165 }
166
167 OutputStream outputStream = null;
168 try {
169 outputStream = getOutputStream(context, recipientId, true);
170 StreamUtil.copy(inputStream, outputStream);
171 } finally {
172 StreamUtil.close(outputStream);
173 }
174 }
175
176 /**
177 * Retrieves an output stream you can write to that will be saved as the avatar for the specified
178 * recipient. Only intended to be used for backup. Otherwise, use {@link #setAvatar(Context, RecipientId, InputStream)}.
179 */
180 public static @NonNull OutputStream getOutputStream(@NonNull Context context, @NonNull RecipientId recipientId, boolean isSyncAvatar) throws IOException {
181 AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
182 File targetFile = getAvatarFile(context, recipientId, isSyncAvatar);
183 return ModernEncryptingPartOutputStream.createFor(attachmentSecret, targetFile, true).second;
184 }
185
186 /**
187 * Returns a {@link StreamDetails} for the local user's own avatar, or null if one does not exist.
188 */
189 public static @Nullable StreamDetails getSelfProfileAvatarStream(@NonNull Context context) {
190 RecipientId selfId = Recipient.self().getId();
191
192 if (!hasAvatar(context, selfId)) {
193 return null;
194 }
195
196 try {
197 InputStream stream = getAvatar(context, selfId);
198 return new StreamDetails(stream, MediaUtil.IMAGE_JPEG, getAvatarLength(context, selfId));
199 } catch (IOException e) {
200 Log.w(TAG, "Failed to read own avatar!", e);
201 return null;
202 }
203 }
204
205 public static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId) {
206 File profileAvatar = getAvatarFile(context, recipientId, false);
207 boolean profileAvatarExists = profileAvatar.exists() && profileAvatar.length() > 0;
208 File syncAvatar = getAvatarFile(context, recipientId, true);
209 boolean syncAvatarExists = syncAvatar.exists() && syncAvatar.length() > 0;
210
211 if (SignalStore.settings().isPreferSystemContactPhotos() && syncAvatarExists) {
212 return syncAvatar;
213 } else if (profileAvatarExists) {
214 return profileAvatar;
215 } else if (syncAvatarExists) {
216 return syncAvatar;
217 }
218
219 return profileAvatar;
220 }
221
222 private static @NonNull File getAvatarFile(@NonNull Context context, @NonNull RecipientId recipientId, boolean isSyncAvatar) {
223 return new File(getAvatarDirectory(context), recipientId.serialize() + (isSyncAvatar ? "-sync" : ""));
224 }
225
226 public static class Avatar {
227 private final InputStream inputStream;
228 private final String filename;
229 private final long length;
230
231 public Avatar(@NonNull InputStream inputStream, @NonNull String filename, long length) {
232 this.inputStream = inputStream;
233 this.filename = filename;
234 this.length = length;
235 }
236
237 public @NonNull InputStream getInputStream() {
238 return inputStream;
239 }
240
241 public @NonNull String getFilename() {
242 return filename;
243 }
244
245 public long getLength() {
246 return length;
247 }
248 }
249}