ImportBackupService.java

  1package eu.siacs.conversations.services;
  2
  3import android.app.NotificationManager;
  4import android.app.PendingIntent;
  5import android.app.Service;
  6import android.content.Context;
  7import android.content.Intent;
  8import android.database.Cursor;
  9import android.database.sqlite.SQLiteDatabase;
 10import android.net.Uri;
 11import android.os.Binder;
 12import android.os.IBinder;
 13import android.support.v4.app.NotificationCompat;
 14import android.util.Log;
 15
 16import java.io.BufferedReader;
 17import java.io.DataInputStream;
 18import java.io.File;
 19import java.io.FileInputStream;
 20import java.io.FileNotFoundException;
 21import java.io.IOException;
 22import java.io.InputStream;
 23import java.io.InputStreamReader;
 24import java.util.ArrayList;
 25import java.util.Arrays;
 26import java.util.Collections;
 27import java.util.HashSet;
 28import java.util.List;
 29import java.util.Set;
 30import java.util.WeakHashMap;
 31import java.util.concurrent.atomic.AtomicBoolean;
 32import java.util.zip.GZIPInputStream;
 33import java.util.zip.ZipException;
 34
 35import javax.crypto.BadPaddingException;
 36import javax.crypto.Cipher;
 37import javax.crypto.CipherInputStream;
 38import javax.crypto.spec.IvParameterSpec;
 39import javax.crypto.spec.SecretKeySpec;
 40
 41import eu.siacs.conversations.Config;
 42import eu.siacs.conversations.R;
 43import eu.siacs.conversations.persistance.DatabaseBackend;
 44import eu.siacs.conversations.persistance.FileBackend;
 45import eu.siacs.conversations.ui.ManageAccountActivity;
 46import eu.siacs.conversations.utils.BackupFileHeader;
 47import eu.siacs.conversations.utils.Compatibility;
 48import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
 49import rocks.xmpp.addr.Jid;
 50
 51import static eu.siacs.conversations.services.ExportBackupService.CIPHERMODE;
 52import static eu.siacs.conversations.services.ExportBackupService.KEYTYPE;
 53import static eu.siacs.conversations.services.ExportBackupService.PROVIDER;
 54
 55public class ImportBackupService extends Service {
 56
 57    private static final int NOTIFICATION_ID = 21;
 58    private static AtomicBoolean running = new AtomicBoolean(false);
 59    private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder();
 60    private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName());
 61    private final Set<OnBackupProcessed> mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>());
 62    private DatabaseBackend mDatabaseBackend;
 63    private NotificationManager notificationManager;
 64
 65    private static int count(String input, char c) {
 66        int count = 0;
 67        for (char aChar : input.toCharArray()) {
 68            if (aChar == c) {
 69                ++count;
 70            }
 71        }
 72        return count;
 73    }
 74
 75    @Override
 76    public void onCreate() {
 77        mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext());
 78        notificationManager = (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
 79    }
 80
 81    @Override
 82    public int onStartCommand(Intent intent, int flags, int startId) {
 83        if (intent == null) {
 84            return START_NOT_STICKY;
 85        }
 86        final String password = intent.getStringExtra("password");
 87        final Uri data = intent.getData();
 88        final Uri uri;
 89        if (data == null) {
 90            final String file = intent.getStringExtra("file");
 91            uri = file == null ? null : Uri.fromFile(new File(file));
 92        } else {
 93            uri = data;
 94        }
 95
 96        if (password == null || password.isEmpty() || uri == null) {
 97            return START_NOT_STICKY;
 98        }
 99        if (running.compareAndSet(false, true)) {
100            executor.execute(() -> {
101                startForegroundService();
102                final boolean success = importBackup(uri, password);
103                stopForeground(true);
104                running.set(false);
105                if (success) {
106                    notifySuccess();
107                }
108                stopSelf();
109            });
110        } else {
111            Log.d(Config.LOGTAG, "backup already running");
112        }
113        return START_NOT_STICKY;
114    }
115
116    public boolean getLoadingState() {
117        return running.get();
118    }
119
120    public void loadBackupFiles(OnBackupFilesLoaded onBackupFilesLoaded) {
121        executor.execute(() -> {
122            List<Jid> accounts = mDatabaseBackend.getAccountJids(false);
123            final ArrayList<BackupFile> backupFiles = new ArrayList<>();
124            final Set<String> apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name)));
125            for (String app : apps) {
126                final File directory = new File(FileBackend.getBackupDirectory(app));
127                if (!directory.exists() || !directory.isDirectory()) {
128                    Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath());
129                    continue;
130                }
131                for (File file : directory.listFiles()) {
132                    if (file.isFile() && file.getName().endsWith(".ceb")) {
133                        try {
134                            final BackupFile backupFile = BackupFile.read(file);
135                            if (accounts.contains(backupFile.getHeader().getJid())) {
136                                Log.d(Config.LOGTAG, "skipping backup for " + backupFile.getHeader().getJid());
137                            } else {
138                                backupFiles.add(backupFile);
139                            }
140                        } catch (IOException | IllegalArgumentException e) {
141                            Log.d(Config.LOGTAG, "unable to read backup file ", e);
142                        }
143                    }
144                }
145            }
146            Collections.sort(backupFiles, (a, b) -> a.header.getJid().toString().compareTo(b.header.getJid().toString()));
147            onBackupFilesLoaded.onBackupFilesLoaded(backupFiles);
148        });
149    }
150
151    private void startForegroundService() {
152        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
153        mBuilder.setContentTitle(getString(R.string.restoring_backup))
154                .setSmallIcon(R.drawable.ic_unarchive_white_24dp)
155                .setProgress(1, 0, true);
156        startForeground(NOTIFICATION_ID, mBuilder.build());
157    }
158
159    private boolean importBackup(Uri uri, String password) {
160        Log.d(Config.LOGTAG, "importing backup from " + uri);
161        try {
162            SQLiteDatabase db = mDatabaseBackend.getWritableDatabase();
163            final InputStream inputStream;
164            if ("file".equals(uri.getScheme())) {
165                inputStream = new FileInputStream(new File(uri.getPath()));
166            } else {
167                inputStream = getContentResolver().openInputStream(uri);
168            }
169            final DataInputStream dataInputStream = new DataInputStream(inputStream);
170            final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
171            Log.d(Config.LOGTAG, backupFileHeader.toString());
172
173            if (mDatabaseBackend.getAccountJids(false).contains(backupFileHeader.getJid())) {
174                synchronized (mOnBackupProcessedListeners) {
175                    for (OnBackupProcessed l : mOnBackupProcessedListeners) {
176                        l.onAccountAlreadySetup();
177                    }
178                }
179                return false;
180            }
181
182            final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
183            byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt());
184            SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
185            IvParameterSpec ivSpec = new IvParameterSpec(backupFileHeader.getIv());
186            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
187            CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
188
189            GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream);
190            BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, "UTF-8"));
191            String line;
192            StringBuilder multiLineQuery = null;
193            while ((line = reader.readLine()) != null) {
194                int count = count(line, '\'');
195                if (multiLineQuery != null) {
196                    multiLineQuery.append('\n');
197                    multiLineQuery.append(line);
198                    if (count % 2 == 1) {
199                        db.execSQL(multiLineQuery.toString());
200                        multiLineQuery = null;
201                    }
202                } else {
203                    if (count % 2 == 0) {
204                        db.execSQL(line);
205                    } else {
206                        multiLineQuery = new StringBuilder(line);
207                    }
208                }
209            }
210            final Jid jid = backupFileHeader.getJid();
211            Cursor countCursor = db.rawQuery("select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?", new String[]{jid.getEscapedLocal(), jid.getDomain()});
212            countCursor.moveToFirst();
213            int count = countCursor.getInt(0);
214            Log.d(Config.LOGTAG, "restored " + count + " messages");
215            countCursor.close();
216            stopBackgroundService();
217            synchronized (mOnBackupProcessedListeners) {
218                for (OnBackupProcessed l : mOnBackupProcessedListeners) {
219                    l.onBackupRestored();
220                }
221            }
222            return true;
223        } catch (Exception e) {
224            Throwable throwable = e.getCause();
225            final boolean reasonWasCrypto = throwable instanceof BadPaddingException || e instanceof ZipException;
226            synchronized (mOnBackupProcessedListeners) {
227                for (OnBackupProcessed l : mOnBackupProcessedListeners) {
228                    if (reasonWasCrypto) {
229                        l.onBackupDecryptionFailed();
230                    } else {
231                        l.onBackupRestoreFailed();
232                    }
233                }
234            }
235            Log.d(Config.LOGTAG, "error restoring backup " + uri, e);
236            return false;
237        }
238    }
239
240    private void notifySuccess() {
241        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
242        mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title))
243                .setContentText(getString(R.string.notification_restored_backup_subtitle))
244                .setAutoCancel(true)
245                .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), PendingIntent.FLAG_UPDATE_CURRENT))
246                .setSmallIcon(R.drawable.ic_unarchive_white_24dp);
247        notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
248    }
249
250    private void stopBackgroundService() {
251        Intent intent = new Intent(this, XmppConnectionService.class);
252        stopService(intent);
253    }
254
255    public void removeOnBackupProcessedListener(OnBackupProcessed listener) {
256        synchronized (mOnBackupProcessedListeners) {
257            mOnBackupProcessedListeners.remove(listener);
258        }
259    }
260
261    public void addOnBackupProcessedListener(OnBackupProcessed listener) {
262        synchronized (mOnBackupProcessedListeners) {
263            mOnBackupProcessedListeners.add(listener);
264        }
265    }
266
267    @Override
268    public IBinder onBind(Intent intent) {
269        return this.binder;
270    }
271
272    public interface OnBackupFilesLoaded {
273        void onBackupFilesLoaded(List<BackupFile> files);
274    }
275
276    public interface OnBackupProcessed {
277        void onBackupRestored();
278
279        void onBackupDecryptionFailed();
280
281        void onBackupRestoreFailed();
282
283        void onAccountAlreadySetup();
284    }
285
286    public static class BackupFile {
287        private final Uri uri;
288        private final BackupFileHeader header;
289
290        private BackupFile(Uri uri, BackupFileHeader header) {
291            this.uri = uri;
292            this.header = header;
293        }
294
295        private static BackupFile read(File file) throws IOException {
296            final FileInputStream fileInputStream = new FileInputStream(file);
297            final DataInputStream dataInputStream = new DataInputStream(fileInputStream);
298            BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
299            fileInputStream.close();
300            return new BackupFile(Uri.fromFile(file), backupFileHeader);
301        }
302
303        public static BackupFile read(final Context context, final Uri uri) throws IOException {
304            final InputStream inputStream = context.getContentResolver().openInputStream(uri);
305            if (inputStream == null) {
306                throw new FileNotFoundException();
307            }
308            final DataInputStream dataInputStream = new DataInputStream(inputStream);
309            BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream);
310            inputStream.close();
311            return new BackupFile(uri, backupFileHeader);
312        }
313
314        public BackupFileHeader getHeader() {
315            return header;
316        }
317
318        public Uri getUri() {
319            return uri;
320        }
321    }
322
323    public class ImportBackupServiceBinder extends Binder {
324        public ImportBackupService getService() {
325            return ImportBackupService.this;
326        }
327    }
328}