That fuck shit the fascists are using
at master 369 lines 12 kB view raw
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}