That fuck shit the fascists are using
1package org.tm.archive.util;
2
3
4import android.Manifest;
5import android.content.Context;
6import android.content.Intent;
7import android.net.Uri;
8import android.os.Build;
9
10import androidx.annotation.NonNull;
11import androidx.annotation.Nullable;
12import androidx.annotation.RequiresApi;
13import androidx.annotation.VisibleForTesting;
14import androidx.documentfile.provider.DocumentFile;
15
16import org.signal.core.util.logging.Log;
17import org.signal.libsignal.protocol.util.ByteUtil;
18import org.tm.archive.R;
19import org.tm.archive.backup.BackupPassphrase;
20import org.tm.archive.database.NoExternalStorageException;
21import org.tm.archive.dependencies.ApplicationDependencies;
22import org.tm.archive.keyvalue.SignalStore;
23import org.tm.archive.permissions.Permissions;
24
25import java.io.File;
26import java.security.SecureRandom;
27import java.util.ArrayList;
28import java.util.Calendar;
29import java.util.Collections;
30import java.util.List;
31import java.util.Locale;
32import java.util.Objects;
33
34public class BackupUtil {
35
36 private static final String TAG = Log.tag(BackupUtil.class);
37
38 public static final int PASSPHRASE_LENGTH = 30;
39
40 public static @NonNull String getLastBackupTime(@NonNull Context context, @NonNull Locale locale) {
41 try {
42 BackupInfo backup = getLatestBackup();
43
44 if (backup == null) return context.getString(R.string.BackupUtil_never);
45 else return DateUtils.getExtendedRelativeTimeSpanString(context, locale, backup.getTimestamp());
46 } catch (NoExternalStorageException e) {
47 Log.w(TAG, e);
48 return context.getString(R.string.BackupUtil_unknown);
49 }
50 }
51
52 public static boolean isUserSelectionRequired(@NonNull Context context) {
53 return Build.VERSION.SDK_INT >= 29 && !Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE);
54 }
55
56 public static boolean canUserAccessBackupDirectory(@NonNull Context context) {
57 if (isUserSelectionRequired(context)) {
58 Uri backupDirectoryUri = SignalStore.settings().getSignalBackupDirectory();
59 if (backupDirectoryUri == null) {
60 return false;
61 }
62
63 DocumentFile backupDirectory = DocumentFile.fromTreeUri(context, backupDirectoryUri);
64 return backupDirectory != null && backupDirectory.exists() && backupDirectory.canRead() && backupDirectory.canWrite();
65 } else {
66 return Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE);
67 }
68 }
69
70 public static @Nullable BackupInfo getLatestBackup() throws NoExternalStorageException {
71 List<BackupInfo> backups = getAllBackupsNewestFirst();
72
73 return backups.isEmpty() ? null : backups.get(0);
74 }
75
76 public static void deleteAllBackups() {
77 Log.i(TAG, "Deleting all backups");
78
79 try {
80 List<BackupInfo> backups = getAllBackupsNewestFirst();
81
82 for (BackupInfo backup : backups) {
83 backup.delete();
84 }
85 } catch (NoExternalStorageException e) {
86 Log.w(TAG, e);
87 }
88 }
89
90 public static void deleteOldBackups() {
91 Log.i(TAG, "Deleting older backups");
92
93 try {
94 List<BackupInfo> backups = getAllBackupsNewestFirst();
95
96 for (int i = 2; i < backups.size(); i++) {
97 backups.get(i).delete();
98 }
99 } catch (NoExternalStorageException e) {
100 Log.w(TAG, e);
101 }
102 }
103
104 public static void disableBackups(@NonNull Context context) {
105 BackupPassphrase.set(context, null);
106 SignalStore.settings().setBackupEnabled(false);
107 BackupUtil.deleteAllBackups();
108
109 if (BackupUtil.isUserSelectionRequired(context)) {
110 Uri backupLocationUri = SignalStore.settings().getSignalBackupDirectory();
111
112 if (backupLocationUri == null) {
113 return;
114 }
115
116 SignalStore.settings().clearSignalBackupDirectory();
117
118 try {
119 context.getContentResolver()
120 .releasePersistableUriPermission(Objects.requireNonNull(backupLocationUri),
121 Intent.FLAG_GRANT_READ_URI_PERMISSION |
122 Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
123 } catch (SecurityException e) {
124 Log.w(TAG, "Could not release permissions", e);
125 }
126 }
127 }
128
129 private static List<BackupInfo> getAllBackupsNewestFirst() throws NoExternalStorageException {
130 if (isUserSelectionRequired(ApplicationDependencies.getApplication())) {
131 return getAllBackupsNewestFirstApi29();
132 } else {
133 return getAllBackupsNewestFirstLegacy();
134 }
135 }
136
137 @RequiresApi(29)
138 private static List<BackupInfo> getAllBackupsNewestFirstApi29() {
139 Uri backupDirectoryUri = SignalStore.settings().getSignalBackupDirectory();
140 if (backupDirectoryUri == null) {
141 Log.i(TAG, "Backup directory is not set. Returning an empty list.");
142 return Collections.emptyList();
143 }
144
145 DocumentFile backupDirectory = DocumentFile.fromTreeUri(ApplicationDependencies.getApplication(), backupDirectoryUri);
146 if (backupDirectory == null || !backupDirectory.exists() || !backupDirectory.canRead()) {
147 Log.w(TAG, "Backup directory is inaccessible. Returning an empty list.");
148 return Collections.emptyList();
149 }
150
151 DocumentFile[] files = backupDirectory.listFiles();
152 List<BackupInfo> backups = new ArrayList<>(files.length);
153
154 for (DocumentFile file : files) {
155 if (file.isFile() && file.getName() != null && file.getName().endsWith(".backup")) {
156 long backupTimestamp = getBackupTimestamp(file.getName());
157
158 if (backupTimestamp != -1) {
159 backups.add(new BackupInfo(backupTimestamp, file.length(), file.getUri()));
160 }
161 }
162 }
163
164 Collections.sort(backups, (a, b) -> Long.compare(b.timestamp, a.timestamp));
165
166 return backups;
167 }
168
169 public static @Nullable BackupInfo getBackupInfoFromSingleUri(@NonNull Context context, @NonNull Uri singleUri) throws BackupFileException {
170 DocumentFile documentFile = Objects.requireNonNull(DocumentFile.fromSingleUri(context, singleUri));
171
172 return getBackupInfoFromSingleDocumentFile(documentFile);
173 }
174
175 @VisibleForTesting
176 static @Nullable BackupInfo getBackupInfoFromSingleDocumentFile(@NonNull DocumentFile documentFile) throws BackupFileException {
177 BackupFileState backupFileState = getBackupFileState(documentFile);
178
179 if (backupFileState.isSuccess()) {
180 long backupTimestamp = getBackupTimestamp(Objects.requireNonNull(documentFile.getName()));
181 return new BackupInfo(backupTimestamp, documentFile.length(), documentFile.getUri());
182 } else {
183 Log.w(TAG, "Could not load backup info.");
184 backupFileState.throwIfError();
185 return null;
186 }
187 }
188
189 private static List<BackupInfo> getAllBackupsNewestFirstLegacy() throws NoExternalStorageException {
190 File backupDirectory = StorageUtil.getOrCreateBackupDirectory();
191 File[] files = backupDirectory.listFiles();
192 List<BackupInfo> backups = new ArrayList<>(files.length);
193
194 for (File file : files) {
195 if (file.isFile() && file.getAbsolutePath().endsWith(".backup")) {
196 long backupTimestamp = getBackupTimestamp(file.getName());
197
198 if (backupTimestamp != -1) {
199 backups.add(new BackupInfo(backupTimestamp, file.length(), Uri.fromFile(file)));
200 }
201 }
202 }
203
204 Collections.sort(backups, (a, b) -> Long.compare(b.timestamp, a.timestamp));
205
206 return backups;
207 }
208
209 public static @NonNull String[] generateBackupPassphrase() {
210 String[] result = new String[6];
211 byte[] random = new byte[30];
212
213 new SecureRandom().nextBytes(random);
214
215 for (int i=0;i<30;i+=5) {
216 result[i/5] = String.format(Locale.ENGLISH, "%05d", ByteUtil.byteArray5ToLong(random, i) % 100000);
217 }
218
219 return result;
220 }
221
222 public static boolean hasBackupFiles(@NonNull Context context) {
223 if (Permissions.hasAll(context, Manifest.permission.READ_EXTERNAL_STORAGE)) {
224 try {
225 File directory = StorageUtil.getBackupDirectory();
226
227 if (directory.exists() && directory.isDirectory()) {
228 File[] files = directory.listFiles();
229 return files != null && files.length > 0;
230 } else {
231 return false;
232 }
233 } catch (NoExternalStorageException e) {
234 Log.w(TAG, "Failed to read storage!", e);
235 return false;
236 }
237 } else {
238 return false;
239 }
240 }
241
242 private static long getBackupTimestamp(@NonNull String backupName) {
243 String[] prefixSuffix = backupName.split("[.]");
244
245 if (prefixSuffix.length == 2) {
246 String[] parts = prefixSuffix[0].split("\\-");
247
248 if (parts.length == 7) {
249 try {
250 Calendar calendar = Calendar.getInstance();
251 calendar.set(Calendar.YEAR, Integer.parseInt(parts[1]));
252 calendar.set(Calendar.MONTH, Integer.parseInt(parts[2]) - 1);
253 calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(parts[3]));
254 calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(parts[4]));
255 calendar.set(Calendar.MINUTE, Integer.parseInt(parts[5]));
256 calendar.set(Calendar.SECOND, Integer.parseInt(parts[6]));
257 calendar.set(Calendar.MILLISECOND, 0);
258
259 return calendar.getTimeInMillis();
260 } catch (NumberFormatException e) {
261 Log.w(TAG, e);
262 }
263 }
264 }
265
266 return -1;
267 }
268
269 private static BackupFileState getBackupFileState(@NonNull DocumentFile documentFile) {
270 if (!documentFile.exists()) {
271 return BackupFileState.NOT_FOUND;
272 } else if (!documentFile.canRead()) {
273 return BackupFileState.NOT_READABLE;
274 } else if (Util.isEmpty(documentFile.getName()) || !documentFile.getName().endsWith(".backup")) {
275 return BackupFileState.UNSUPPORTED_FILE_EXTENSION;
276 } else {
277 return BackupFileState.READABLE;
278 }
279 }
280
281 /**
282 * Describes the validity of a backup file.
283 */
284 public enum BackupFileState {
285 READABLE("The document at the specified Uri looks like a readable backup."),
286 NOT_FOUND("The document at the specified Uri cannot be found."),
287 NOT_READABLE("The document at the specified Uri cannot be read."),
288 UNSUPPORTED_FILE_EXTENSION("The document at the specified Uri has an unsupported file extension.");
289
290 private final String message;
291
292 BackupFileState(String message) {
293 this.message = message;
294 }
295
296 public boolean isSuccess() {
297 return this == READABLE;
298 }
299
300 public void throwIfError() throws BackupFileException {
301 if (!isSuccess()) {
302 throw new BackupFileException(this);
303 }
304 }
305 }
306
307 /**
308 * Wrapping exception for a non-successful BackupFileState.
309 */
310 public static class BackupFileException extends Exception {
311
312 private final BackupFileState state;
313
314 BackupFileException(BackupFileState backupFileState) {
315 super(backupFileState.message);
316 this.state = backupFileState;
317 }
318
319 public @NonNull BackupFileState getState() {
320 return state;
321 }
322 }
323
324 public static class BackupInfo {
325
326 private final long timestamp;
327 private final long size;
328 private final Uri uri;
329
330 BackupInfo(long timestamp, long size, Uri uri) {
331 this.timestamp = timestamp;
332 this.size = size;
333 this.uri = uri;
334 }
335
336 public long getTimestamp() {
337 return timestamp;
338 }
339
340 public long getSize() {
341 return size;
342 }
343
344 public Uri getUri() {
345 return uri;
346 }
347
348 private void delete() {
349 File file = new File(Objects.requireNonNull(uri.getPath()));
350
351 if (file.exists()) {
352 Log.i(TAG, "Deleting File: " + file.getAbsolutePath());
353
354 if (!file.delete()) {
355 Log.w(TAG, "Delete failed: " + file.getAbsolutePath());
356 }
357 } else {
358 DocumentFile document = DocumentFile.fromSingleUri(ApplicationDependencies.getApplication(), uri);
359 if (document != null && document.exists()) {
360 Log.i(TAG, "Deleting DocumentFile: " + uri);
361
362 if (!document.delete()) {
363 Log.w(TAG, "Delete failed: " + uri);
364 }
365 }
366 }
367 }
368 }
369}