BackupFile.java

  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}