1package eu.siacs.conversations.utils;
2
3import android.content.Context;
4import android.content.UriPermission;
5import android.net.Uri;
6import android.os.Build;
7import android.provider.DocumentsContract;
8import android.util.Log;
9import androidx.documentfile.provider.DocumentFile;
10import com.google.common.collect.Collections2;
11import com.google.common.collect.ComparisonChain;
12import com.google.common.collect.ImmutableList;
13import com.google.common.collect.ImmutableSet;
14import com.google.common.collect.Ordering;
15import com.google.common.util.concurrent.Futures;
16import com.google.common.util.concurrent.ListenableFuture;
17import eu.siacs.conversations.Config;
18import eu.siacs.conversations.R;
19import eu.siacs.conversations.persistance.DatabaseBackend;
20import eu.siacs.conversations.persistance.FileBackend;
21import eu.siacs.conversations.services.QuickConversationsService;
22import eu.siacs.conversations.worker.ExportBackupWorker;
23import eu.siacs.conversations.xmpp.Jid;
24import java.io.DataInputStream;
25import java.io.File;
26import java.io.FileInputStream;
27import java.io.FileNotFoundException;
28import java.io.IOException;
29import java.io.InputStream;
30import java.util.ArrayList;
31import java.util.List;
32import java.util.concurrent.ExecutorService;
33import java.util.concurrent.Executors;
34
35public class BackupFile implements Comparable<BackupFile> {
36
37 private static final ExecutorService BACKUP_FILE_READER_EXECUTOR =
38 Executors.newSingleThreadExecutor();
39
40 private final Uri uri;
41 private final BackupFileHeader header;
42
43 private BackupFile(Uri uri, BackupFileHeader header) {
44 this.uri = uri;
45 this.header = header;
46 }
47
48 public static ListenableFuture<BackupFile> readAsync(final Context context, final Uri uri) {
49 return Futures.submit(() -> read(context, uri), BACKUP_FILE_READER_EXECUTOR);
50 }
51
52 private static BackupFile read(final File file) throws IOException {
53 final FileInputStream fileInputStream = new FileInputStream(file);
54 final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
55 BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
56 fileInputStream.close();
57 return new BackupFile(Uri.fromFile(file), backupFileHeader);
58 }
59
60 public static BackupFile read(final Context context, final Uri uri) throws IOException {
61 final InputStream inputStream = context.getContentResolver().openInputStream(uri);
62 if (inputStream == null) {
63 throw new FileNotFoundException();
64 }
65 final DataInputStream dataInputStream = new DataInputStream(inputStream);
66 final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
67 inputStream.close();
68 return new BackupFile(uri, backupFileHeader);
69 }
70
71 public BackupFileHeader getHeader() {
72 return header;
73 }
74
75 public Uri getUri() {
76 return uri;
77 }
78
79 public static ListenableFuture<List<BackupFile>> listAsync(final Context context) {
80 return Futures.submit(() -> list(context), BACKUP_FILE_READER_EXECUTOR);
81 }
82
83 private static List<BackupFile> list(final Context context) {
84 final var database = DatabaseBackend.getInstance(context);
85 final List<Jid> accounts = database.getAccountJids(false);
86 final var backupFiles = new ImmutableList.Builder<BackupFile>();
87 final var apps =
88 ImmutableSet.of("Conversations", "Quicksy", context.getString(R.string.app_name));
89
90 final var uriPermissions = context.getContentResolver().getPersistedUriPermissions();
91
92 for (final UriPermission uriPermission : uriPermissions) {
93 final var uri = uriPermission.getUri();
94 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
95 && DocumentsContract.isTreeUri(uri)) {
96 Log.d(Config.LOGTAG, "looking for backups in " + uri);
97 final var tree = DocumentFile.fromTreeUri(context, uriPermission.getUri());
98 final var files = tree == null ? new DocumentFile[0] : tree.listFiles();
99 for (final DocumentFile documentFile : files) {
100 final var name = documentFile.getName();
101 if (documentFile.isFile()
102 && (ExportBackupWorker.MIME_TYPE.equals(documentFile.getType())
103 || (name != null && name.endsWith(".ceb")))) {
104 try {
105 final BackupFile backupFile =
106 BackupFile.read(context, documentFile.getUri());
107 if (accounts.contains(backupFile.getHeader().getJid())) {
108 Log.d(
109 Config.LOGTAG,
110 "skipping backup for " + backupFile.getHeader().getJid());
111 } else {
112 backupFiles.add(backupFile);
113 }
114 } catch (final IOException
115 | IllegalArgumentException
116 | BackupFileHeader.OutdatedBackupFileVersion e) {
117 Log.d(Config.LOGTAG, "unable to read backup file ", e);
118 }
119 }
120 }
121 }
122 }
123
124 final List<File> directories = new ArrayList<>();
125 for (final String app : apps) {
126 directories.add(FileBackend.getLegacyBackupDirectory(app));
127 }
128 if (uriPermissions.isEmpty()) {
129 Log.d(
130 Config.LOGTAG,
131 "including default directory since no uri permissions have been granted");
132 directories.add(FileBackend.getBackupDirectory(context));
133 }
134 for (final File directory : directories) {
135 if (!directory.exists() || !directory.isDirectory()) {
136 Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
137 continue;
138 }
139 final File[] files = directory.listFiles();
140 if (files == null) {
141 continue;
142 }
143 Log.d(Config.LOGTAG, "looking for backups in " + directory);
144 for (final File file : files) {
145 if (file.isFile() && file.getName().endsWith(".ceb")) {
146 try {
147 final BackupFile backupFile = BackupFile.read(file);
148 if (accounts.contains(backupFile.getHeader().getJid())) {
149 Log.d(
150 Config.LOGTAG,
151 "skipping backup for " + backupFile.getHeader().getJid());
152 } else {
153 backupFiles.add(backupFile);
154 }
155 } catch (final IOException
156 | IllegalArgumentException
157 | BackupFileHeader.OutdatedBackupFileVersion e) {
158 Log.d(Config.LOGTAG, "unable to read backup file ", e);
159 }
160 }
161 }
162 }
163 final var list = backupFiles.build();
164 if (QuickConversationsService.isQuicksy()) {
165 return Ordering.natural()
166 .immutableSortedCopy(
167 Collections2.filter(
168 list,
169 b ->
170 b.header
171 .getJid()
172 .getDomain()
173 .equals(Config.QUICKSY_DOMAIN)));
174 }
175 return Ordering.natural().immutableSortedCopy(backupFiles.build());
176 }
177
178 @Override
179 public int compareTo(final BackupFile o) {
180 return ComparisonChain.start()
181 .compare(header.getJid(), o.header.getJid())
182 .compare(o.header.getTimestamp(), header.getTimestamp())
183 .result();
184 }
185}