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}