1 /*
2  *   Copyright (C) 2020 by Chernov A.A.
3  *   valexlin@gmail.com
4  *
5  *   This program is free software: you can redistribute it and/or modify
6  *   it under the terms of the GNU General Public License as published by
7  *   the Free Software Foundation, either version 2 of the License, or
8  *   (at your option) any later version.
9  *
10  *   This program is distributed in the hope that it will be useful,
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *   GNU General Public License for more details.
14  *
15  *   You should have received a copy of the GNU General Public License
16  *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 package org.coolreader.sync2;
20 
21 import android.annotation.TargetApi;
22 import android.content.Intent;
23 import android.os.Build;
24 import android.os.Bundle;
25 import android.util.Xml;
26 
27 import org.coolreader.CoolReader;
28 import org.coolreader.R;
29 import org.coolreader.crengine.BackgroundThread;
30 import org.coolreader.crengine.BookInfo;
31 import org.coolreader.crengine.Bookmark;
32 import org.coolreader.crengine.FileInfo;
33 import org.coolreader.crengine.L;
34 import org.coolreader.crengine.Logger;
35 import org.coolreader.crengine.Properties;
36 import org.coolreader.crengine.Scanner;
37 import org.coolreader.crengine.Services;
38 import org.coolreader.crengine.Settings;
39 import org.coolreader.crengine.Utils;
40 import org.coolreader.db.CRDBService;
41 import org.xml.sax.InputSource;
42 import org.xml.sax.XMLReader;
43 import org.xmlpull.v1.XmlSerializer;
44 
45 import java.io.ByteArrayOutputStream;
46 import java.io.File;
47 import java.io.FileInputStream;
48 import java.io.FileOutputStream;
49 import java.io.IOException;
50 import java.io.InputStream;
51 import java.util.ArrayList;
52 import java.util.Date;
53 import java.util.HashMap;
54 import java.util.List;
55 import java.util.Set;
56 import java.util.zip.GZIPInputStream;
57 import java.util.zip.GZIPOutputStream;
58 
59 import javax.xml.parsers.SAXParser;
60 import javax.xml.parsers.SAXParserFactory;
61 
62 @TargetApi(Build.VERSION_CODES.GINGERBREAD)
63 public class Synchronizer {
64 
65 	public static final Logger log = L.create("sync2");
66 
67 	public enum SyncTarget {
68 		NONE,
69 		SETTINGS,
70 		BOOKMARKS,
71 		CURRENTBOOKINFO,
72 		CURRENTBOOKBODY
73 	}
74 
75 	public enum SyncDirection {
76 		None,
77 		SyncTo,
78 		SyncFrom
79 	}
80 
81 	public final static int SYNC_FLAG_NONE = 0x00;
82 	// force all operation perform everything, even disabled operations (regardless of specified sync targets).
83 	public final static int SYNC_FLAG_FORCE = 0x01;
84 	/// quiet mode, do not ask the user for anything.
85 	public final static int SYNC_FLAG_QUIETLY = 0x02;
86 	/// when required - show activity to log into account, otherwise try quietly (non-interactive) variant.
87 	public final static int SYNC_FLAG_SHOW_SIGN_IN = 0x04;
88 	/// show progress bar for sync process
89 	public final static int SYNC_FLAG_SHOW_PROGRESS = 0x08;
90 	/// ask user about changed position or changed document
91 	public final static int SYNC_FLAG_ASK_CHANGED = 0x10;
92 
93 	private class DownloadInfo implements Cloneable {
94 		public String m_filepath;		// full path to file on the cloud
95 		public FileMetadata m_meta;		// file metadata
96 
DownloadInfo(String filepath, FileMetadata meta)97 		public DownloadInfo(String filepath, FileMetadata meta) {
98 			m_filepath = filepath;
99 			m_meta = (FileMetadata) meta.clone();
100 		}
101 
clone()102 		public Object clone() {
103 			return new DownloadInfo(m_filepath, m_meta);
104 		}
105 	}
106 
107 	private RemoteAccess m_remoteAccess;
108 	private CoolReader m_coolReader;
109 	private int m_signInRequestCode;
110 	private String m_appName;
111 	private boolean m_isBusy;
112 	private boolean m_isAbortRequested;
113 	private SyncDirection m_syncDirection;
114 	private int m_currentOperationIndex;
115 	private int m_totalOperationsCount;
116 	private SyncOperation m_startOp;
117 	private SyncOperation m_lastOp;
118 	private OnSyncStatusListener m_onStatusListener;
119 	private Runnable m_onAbortedListener;
120 	private HashMap<SyncTarget, Boolean> m_syncTargets;
121 	private int m_dataKeepAlive = 14;
122 	private int m_flags = 0;
123 	private int m_lockTryCount = 0;
124 
125 	private static final String[] ALLOWED_OPTIONS_PROP_NAMES = {
126 			Settings.PROP_FALLBACK_FONT_FACES,
127 			Settings.PROP_FOOTNOTES,
128 			Settings.PROP_APP_HIGHLIGHT_BOOKMARKS,
129 			Settings.PROP_HYPHENATION_DICT,
130 			Settings.PROP_IMG_SCALING_ZOOMIN_INLINE_MODE,
131 			Settings.PROP_IMG_SCALING_ZOOMIN_INLINE_SCALE,
132 			Settings.PROP_IMG_SCALING_ZOOMOUT_INLINE_MODE,
133 			Settings.PROP_IMG_SCALING_ZOOMOUT_INLINE_SCALE,
134 			Settings.PROP_IMG_SCALING_ZOOMIN_BLOCK_MODE,
135 			Settings.PROP_IMG_SCALING_ZOOMIN_BLOCK_SCALE,
136 			Settings.PROP_IMG_SCALING_ZOOMOUT_BLOCK_MODE,
137 			Settings.PROP_IMG_SCALING_ZOOMOUT_BLOCK_SCALE,
138 			Settings.PROP_STATUS_CHAPTER_MARKS,
139 			Settings.PROP_PAGE_VIEW_MODE,
140 			Settings.PROP_PROGRESS_SHOW_FIRST_PAGE,
141 			Settings.PROP_RENDER_BLOCK_RENDERING_FLAGS,
142 			Settings.PROP_REQUESTED_DOM_VERSION,
143 			Settings.PROP_FLOATING_PUNCTUATION,
144 			Settings.PROP_FORMAT_MAX_ADDED_LETTER_SPACING_PERCENT,
145 			Settings.PROP_FORMAT_MIN_SPACE_CONDENSING_PERCENT,
146 			Settings.PROP_FORMAT_SPACE_WIDTH_SCALE_PERCENT,
147 			Settings.PROP_FORMAT_UNUSED_SPACE_THRESHOLD_PERCENT,
148 			Settings.PROP_TEXTLANG_EMBEDDED_LANGS_ENABLED,
149 			Settings.PROP_TEXTLANG_HYPHENATION_ENABLED,
150 			Settings.PROP_FONT_KERNING_ENABLED,
151 			Settings.PROP_FLOATING_PUNCTUATION,
152 			Settings.PROP_APP_TAP_ZONE_ACTIONS_TAP,
153 			// header settings
154 			Settings.PROP_SHOW_TITLE,
155 			Settings.PROP_SHOW_PAGE_NUMBER,
156 			Settings.PROP_SHOW_PAGE_COUNT,
157 			Settings.PROP_SHOW_POS_PERCENT,
158 			Settings.PROP_STATUS_CHAPTER_MARKS,
159 			Settings.PROP_SHOW_BATTERY_PERCENT,
160 			// all per-style definitions
161 			"styles."
162 	};
163 
164 	private static final String REMOTE_FOLDER_PATH = "/.cr3";
165 	private static final String REMOTE_SETTINGS_FILE_PATH = REMOTE_FOLDER_PATH + "/cr3.ini.gz";
166 	private static final String LOCK_FILE_PATH = REMOTE_FOLDER_PATH + "/.lock";
167 	private static final int LOCK_FILE_CHECK_PERIOD = 500;			// ms
168 	private static final int LOCK_FILE_CHECK_MAX_COUNT = 120;		// total wait 60 sec.
169 
170 	private static final int BOOKMARKS_BUNDLE_VERSION = 3;
171 	private static final int CURRENTBOOKINFO_BUNDLE_VERSION = 3;
172 
173 
Synchronizer(CoolReader coolReader, RemoteAccess remoteAccess, String appName, int signInRequestCode)174 	public Synchronizer(CoolReader coolReader, RemoteAccess remoteAccess, String appName, int signInRequestCode) {
175 		m_coolReader = coolReader;
176 		m_remoteAccess = remoteAccess;
177 		m_syncTargets = new HashMap<>();
178 		m_signInRequestCode = signInRequestCode;
179 		m_appName = appName;
180 	}
181 
setTarget(SyncTarget target, boolean enable)182 	public void setTarget(SyncTarget target, boolean enable) {
183 		m_syncTargets.put(target, enable);
184 	}
185 
hasTarget(SyncTarget target)186 	public boolean hasTarget(SyncTarget target) {
187 		return hasTarget(target, false);
188 	}
189 
hasTarget(SyncTarget target, boolean defValue)190 	public boolean hasTarget(SyncTarget target, boolean defValue) {
191 		Boolean value = m_syncTargets.get(target);
192 		if (null == value)
193 			return defValue;
194 		return value;
195 	}
196 
setBookmarksKeepAlive(int days)197 	public void setBookmarksKeepAlive(int days) {
198 		if (days < 0)
199 			days = 0;
200 		else if (days > 365)
201 			days = 365;
202 		m_dataKeepAlive = days;
203 	}
204 
setSignInRequestCode(int requestCode)205 	public void setSignInRequestCode(int requestCode) {
206 		m_signInRequestCode = requestCode;
207 	}
208 
setApplicationName(String appName)209 	public void setApplicationName(String appName) {
210 		m_appName = appName;
211 	}
212 
213 	/**
214 	 * @param requestCode
215 	 * @param resultCode
216 	 * @param data
217 	 * @brief Helper function to handle some operation results from Google Service activity.
218 	 * Must be called from main activity in function onActivityResult().
219 	 */
onActivityResultHandler(int requestCode, int resultCode, Intent data)220 	public void onActivityResultHandler(int requestCode, int resultCode, Intent data) {
221 		m_remoteAccess.onActivityResultHandler(requestCode, resultCode, data);
222 	}
223 
isBusy()224 	public boolean isBusy() {
225 		return m_isBusy;
226 	}
227 
abort()228 	public void abort() {
229 		abort(null);
230 	}
231 
abort(Runnable onAborted)232 	public void abort(Runnable onAborted) {
233 		if (m_isBusy) {
234 			m_isAbortRequested = true;
235 			m_onAbortedListener = onAborted;
236 		} else {
237 			if (null != onAborted) {
238 				onAborted.run();
239 			}
240 		}
241 	}
242 
setOnSyncStatusListener(OnSyncStatusListener listener)243 	public void setOnSyncStatusListener(OnSyncStatusListener listener) {
244 		m_onStatusListener = listener;
245 	}
246 
doneSuccessfully()247 	protected void doneSuccessfully() {
248 		BackgroundThread.instance().executeGUI(() -> {
249 			if (null != m_onStatusListener) {
250 				m_onStatusListener.onSyncCompleted(m_syncDirection, (m_flags & SYNC_FLAG_SHOW_PROGRESS) != 0, (m_flags & SYNC_FLAG_QUIETLY) == 0);
251 			}
252 		});
253 		m_isBusy = false;
254 	}
255 
doneFailed(String error)256 	protected void doneFailed(String error) {
257 		BackgroundThread.instance().executeGUI(() -> {
258 			if (null != m_onStatusListener) {
259 				m_onStatusListener.onSyncError(m_syncDirection, error);
260 			}
261 		});
262 		m_isBusy = false;
263 	}
264 
doneAborted()265 	protected void doneAborted() {
266 		BackgroundThread.instance().executeGUI(() -> {
267 			if (null != m_onStatusListener) {
268 				m_onStatusListener.onAborted(m_syncDirection);
269 			}
270 		});
271 		m_isBusy = false;
272 	}
273 
setSyncStarted(SyncDirection dir)274 	protected void setSyncStarted(SyncDirection dir) {
275 		m_isBusy = true;
276 		m_syncDirection = dir;
277 		BackgroundThread.instance().executeGUI(() -> {
278 			if (null != m_onStatusListener) {
279 				m_onStatusListener.onSyncStarted(m_syncDirection, (m_flags & SYNC_FLAG_SHOW_PROGRESS) != 0, (m_flags & SYNC_FLAG_QUIETLY) == 0);
280 			}
281 		});
282 	}
283 
updateSyncProgress(int current, int total)284 	protected void updateSyncProgress(int current, int total) {
285 		BackgroundThread.instance().executeGUI(() -> {
286 			if (null != m_onStatusListener) {
287 				m_onStatusListener.OnSyncProgress(m_syncDirection, (m_flags & SYNC_FLAG_SHOW_PROGRESS) != 0, current, total, (m_flags & SYNC_FLAG_QUIETLY) == 0);
288 			}
289 		});
290 	}
291 
checkAbort()292 	protected boolean checkAbort() {
293 		if (m_isAbortRequested) {
294 			if (null != m_onAbortedListener) {
295 				m_onAbortedListener.run();
296 				m_onAbortedListener = null;
297 			}
298 			doneAborted();
299 		}
300 		return m_isAbortRequested;
301 	}
302 
clearOperation()303 	protected void clearOperation() {
304 		m_startOp = null;
305 		m_lastOp = null;
306 		m_totalOperationsCount = 0;
307 		m_currentOperationIndex = 0;
308 	}
309 
addOperation(SyncOperation op)310 	protected void addOperation(SyncOperation op) {
311 		if (null == m_startOp) {
312 			m_startOp = op;
313 		} else {
314 			m_lastOp.setNext(op);
315 		}
316 		m_lastOp = op;
317 		m_totalOperationsCount++;
318 	}
319 
replaceOperation(SyncOperation op, SyncOperation with)320 	protected void replaceOperation(SyncOperation op, SyncOperation with) {
321 		SyncOperation operation = m_startOp;
322 		SyncOperation prevOp = null;
323 		while (null != operation && operation != op) {
324 			prevOp = operation;
325 			operation = operation.m_next;
326 		}
327 		if (null != operation) {
328 			// found
329 			with.setNext(operation.m_next);
330 			if (null != prevOp)
331 				prevOp.setNext(with);
332 			else
333 				m_startOp = with;
334 		} else {
335 			log.e("replaceOperation() failed, cannot find a replacement point!");
336 		}
337 	}
338 
insertOperation(SyncOperation after, SyncOperation op)339 	protected void insertOperation(SyncOperation after, SyncOperation op) {
340 		SyncOperation operation = m_startOp;
341 		while (null != operation && operation != after) {
342 			operation = operation.m_next;
343 		}
344 		if (null != operation) {
345 			// found
346 			op.setNext(operation.m_next);
347 			operation.setNext(op);
348 			m_totalOperationsCount++;
349 		} else {
350 			log.e("insertOperation() failed, can't find the insertion point!");
351 		}
352 	}
353 
startOperations()354 	protected void startOperations() {
355 		m_lockTryCount = 0;
356 		if (null != m_startOp)
357 			m_startOp.exec();
358 	}
359 
360 	// SignIn operation
361 	protected class SignInSyncOperation extends SyncOperation {
362 		@Override
call(Runnable onContinue)363 		void call(Runnable onContinue) {
364 			log.d("Starting sign-in operation...");
365 
366 			Bundle params = new Bundle();
367 			params.putInt("requestCode", m_signInRequestCode);
368 			params.putString("appName", m_appName);
369 			m_remoteAccess.signIn(params, (data, statusCode) -> {
370 				if (checkAbort())
371 					return;
372 				m_currentOperationIndex++;
373 				updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
374 				if (0 == statusCode) {
375 					log.d("SignInSyncOperation: SignIn successfully.");
376 					onContinue.run();
377 				} else {
378 					log.e("SignInSyncOperation: SignIn failed");
379 					doneFailed("SignIn failed");
380 				}
381 			});
382 		}
383 	}
384 
385 	// Quietly SignIn operation
386 	protected class SignInQuietlySyncOperation extends SyncOperation {
387 		@Override
call(Runnable onContinue)388 		void call(Runnable onContinue) {
389 			log.d("Starting sign-in (quietly) operation...");
390 
391 			m_remoteAccess.signInQuietly((data, statusCode) -> {
392 				if (checkAbort())
393 					return;
394 				m_currentOperationIndex++;
395 				updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
396 				if (0 == statusCode) {
397 					log.d("SignInQuietlySyncOperation: signIn successfully.");
398 					onContinue.run();
399 				} else {
400 					log.e("SignInQuietlySyncOperation: signIn failed.");
401 					doneFailed("SignIn failed");
402 				}
403 			});
404 		}
405 	}
406 
407 	// SignOut operation
408 	protected class SignOutSyncOperation extends SyncOperation {
409 		@Override
call(Runnable onContinue)410 		void call(Runnable onContinue) {
411 			log.d("Starting sign-out operation...");
412 
413 			Bundle params = new Bundle();
414 			m_remoteAccess.signOut(params, (statusCode) -> {
415 				if (checkAbort())
416 					return;
417 				m_currentOperationIndex++;
418 				updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
419 				if (0 == statusCode) {
420 					log.d("SignOutSyncOperation: SignOut successfully.");
421 					onContinue.run();
422 				} else {
423 					log.e("SignOutSyncOperation: SignOut failed");
424 					doneFailed("SignOut failed");
425 				}
426 			});
427 		}
428 	}
429 
430 	// Check/create application folder operation
431 	protected class CheckAppFolderSyncOperation extends SyncOperation {
432 		@Override
call(Runnable onContinue)433 		void call(Runnable onContinue) {
434 			log.d("Starting CheckAppFolderSyncOperation operation...");
435 
436 			m_remoteAccess.mkdir(REMOTE_FOLDER_PATH, new OnOperationCompleteListener<FileMetadata>() {
437 				@Override
438 				public void onCompleted(FileMetadata meta, boolean ok) {
439 					if (!ok)
440 						return;		// onFailed() will be called
441 					if (checkAbort())
442 						return;
443 					m_currentOperationIndex++;
444 					updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
445 					if (null != meta) {
446 						onContinue.run();
447 					} else {
448 						log.e("CheckAppFolderSyncOperation: failed to create application folder");
449 						doneFailed("Failed to create application folder");
450 					}
451 				}
452 
453 				@Override
454 				public void onFailed(Exception e) {
455 					log.e("CheckAppFolderSyncOperation: mkdir() failed: " + e.toString());
456 					doneFailed(e.toString());
457 				}
458 			});
459 		}
460 	}
461 
462 	protected class CheckLockFileSyncOperation extends SyncOperation {
463 		@Override
call(Runnable onContinue)464 		void call(Runnable onContinue) {
465 			log.d("Starting CheckLockFileSyncOperation operation...");
466 
467 			// 1. Check if lock file is exists
468 			m_remoteAccess.stat(LOCK_FILE_PATH, false, new OnOperationCompleteListener<FileMetadata>() {
469 				@Override
470 				public void onCompleted(FileMetadata meta, boolean ok) {
471 					if (!ok)
472 						return;		// onFailed() will be called
473 					if (checkAbort())
474 						return;
475 					if (null == meta) {
476 						// 2. Lock file not exist, creating...
477 						String contents = "lock file";
478 						m_remoteAccess.writeFile(LOCK_FILE_PATH, contents.getBytes(), null, new OnOperationCompleteListener<Boolean>() {
479 							@Override
480 							public void onCompleted(Boolean result, boolean ok) {
481 								if (!ok)
482 									return;		// onFailed() will be called
483 								if (checkAbort())
484 									return;
485 								// After the other device syncs, new files may appear in Drive,
486 								// so we must clear the directory cache to re-read the directory contents in the next sync step.
487 								m_remoteAccess.discardDirCache();
488 								log.d("lock file created, continue.");
489 								m_currentOperationIndex++;
490 								updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
491 								onContinue.run();
492 							}
493 
494 							@Override
495 							public void onFailed(Exception e) {
496 								log.e("CheckSyncLockerSyncOperation: write lock file failed: " + e.toString());
497 								doneFailed(e.toString());
498 							}
499 						});
500 					} else {
501 						// 3. Wait while lock file exist
502 						m_lockTryCount++;
503 						if (m_lockTryCount < LOCK_FILE_CHECK_MAX_COUNT) {
504 							log.d("lock file exists, waiting...");
505 							SyncOperation this_op = CheckLockFileSyncOperation.this;
506 							BackgroundThread.instance().postBackground(() -> {
507 								insertOperation(this_op, new CheckLockFileSyncOperation());
508 								// update progress bar on screen
509 								m_currentOperationIndex++;
510 								updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
511 								onContinue.run();
512 							}, LOCK_FILE_CHECK_PERIOD);
513 						} else {
514 							// stale lock file?
515 							m_remoteAccess.discardDirCache();
516 							log.d("lock file still exists after waiting " + (LOCK_FILE_CHECK_MAX_COUNT*LOCK_FILE_CHECK_PERIOD)/1000 + " seconds, ignore ..." );
517 							m_currentOperationIndex++;
518 							updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
519 							onContinue.run();
520 						}
521 					}
522 				}
523 
524 				@Override
525 				public void onFailed(Exception e) {
526 					log.e("CheckSyncLockerSyncOperation: stat failed: " + e.toString());
527 					doneFailed(e.toString());
528 				}
529 			});
530 		}
531 	}
532 
533 	protected class RemoveLockFileSyncOperation extends SyncOperation {
534 		@Override
call(Runnable onContinue)535 		void call(Runnable onContinue) {
536 			log.d("Starting RemoveLockFileSyncOperation operation...");
537 
538 			m_remoteAccess.stat(LOCK_FILE_PATH, false, new OnOperationCompleteListener<FileMetadata>() {
539 				@Override
540 				public void onCompleted(FileMetadata meta, boolean ok) {
541 					if (!ok)
542 						return;		// onFailed() will be called
543 					if (checkAbort())
544 						return;
545 					if (null != meta) {
546 						// file exist
547 						m_remoteAccess.delete(LOCK_FILE_PATH, new OnOperationCompleteListener<Boolean>() {
548 							@Override
549 							public void onCompleted(Boolean result, boolean ok) {
550 								//if (!ok)
551 								//	return;        // onFailed() will be called
552 								if (checkAbort())
553 									return;
554 								m_currentOperationIndex++;
555 								updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
556 								onContinue.run();
557 							}
558 
559 							@Override
560 							public void onFailed(Exception e) {
561 								log.d("RemoveLockFileSyncOperation: delete failed: " + e.toString());
562 								// ignore this
563 							}
564 						});
565 					} else {
566 						m_currentOperationIndex++;
567 						updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
568 						onContinue.run();
569 					}
570 				}
571 
572 				@Override
573 				public void onFailed(Exception e) {
574 					log.e("RemoveLockFileSyncOperation: stat failed: " + e.toString());
575 					doneFailed(e.toString());
576 				}
577 			});
578 		}
579 	}
580 
581 	// Check remote settings file modification operation
582 	protected class CheckUploadSettingsSyncOperation extends SyncOperation {
583 		private final String localFilePath;
584 		private final String remoteFilePath;
585 
CheckUploadSettingsSyncOperation(String localFilePath, String remoteFilePath)586 		CheckUploadSettingsSyncOperation(String localFilePath, String remoteFilePath) {
587 			this.localFilePath = localFilePath;
588 			this.remoteFilePath = remoteFilePath;
589 		}
590 
591 		@Override
call(Runnable onContinue)592 		void call(Runnable onContinue) {
593 			log.d("Starting CheckUploadSettingsSyncOperation operation...");
594 
595 			// 1. Check remote file modification file
596 			m_remoteAccess.stat(remoteFilePath, true, new OnOperationCompleteListener<FileMetadata>() {
597 				@Override
598 				public void onCompleted(FileMetadata meta, boolean ok) {
599 					if (!ok)
600 						return;		// onFailed() will be called
601 					if (checkAbort())
602 						return;
603 					m_currentOperationIndex++;
604 					updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
605 					SyncOperation this_op = CheckUploadSettingsSyncOperation.this;
606 					if (null != meta) {
607 						// file found on remote device
608 						File localFile = new File(localFilePath);
609 						if (localFile.exists()) {
610 							// local file exist
611 							if (localFile.lastModified() >= meta.modifiedDate.getTime()) {
612 								// local file newer than remote
613 								insertOperation(this_op, new UploadSettingsSyncOperation(localFilePath, remoteFilePath));
614 								onContinue.run();
615 							} else {
616 								// remote file newer than local
617 								log.d("CheckUploadSettingsSyncOperation: remote file newer than local");
618 								// ask the user via dialog what to do with it
619 								SyncInfoDialog syncDirDialog = new SyncInfoDialog(m_coolReader, m_coolReader.getString(R.string.confirmation_title), m_coolReader.getString(R.string.googledrive_remotefile_is_newer_confirm, remoteFilePath));
620 								syncDirDialog.setPositiveButtonLabel(m_coolReader.getString(R.string.googledrive_upload_local));
621 								syncDirDialog.setNegativeButtonLabel(m_coolReader.getString(R.string.googledrive_load_remote));
622 								syncDirDialog.setOnPositiveClickListener( view -> {
623 									insertOperation(this_op, new UploadSettingsSyncOperation(localFilePath, remoteFilePath));
624 									onContinue.run();
625 								} );
626 								syncDirDialog.setOnNegativeClickListener( view -> {
627 									insertOperation(this_op, new DownloadSettingsSyncOperation(remoteFilePath));
628 									onContinue.run();
629 								} );
630 								syncDirDialog.setOnCancelListener(dialog -> {
631 									log.e("CheckUploadSettingsSyncOperation: canceled");
632 									doneFailed("canceled");
633 								} );
634 								syncDirDialog.show();
635 							}
636 						} else {
637 							// local file not exist
638 							log.e("CheckUploadSettingsSyncOperation: local file not exist!");
639 							doneFailed("local file not exist!");
640 						}
641 					} else {
642 						// file not found on remote service
643 						insertOperation(this_op, new UploadSettingsSyncOperation(localFilePath, remoteFilePath));
644 						onContinue.run();
645 					}
646 				}
647 
648 				@Override
649 				public void onFailed(Exception e) {
650 					log.e("CheckUploadSettingsSyncOperation: stat failed: " + e.toString());
651 					doneFailed(e.toString());
652 				}
653 			});
654 		}
655 	}
656 
657 	// Upload settings operation
658 	protected class UploadSettingsSyncOperation extends SyncOperation {
659 		private final String localFilePath;
660 		private final String remoteFilePath;
661 
UploadSettingsSyncOperation(String localFilePath, String remoteFilePath)662 		UploadSettingsSyncOperation(String localFilePath, String remoteFilePath) {
663 			this.localFilePath = localFilePath;
664 			this.remoteFilePath = remoteFilePath;
665 		}
666 
667 		@Override
call(Runnable onContinue)668 		public void call(Runnable onContinue) {
669 			log.d("Starting UploadSettingsSyncOperation operation...");
670 
671 			try {
672 				FileInputStream inputStream = new FileInputStream(localFilePath);
673 				ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
674 				GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
675 				byte[] buf = new byte[4096];
676 				int readBytes;
677 				while (true) {
678 					readBytes = inputStream.read(buf);
679 					if (readBytes > 0)
680 						gzipOutputStream.write(buf, 0, readBytes);
681 					else
682 						break;
683 				}
684 				gzipOutputStream.close();
685 				outputStream.close();
686 				m_remoteAccess.writeFile(remoteFilePath, outputStream.toByteArray(), null, new OnOperationCompleteListener<Boolean>() {
687 					@Override
688 					public void onCompleted(Boolean result, boolean ok) {
689 						if (!ok)
690 							return;		// onFailed() will be called
691 						if (checkAbort())
692 							return;
693 						m_currentOperationIndex++;
694 						updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
695 						if (null != result && result) {
696 							log.d("file created or updated.");
697 							onContinue.run();
698 						} else {
699 							log.e("UploadSettingsSyncOperation: file NOT created!");
700 							doneFailed("file NOT created");
701 						}
702 					}
703 
704 					@Override
705 					public void onFailed(Exception e) {
706 						log.e("UploadSettingsSyncOperation: file creation failed: " + e.toString());
707 						doneFailed("file creation failed: " + e.toString());
708 					}
709 				});
710 			} catch (Exception e) {
711 				log.e("UploadSettingsSyncOperation: Can't read local file: " + e.toString());
712 				doneFailed("Can't read local file: " + e.toString());
713 			}
714 		}
715 	}
716 
717 	// Check remote settings file modification operation
718 	protected class CheckDownloadSettingsSyncOperation extends SyncOperation {
719 		private final String localFilePath;
720 		private final String remoteFilePath;
721 
CheckDownloadSettingsSyncOperation(String localFilePath, String remoteFilePath)722 		CheckDownloadSettingsSyncOperation(String localFilePath, String remoteFilePath) {
723 			this.localFilePath = localFilePath;
724 			this.remoteFilePath = remoteFilePath;
725 		}
726 
727 		@Override
call(Runnable onContinue)728 		void call(Runnable onContinue) {
729 			log.d("Starting CheckDownloadSettingsSyncOperation operation...");
730 
731 			// 1. Check remote file modification file
732 			m_remoteAccess.stat(remoteFilePath, true, new OnOperationCompleteListener<FileMetadata>() {
733 				@Override
734 				public void onCompleted(FileMetadata meta, boolean ok) {
735 					if (!ok)
736 						return;		// onFailed() will be called
737 					if (checkAbort())
738 						return;
739 					m_currentOperationIndex++;
740 					updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
741 					SyncOperation this_op = CheckDownloadSettingsSyncOperation.this;
742 					if (null != meta) {
743 						// file found on remote device
744 						File localFile = new File(localFilePath);
745 						if (localFile.exists()) {
746 							// local file exist
747 							if (meta.modifiedDate.getTime() + 10000 >= localFile.lastModified()) {	// up to 10 s. difference allowed
748 								// remote file newer than remote
749 								insertOperation(this_op, new DownloadSettingsSyncOperation(remoteFilePath));
750 								onContinue.run();
751 							} else {
752 								// local file newer than remote
753 								log.d("CheckDownloadSettingsSyncOperation: local file (" + new Date(localFile.lastModified()).toString() + ") newer than remote (" + meta.modifiedDate.toString() + ")");
754 								// ask the user via dialog what to do with it
755 								SyncInfoDialog syncDirDialog = new SyncInfoDialog(m_coolReader, m_coolReader.getString(R.string.confirmation_title), m_coolReader.getString(R.string.googledrive_localfile_is_newer_confirm, localFilePath));
756 								syncDirDialog.setPositiveButtonLabel(m_coolReader.getString(R.string.googledrive_load_remote));
757 								syncDirDialog.setNegativeButtonLabel(m_coolReader.getString(R.string.googledrive_upload_local));
758 								syncDirDialog.setOnPositiveClickListener( view -> {
759 									insertOperation(this_op, new DownloadSettingsSyncOperation(remoteFilePath));
760 									onContinue.run();
761 								} );
762 								syncDirDialog.setOnNegativeClickListener( view -> {
763 									insertOperation(this_op, new UploadSettingsSyncOperation(localFilePath, remoteFilePath));
764 									onContinue.run();
765 								} );
766 								syncDirDialog.setOnCancelListener(dialog -> {
767 									log.e("CheckDownloadSettingsSyncOperation: canceled");
768 									doneFailed("canceled");
769 								} );
770 								syncDirDialog.show();
771 							}
772 						} else {
773 							// local file not exist, just copy from remote
774 							insertOperation(this_op, new DownloadSettingsSyncOperation(remoteFilePath));
775 							onContinue.run();
776 						}
777 					} else {
778 						// file not found on remote service
779 						log.e("CheckDownloadSettingsSyncOperation: remote file not exist!");
780 						doneFailed("remote file not exist!");
781 					}
782 				}
783 
784 				@Override
785 				public void onFailed(Exception e) {
786 					log.e("CheckDownloadSettingsSyncOperation: stat failed: " + e.toString());
787 					doneFailed(e.toString());
788 				}
789 			});
790 		}
791 	}
792 
793 	// Download settings operation
794 	protected class DownloadSettingsSyncOperation extends SyncOperation {
795 		private final String remoteFilePath;
796 
DownloadSettingsSyncOperation(String remoteFilePath)797 		DownloadSettingsSyncOperation(String remoteFilePath) {
798 			this.remoteFilePath = remoteFilePath;
799 		}
800 
801 		@Override
call(Runnable onContinue)802 		void call(Runnable onContinue) {
803 			log.d("Starting DownloadSettingsSyncOperation operation...");
804 
805 			m_remoteAccess.readFile(remoteFilePath, new OnOperationCompleteListener<InputStream>() {
806 				@Override
807 				public void onCompleted(InputStream inputStream, boolean ok) {
808 					if (!ok)
809 						return;		// onFailed() will be called
810 					if (checkAbort())
811 						return;
812 					m_currentOperationIndex++;
813 					updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
814 					if (null != inputStream) {
815 						log.d("Reading settings from remote service...");
816 						try {
817 							GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
818 							Properties allProps = new Properties();
819 							allProps.load(gzipInputStream);
820 							inputStream.close();
821 							// filter not allowed options
822 							Set<String> keys = allProps.stringPropertyNames();
823 							boolean allowed;
824 							Properties props = new Properties();
825 							for (String key : keys) {
826 								allowed = false;
827 								for (String allowedPropName : ALLOWED_OPTIONS_PROP_NAMES) {
828 									if (key.startsWith(allowedPropName)) {
829 										allowed = true;
830 										break;
831 									}
832 								}
833 								if (allowed)
834 									props.put(key, allProps.get(key));
835 							}
836 							BackgroundThread.instance().executeGUI(() -> {
837 								if (null != m_onStatusListener) {
838 									m_onStatusListener.onSettingsLoaded(props, (m_flags & SYNC_FLAG_QUIETLY) == 0);
839 								}
840 							});
841 							log.d(" ... done.");
842 						} catch (Exception e) {
843 							log.e("DownloadSettingsSyncOperation: file opened, but failed to read or write: " + e.toString());
844 							// don't mark as failure, may be next operation will be successfully
845 						}
846 						// call next operation regardless of this operation result
847 						onContinue.run();
848 					} else {
849 						log.e("DownloadSettingsSyncOperation: read remote file: return null stream!");
850 						doneFailed("read remote file: return null stream!");
851 					}
852 				}
853 
854 				@Override
855 				public void onFailed(Exception e) {
856 					log.e("DownloadSettingsSyncOperation: Can't read remote file: " + e.toString());
857 					doneFailed(e.toString());
858 				}
859 			});
860 		}
861 	}
862 
863 	// upload bookmarks for currently opened book
864 	protected class UploadBookmarksSyncOperation extends SyncOperation {
865 		private final BookInfo bookInfo;
866 
UploadBookmarksSyncOperation(BookInfo bookInfo)867 		UploadBookmarksSyncOperation(BookInfo bookInfo) {
868 			this.bookInfo = new BookInfo(bookInfo);		// make a copy
869 		}
870 
871 		@Override
call(Runnable onContinue)872 		void call(Runnable onContinue) {
873 			log.d("Starting UploadBookmarksSyncOperation operation...");
874 			FileInfo fileInfo = bookInfo.getFileInfo();
875 			byte[] data = getCurrentBookBookmarksData(bookInfo);
876 			if (null != data) {
877 				// TODO: replace crc32 with sha512 and remove filename from this
878 				String fileName = fileInfo.filename + "_" + fileInfo.crc32 + ".bmk.xml.gz";
879 				m_remoteAccess.writeFile(REMOTE_FOLDER_PATH + "/" + fileName, data, null, new OnOperationCompleteListener<Boolean>() {
880 					@Override
881 					public void onCompleted(Boolean result, boolean ok) {
882 						if (!ok)
883 							return;        // onFailed() will be called
884 						if (checkAbort())
885 							return;
886 						m_currentOperationIndex++;
887 						updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
888 						if (null != result && result) {
889 							log.d("file created or updated.");
890 							onContinue.run();
891 						} else {
892 							log.e("UploadBookmarksSyncOperation: file NOT created!");
893 							doneFailed("file NOT created");
894 						}
895 					}
896 
897 					@Override
898 					public void onFailed(Exception e) {
899 						log.e("UploadBookmarksSyncOperation: write failed: " + e.toString());
900 						doneFailed(e.toString());
901 					}
902 				});
903 			} else {
904 				// bookmarks data is null, continue with next operation
905 				log.d("bookmarks data is null, continue with next operation");
906 				m_currentOperationIndex++;
907 				updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
908 				onContinue.run();
909 			}
910 		}
911 	}
912 
913 	// download bookmarks for one specified book
914 	protected class DownloadBookmarksSyncOperation extends SyncOperation {
915 		private final String fileName;
916 
DownloadBookmarksSyncOperation(String fileName)917 		public DownloadBookmarksSyncOperation(String fileName) {
918 			this.fileName = fileName;
919 		}
920 
921 		@Override
call(Runnable onContinue)922 		void call(Runnable onContinue) {
923 			log.d("Starting DownloadBookmarksSyncOperation operation...");
924 
925 			m_remoteAccess.readFile(fileName, new OnOperationCompleteListener<InputStream>() {
926 				@Override
927 				public void onCompleted(InputStream inputStream, boolean ok) {
928 					if (!ok)
929 						return;		// onFailed() will be called
930 					if (checkAbort())
931 						return;
932 					m_currentOperationIndex++;
933 					updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
934 					if (null != inputStream) {
935 						syncBookmarks(inputStream);
936 						onContinue.run();
937 					} else {
938 						log.e("DownloadBookmarksSyncOperation: can't read bookmarks bundle");
939 						doneFailed("Can't read bookmarks bundle");
940 					}
941 				}
942 
943 				@Override
944 				public void onFailed(Exception e) {
945 					log.e("DownloadBookmarksSyncOperation: readFile failed: " + e.toString());
946 					doneFailed(e.toString());
947 				}
948 			});
949 		}
950 	}
951 
952 	protected class DownloadAllBookmarksSyncOperation extends SyncOperation {
953 		@Override
call(Runnable onContinue)954 		void call(Runnable onContinue) {
955 			log.d("Starting DownloadAllBookmarksSyncOperation operation...");
956 
957 			m_remoteAccess.list(REMOTE_FOLDER_PATH, true, new OnOperationCompleteListener<FileMetadataList>() {
958 				@Override
959 				public void onCompleted(FileMetadataList metalist, boolean ok) {
960 					if (!ok)
961 						return;		// onFailed() will be called
962 					if (checkAbort())
963 						return;
964 					m_currentOperationIndex++;
965 					updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
966 					if (null != metalist) {
967 						SyncOperation op = DownloadAllBookmarksSyncOperation.this;
968 						for (FileMetadata meta : metalist) {
969 							if (meta.fileName.endsWith(".bmk.xml.gz")) {
970 								log.d("scheduling bookmark loading from file " + meta.fileName);
971 								String fileName = REMOTE_FOLDER_PATH + "/" + meta.fileName;
972 								SyncOperation downloadBookmark = new DownloadBookmarksSyncOperation(fileName);
973 								insertOperation(op, downloadBookmark);
974 								op = downloadBookmark;
975 							}
976 						}
977 						onContinue.run();
978 					} else {
979 						log.e("DownloadAllBookmarksSyncOperation: list return null");
980 						doneFailed("list return null");
981 					}
982 				}
983 
984 				@Override
985 				public void onFailed(Exception e) {
986 					log.e("DownloadAllBookmarksSyncOperation: list failed: " + e.toString());
987 					doneFailed(e.toString());
988 				}
989 			});
990 		}
991 	}
992 
993 	protected class UploadCurrentBookInfoSyncOperation extends SyncOperation {
994 		private final BookInfo bookInfo;
995 
UploadCurrentBookInfoSyncOperation(BookInfo bookInfo)996 		UploadCurrentBookInfoSyncOperation(BookInfo bookInfo) {
997 			this.bookInfo = new BookInfo(bookInfo);		// make a copy
998 		}
999 
1000 		@Override
call(Runnable onContinue)1001 		void call(Runnable onContinue) {
1002 			log.d("Starting UploadCurrentBookInfoSyncOperation operation...");
1003 
1004 			FileInfo fileInfo = bookInfo.getFileInfo();
1005 			try {
1006 				ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
1007 				GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
1008 				Properties props = new Properties();
1009 				props.setInt("version", CURRENTBOOKINFO_BUNDLE_VERSION);
1010 				props.setProperty("filename", fileInfo.filename);
1011 				props.setProperty("authors", fileInfo.authors);
1012 				props.setProperty("title", fileInfo.title);
1013 				props.setProperty("series", fileInfo.series);
1014 				props.setInt("seriesNumber", fileInfo.seriesNumber);
1015 				props.setInt("size", fileInfo.size);
1016 				props.setLong("crc32", fileInfo.crc32);
1017 				props.storeToXML(gzipOutputStream, "CoolReader current document info");
1018 				gzipOutputStream.close();
1019 				outputStream.close();
1020 				m_remoteAccess.writeFile(REMOTE_FOLDER_PATH + "/current.xml.gz", outputStream.toByteArray(), null, new OnOperationCompleteListener<Boolean>() {
1021 					@Override
1022 					public void onCompleted(Boolean result, boolean ok) {
1023 						if (!ok)
1024 							return;        // onFailed() will be called
1025 						if (checkAbort())
1026 							return;
1027 						m_currentOperationIndex++;
1028 						updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
1029 						if (null != result && result) {
1030 							log.d("file created or updated.");
1031 							onContinue.run();
1032 						} else {
1033 							log.e("UploadCurrentBookInfoSyncOperation: failed to save current book info");
1034 							doneFailed("Failed to save current book info");
1035 						}
1036 					}
1037 
1038 					@Override
1039 					public void onFailed(Exception e) {
1040 						log.e("UploadCurrentBookInfoSyncOperation: write failed: " + e.toString());
1041 						doneFailed(e.toString());
1042 					}
1043 				});
1044 			} catch (Exception e) {
1045 				log.e("UploadCurrentBookInfoSyncOperation: " + e.toString());
1046 				doneFailed(e.toString());
1047 			}
1048 		}
1049 	}
1050 
1051 	protected class DownloadCurrentBookInfoSyncOperation extends SyncOperation {
1052 		@Override
call(Runnable onContinue)1053 		void call(Runnable onContinue) {
1054 			log.d("Starting DownloadCurrentBookInfoSyncOperation operation...");
1055 
1056 			m_remoteAccess.readFile(REMOTE_FOLDER_PATH + "/current.xml.gz", new OnOperationCompleteListener<InputStream>() {
1057 				@Override
1058 				public void onCompleted(InputStream inputStream, boolean ok) {
1059 					if (!ok)
1060 						return;		// onFailed() will be called
1061 					if (checkAbort())
1062 						return;
1063 					m_currentOperationIndex++;
1064 					updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
1065 					if (null != inputStream) {
1066 						try {
1067 							Properties props = new Properties();
1068 							GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
1069 							props.loadFromXML(gzipInputStream);
1070 							int version = props.getInt("version", -1);
1071 							if (CURRENTBOOKINFO_BUNDLE_VERSION == version) {
1072 								FileInfo fileInfo = new FileInfo();
1073 								fileInfo.filename = props.getProperty("filename");
1074 								fileInfo.authors = props.getProperty("authors");
1075 								fileInfo.title = props.getProperty("title");
1076 								fileInfo.series = props.getProperty("series");
1077 								fileInfo.seriesNumber = props.getInt("seriesNumber", 0);
1078 								fileInfo.size = props.getInt("size", 0);
1079 								fileInfo.crc32 = props.getLong("crc32", 0);
1080 								syncSetCurrentBook(fileInfo);
1081 								onContinue.run();
1082 							} else {
1083 								throw new RuntimeException("Incompatible file info version " + version);
1084 							}
1085 						} catch (Exception e) {
1086 							log.e("DownloadCurrentBookInfoSyncOperation: " + e.toString());
1087 							doneFailed(e.toString());
1088 						}
1089 					} else {
1090 						log.e("DownloadCurrentBookInfoSyncOperation: input stream is null");
1091 						doneFailed("Can't read bookmarks bundle");
1092 					}
1093 				}
1094 
1095 				@Override
1096 				public void onFailed(Exception e) {
1097 					log.e("DownloadCurrentBookInfoSyncOperation: read failed: " + e.toString());
1098 					doneFailed(e.toString());
1099 				}
1100 			});
1101 		}
1102 	}
1103 
1104 	protected class UploadCurrentBookBodySyncOperation extends SyncOperation {
1105 		private final BookInfo bookInfo;
1106 
UploadCurrentBookBodySyncOperation(BookInfo bookInfo)1107 		UploadCurrentBookBodySyncOperation(BookInfo bookInfo) {
1108 			this.bookInfo = new BookInfo(bookInfo);
1109 		}
1110 
1111 		@Override
call(Runnable onContinue)1112 		void call(Runnable onContinue) {
1113 			log.d("Starting UploadCurrentBookBodySyncOperation operation...");
1114 
1115 			FileInfo fileInfo = bookInfo.getFileInfo();
1116 			// 1. Check if file already exist on Drive
1117 			// TODO: replace CRC32 with SHA512
1118 			// TODO: check if CRC32 on arcname != fileInfo.crc32
1119 			String fingerprint = Long.toString(fileInfo.crc32, 10);
1120 			String bookFilePath = (fileInfo.isArchive && null != fileInfo.arcname) ? fileInfo.arcname : fileInfo.pathname;
1121 			int bookFileSize = (fileInfo.isArchive && null != fileInfo.arcname) ? fileInfo.arcsize : fileInfo.size;
1122 			File bookFile = new File(bookFilePath);
1123 			String bookFileName = bookFile.getName();
1124 			// ".cr3/some_file.fb2.123456.data.gz"
1125 			// ".cr3/some_file.fb2.zip.123456.data.gz"
1126 			String cloudFilePath = REMOTE_FOLDER_PATH + "/" + bookFileName + "." + fingerprint + ".data.gz";
1127 			m_remoteAccess.stat(cloudFilePath, true, new OnOperationCompleteListener<FileMetadata>() {
1128 				@Override
1129 				public void onCompleted(FileMetadata metadata, boolean ok) {
1130 					if (!ok)
1131 						return;		// onFailed() will be called
1132 					if (checkAbort())
1133 						return;
1134 					boolean needUpload = false;
1135 					if (null != metadata) {
1136 						// OK, remote file exists, compare them
1137 						if (bookFileSize != metadata.getCustomPropSourceSize() || !fingerprint.equals(metadata.getCustomPropFingerprint()))
1138 							needUpload = true;
1139 					} else {
1140 						// remote file not exists
1141 						needUpload = true;
1142 					}
1143 					if (needUpload) {
1144 						// Upload file content
1145 						try {
1146 							byte[] buff = new byte[4096];
1147 							FileInputStream inputStream = new FileInputStream(bookFile);
1148 							ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
1149 							GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
1150 							int rb;
1151 							while ((rb = inputStream.read(buff)) > 0) {
1152 								gzipOutputStream.write(buff, 0, rb);
1153 							}
1154 							gzipOutputStream.close();
1155 							outputStream.close();
1156 							HashMap<String, String> customProps = new HashMap<String, String>(2);
1157 							customProps.put(FileMetadata.CUSTOM_PROP_FINGERPRINT, fingerprint);
1158 							customProps.put(FileMetadata.CUSTOM_PROP_SOURCE_SIZE, Integer.toString(bookFileSize, 10));
1159 							log.d("UploadCurrentBookBodySyncOperation: starting to upload file: " + bookFileName);
1160 							m_remoteAccess.writeFile(cloudFilePath, outputStream.toByteArray(), customProps, new OnOperationCompleteListener<Boolean>() {
1161 								@Override
1162 								public void onCompleted(Boolean result, boolean ok) {
1163 									if (!ok)
1164 										return;        // onFailed() will be called
1165 									if (checkAbort())
1166 										return;
1167 									m_currentOperationIndex++;
1168 									updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
1169 									if (null != result && result) {
1170 										log.d("file created or updated.");
1171 										onContinue.run();
1172 									} else {
1173 										log.e("UploadCurrentBookInfoSyncOperation: failed to save current book info");
1174 										doneFailed("Failed to save current book info");
1175 									}
1176 								}
1177 
1178 								@Override
1179 								public void onFailed(Exception e) {
1180 									log.e("UploadCurrentBookBodySyncOperation: upload failed: " + e.toString());
1181 									doneFailed(e.toString());
1182 								}
1183 							});
1184 						} catch (Exception e) {
1185 							log.e("UploadCurrentBookBodySyncOperation: file read failed: " + e.toString());
1186 							doneFailed(e.toString());
1187 						}
1188 					} else {
1189 						log.d("book data file already exist on the cloud.");
1190 						m_currentOperationIndex++;
1191 						updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
1192 						onContinue.run();
1193 					}
1194 				}
1195 
1196 				@Override
1197 				public void onFailed(Exception e) {
1198 					log.e("UploadCurrentBookBodySyncOperation: stat failed: " + e.toString());
1199 					doneFailed(e.toString());
1200 				}
1201 			});
1202 		}
1203 	}
1204 
1205 	protected class DownloadAllBooksBodySyncOperation extends SyncOperation {
1206 		@Override
call(Runnable onContinue)1207 		void call(Runnable onContinue) {
1208 			log.d("Starting DownloadAllBooksBodySyncOperation operation...");
1209 
1210 			m_remoteAccess.list(REMOTE_FOLDER_PATH, true, new OnOperationCompleteListener<FileMetadataList>() {
1211 				@Override
1212 				public void onCompleted(FileMetadataList metalist, boolean ok) {
1213 					if (!ok)
1214 						return;		// onFailed() will be called
1215 					if (checkAbort())
1216 						return;
1217 					m_currentOperationIndex++;
1218 					updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
1219 					if (null != metalist) {
1220 						ArrayList<DownloadInfo> filesToCheck = new ArrayList<DownloadInfo>();
1221 						for (FileMetadata meta : metalist) {
1222 							if (meta.fileName.length() > 8 && meta.fileName.endsWith(".data.gz")) {
1223 								String fingerprint = meta.getCustomPropFingerprint();
1224 								int sourceSize = meta.getCustomPropSourceSize();
1225 								String sourceName = meta.fileName;
1226 								// drop ".data.gz"
1227 								sourceName = sourceName.substring(0, sourceName.length() - 8);
1228 								int dotPos = sourceName.lastIndexOf('.');
1229 								if (dotPos > 0) {
1230 									// drop <fingerprint>
1231 									// When changing the type of fingerprint, symbols '.' may appear in it,
1232 									//  accordingly this will require changes.
1233 									if (fingerprint.length() > 0 && sourceSize > 0) {
1234 										String cloudFileName = REMOTE_FOLDER_PATH + "/" + meta.fileName;
1235 										filesToCheck.add(new DownloadInfo(cloudFileName, meta));
1236 									} else {
1237 										log.d("Found unsuitable file for synchronization: " + meta.fileName);
1238 									}
1239 								}
1240 							}
1241 						}
1242 						if (filesToCheck.size() > 0) {
1243 							// check if files already exist in DB on this device...
1244 							BackgroundThread.instance().executeGUI(() -> m_coolReader.waitForCRDBService(() -> {
1245 								// GUI thread
1246 								CRDBService.LocalBinder db = m_coolReader.getDB();
1247 								ArrayList<DownloadInfo> filesToDownload = new ArrayList<DownloadInfo>();
1248 								ArrayList<String> fingerprints = new ArrayList<String>(filesToCheck.size());
1249 								for (DownloadInfo info : filesToCheck) {
1250 									fingerprints.add(info.m_meta.getCustomPropFingerprint());
1251 								}
1252 								db.findByFingerprints(fingerprints.size() + 1, fingerprints, fileList -> {
1253 									// db service thread
1254 									for (DownloadInfo reqinfo : filesToCheck) {
1255 										boolean found = false;
1256 										for (FileInfo fileInfo : fileList) {
1257 											long req_crc32 = -1;
1258 											try {
1259 												req_crc32 = Long.parseLong(reqinfo.m_meta.getCustomPropFingerprint());
1260 											} catch (Exception ignored) {}
1261 											if (fileInfo.crc32 == req_crc32 && fileInfo.exists()) {
1262 												found = true;
1263 												break;
1264 											}
1265 										}
1266 										if (!found)
1267 											filesToDownload.add(reqinfo);
1268 									}
1269 									if (filesToDownload.size() > 0) {
1270 										SyncOperation op = DownloadAllBooksBodySyncOperation.this;
1271 										for (DownloadInfo info : filesToDownload) {
1272 											log.d("scheduling book loading from file: \"" + info.m_filepath + "\"");
1273 											SyncOperation downloadBookBody = new DownloadBookBodySyncOperation(info);
1274 											insertOperation(op, downloadBookBody);
1275 											op = downloadBookBody;
1276 										}
1277 									} else {
1278 										log.d("No files to download from cloud...");
1279 									}
1280 									onContinue.run();
1281 								});
1282 							}));
1283 						} else {
1284 							log.d("No files found for downloading from cloud...");
1285 							onContinue.run();
1286 						}
1287 					} else {
1288 						log.e("DownloadAllBooksBodySyncOperation: list return null");
1289 						doneFailed("list return null");
1290 					}
1291 				}
1292 
1293 				@Override
1294 				public void onFailed(Exception e) {
1295 					log.e("DownloadAllBooksBodySyncOperation: list failed: " + e.toString());
1296 					doneFailed(e.toString());
1297 				}
1298 			});
1299 		}
1300 	}
1301 
1302 	protected class DownloadBookBodySyncOperation extends SyncOperation {
1303 		DownloadInfo downloadInfo;
1304 
DownloadBookBodySyncOperation(DownloadInfo downloadInfo)1305 		DownloadBookBodySyncOperation(DownloadInfo downloadInfo) {
1306 			this.downloadInfo = downloadInfo;
1307 		}
1308 
1309 		@Override
call(Runnable onContinue)1310 		void call(Runnable onContinue) {
1311 			log.d("Starting DownloadBookBodySyncOperation operation...");
1312 
1313 			m_remoteAccess.readFile(downloadInfo.m_filepath, new OnOperationCompleteListener<InputStream>() {
1314 				@Override
1315 				public void onCompleted(InputStream inputStream, boolean ok) {
1316 					if (!ok)
1317 						return;		// onFailed() will be called
1318 					if (checkAbort())
1319 						return;
1320 					m_currentOperationIndex++;
1321 					updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
1322 					if (null != inputStream) {
1323 						String sourceName = downloadInfo.m_meta.fileName;
1324 						int sourceSize = downloadInfo.m_meta.getCustomPropSourceSize();
1325 						if (sourceName.length() > 8) {
1326 							// drop ".data.gz"
1327 							sourceName = sourceName.substring(0, sourceName.length() - 8);
1328 							int dotPos = sourceName.lastIndexOf('.');
1329 							if (dotPos > 0) {
1330 								// drop <fingerprint>
1331 								// When changing the type of fingerprint, symbols '.' may appear in it,
1332 								//  accordingly this will require changes.
1333 								sourceName = sourceName.substring(0, dotPos);
1334 								String fingerprint = downloadInfo.m_meta.getCustomPropFingerprint();
1335 								if (fingerprint.length() > 0) {
1336 									File outDir = getDownloadDir();
1337 									if (null != outDir && outDir.exists()) {
1338 										File file = new File(outDir.getAbsolutePath(), sourceName);
1339 										boolean skipDownloading = false;
1340 										if (file.exists()) {
1341 											// TODO: add an extra check to see if these two files match
1342 											if (file.length() == sourceSize) {
1343 												log.d("DownloadBookBodySyncOperation: file \"" + sourceName + "\" already exists, the size is the same");
1344 												skipDownloading = true;
1345 											} else {
1346 												log.d("DownloadBookBodySyncOperation: file \"" + sourceName + "\" already exists, the size varies, finding new name...");
1347 												file = Utils.getReplacementFile(file);
1348 												if (null == file) {
1349 													log.e("DownloadBookBodySyncOperation: failed to generate replacement file name for \"" + sourceName + "\"!");
1350 													skipDownloading = true;
1351 												}
1352 											}
1353 										}
1354 										if (!skipDownloading) {
1355 											// Save to file
1356 											try {
1357 												GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
1358 												FileOutputStream outputStream = new FileOutputStream(file);
1359 												byte[] buff = new byte[4096];
1360 												int totalSize = 0;
1361 												int rb;
1362 												while ((rb = gzipInputStream.read(buff)) > 0) {
1363 													outputStream.write(buff, 0, rb);
1364 													totalSize += rb;
1365 												}
1366 												gzipInputStream.close();
1367 												inputStream.close();
1368 												outputStream.close();
1369 												if (totalSize != sourceSize)
1370 													throw new IOException("Invalid size of file, saved " + totalSize + ", must be " + sourceSize);
1371 												// parse & save in DB
1372 												BackgroundThread.instance().executeGUI(() -> m_coolReader.waitForCRDBService(() -> {
1373 													Services.getScanner().scanDirectory(m_coolReader.getDB(), new FileInfo(outDir), () -> onContinue.run(), false, new Scanner.ScanControl());
1374 												}));
1375 												log.d("File \"" + file.getAbsolutePath() + "\" successfully saved.");
1376 											} catch (Exception e) {
1377 												log.e("DownloadBookBodySyncOperation: failed to save file: " + e.toString());
1378 												// ignoring, goto next task
1379 												onContinue.run();
1380 											}
1381 										} else {
1382 											log.d("DownloadBookBodySyncOperation: skipping downloading file \"" + sourceName + "\"");
1383 											onContinue.run();
1384 										}
1385 									} else {
1386 										log.e("DownloadBookBodySyncOperation: outdir not exits: \"" + outDir + "\"!");
1387 										onContinue.run();
1388 									}
1389 								} else {
1390 									log.e("DownloadBookBodySyncOperation: fingerprint is empty!");
1391 									onContinue.run();
1392 								}
1393 							} else {
1394 								log.e("DownloadBookBodySyncOperation: Invalid file name!");
1395 								onContinue.run();
1396 							}
1397 						} else {
1398 							log.e("DownloadBookBodySyncOperation: source file name too short!");
1399 							onContinue.run();
1400 						}
1401 					} else {
1402 						log.e("DownloadBookBodySyncOperation: can't read book data!");
1403 						doneFailed("Can't read bookmarks bundle");
1404 					}
1405 				}
1406 
1407 				@Override
1408 				public void onFailed(Exception e) {
1409 					log.e("DownloadBookBodySyncOperation: readFile failed: " + e.toString());
1410 					doneFailed(e.toString());
1411 				}
1412 			});
1413 		}
1414 	}
1415 
1416 	protected class DeleteAllAppDataSyncOperation extends SyncOperation {
1417 		@Override
call(Runnable onContinue)1418 		void call(Runnable onContinue) {
1419 			log.d("Starting DeleteAllAppDataSyncOperation operation...");
1420 
1421 			// use trash() instead of delete() so that the user can recover the data later.
1422 			m_remoteAccess.trash(REMOTE_FOLDER_PATH, new OnOperationCompleteListener<Boolean>() {
1423 				@Override
1424 				public void onCompleted(Boolean result, boolean ok) {
1425 					if (!ok)
1426 						return;		// onFailed() will be called
1427 					if (checkAbort())
1428 						return;
1429 					m_currentOperationIndex++;
1430 					updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
1431 					if (null != result && result) {
1432 						log.d("data removed.");
1433 						onContinue.run();
1434 					} else {
1435 						log.e("DeleteAllAppDataSyncOperation: failed to remove");
1436 						doneFailed("Failed to remove application data");
1437 					}
1438 				}
1439 
1440 				@Override
1441 				public void onFailed(Exception e) {
1442 					log.e("DeleteAllAppDataSyncOperation: delete failed: " + e.toString());
1443 					doneFailed(e.toString());
1444 				}
1445 			});
1446 		}
1447 	}
1448 
1449 	protected class DeleteOldDataSyncOperation extends SyncOperation {
1450 		@Override
call(Runnable onContinue)1451 		void call(Runnable onContinue) {
1452 			log.d("Starting DeleteOldDataSyncOperation operation...");
1453 			m_remoteAccess.list(REMOTE_FOLDER_PATH, true, new OnOperationCompleteListener<FileMetadataList>() {
1454 				@Override
1455 				public void onCompleted(FileMetadataList metalist, boolean ok) {
1456 					if (!ok)
1457 						return;		// onFailed() will be called
1458 					if (checkAbort())
1459 						return;
1460 					m_currentOperationIndex++;
1461 					updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
1462 					if (null == metalist) {
1463 						// `metalist` can be null, which means that the folder you are looking for was not found.
1464 						log.d(REMOTE_FOLDER_PATH + " don't exist yet...");
1465 					} else {
1466 						Date now = new Date();
1467 						SyncOperation op = DeleteOldDataSyncOperation.this;
1468 						for (FileMetadata meta : metalist) {
1469 							if (meta.fileName.endsWith(".bmk.xml.gz") ||
1470 									meta.fileName.endsWith(".data.gz")) {
1471 								if (meta.modifiedDate.getTime() + 86400000 * (long) m_dataKeepAlive < now.getTime()) {
1472 									log.d("scheduling to remove file \"" + meta.fileName + "\".");
1473 									String fileName = REMOTE_FOLDER_PATH + "/" + meta.fileName;
1474 									SyncOperation deleteFileOp = new DeleteFileSyncOperation(fileName);
1475 									insertOperation(op, deleteFileOp);
1476 									op = deleteFileOp;
1477 								}
1478 							}
1479 						}
1480 					}
1481 					onContinue.run();
1482 				}
1483 
1484 				@Override
1485 				public void onFailed(Exception e) {
1486 					log.e("DeleteOldBookmarksOperation: list failed: " + e.toString());
1487 					doneFailed(e.toString());
1488 				}
1489 			});
1490 		}
1491 	}
1492 
1493 	protected class DeleteFileSyncOperation extends SyncOperation {
1494 		private String m_fileName;
1495 
DeleteFileSyncOperation(String fileName)1496 		public DeleteFileSyncOperation(String fileName) {
1497 			m_fileName = fileName;
1498 		}
1499 
1500 		@Override
call(Runnable onContinue)1501 		void call(Runnable onContinue) {
1502 			log.d("Starting DeleteFileSyncOperation operation...");
1503 			m_remoteAccess.trash(m_fileName, new OnOperationCompleteListener<Boolean>() {
1504 				@Override
1505 				public void onCompleted(Boolean result, boolean ok) {
1506 					if (!ok)
1507 						return;		// onFailed() will be called
1508 					if (checkAbort())
1509 						return;
1510 					m_currentOperationIndex++;
1511 					updateSyncProgress(m_currentOperationIndex, m_totalOperationsCount);
1512 					if (result)
1513 						log.d("File \"" + m_fileName + "\" trashed.");
1514 					onContinue.run();
1515 				}
1516 
1517 				@Override
1518 				public void onFailed(Exception e) {
1519 					log.e("DeleteFileSyncOperation: trash failed: " + e.toString());
1520 					doneFailed(e.toString());
1521 				}
1522 			});
1523 		}
1524 	}
1525 
1526 	protected SyncOperation m_doneOp = new SyncOperation() {
1527 		@Override
1528 		void call(Runnable onContinue) {
1529 			log.d("All operation completed.");
1530 			doneSuccessfully();
1531 		}
1532 	};
1533 
1534 
1535 	/**
1536 	 * Starts the process of data synchronization - downloading from a remote service.
1537 	 * @param flags Synchtonization flags, @see SYNC_FLAG_*.
1538 	 */
startSyncFrom(int flags)1539 	public void startSyncFrom(int flags) {
1540 		if (m_isBusy)
1541 			return;
1542 		m_flags = flags;
1543 		// make "Sync From" operations chain and run it
1544 		m_isAbortRequested = false;
1545 		setSyncStarted(SyncDirection.SyncFrom);
1546 
1547 		clearOperation();
1548 		if ((m_flags & SYNC_FLAG_SHOW_SIGN_IN) != 0 || m_remoteAccess.needSignInRepeat())
1549 			addOperation(new SignInSyncOperation());
1550 		else
1551 			addOperation(new SignInQuietlySyncOperation());
1552 		addOperation(new CheckAppFolderSyncOperation());
1553 		addOperation(new CheckLockFileSyncOperation());
1554 		if ((m_flags & SYNC_FLAG_FORCE) != 0 || hasTarget(SyncTarget.SETTINGS)) {
1555 			if ((m_flags & SYNC_FLAG_QUIETLY) != 0)
1556 				addOperation(new DownloadSettingsSyncOperation(REMOTE_SETTINGS_FILE_PATH));
1557 			else
1558 				addOperation(new CheckDownloadSettingsSyncOperation(m_coolReader.getSettingsFile(0), REMOTE_SETTINGS_FILE_PATH));
1559 		}
1560 		if ((m_flags & SYNC_FLAG_FORCE) != 0 || hasTarget(SyncTarget.CURRENTBOOKBODY))
1561 			addOperation(new DownloadAllBooksBodySyncOperation());
1562 		if ((m_flags & SYNC_FLAG_FORCE) != 0 || hasTarget(SyncTarget.BOOKMARKS))
1563 			addOperation(new DownloadAllBookmarksSyncOperation());
1564 		if (m_dataKeepAlive > 0)		// if equals 0 -> disabled
1565 			addOperation(new DeleteOldDataSyncOperation());
1566 		if ((m_flags & SYNC_FLAG_FORCE) != 0 || hasTarget(SyncTarget.CURRENTBOOKINFO))
1567 			addOperation(new DownloadCurrentBookInfoSyncOperation());
1568 		addOperation(new RemoveLockFileSyncOperation());
1569 		addOperation(m_doneOp);
1570 		startOperations();
1571 	}
1572 
1573 	/**
1574 	 * Starts the process of data synchronization - uploading to a remote service.
1575 	 * @param bookInfo book information to synchronize.
1576 	 * @param flags Synchtonization flags, @see SYNC_FLAG_*.
1577 	 */
startSyncTo(BookInfo bookInfo, int flags)1578 	public void startSyncTo(BookInfo bookInfo, int flags) {
1579 		if (m_isBusy)
1580 			return;
1581 		// make "Sync To" operations chain and run it
1582 		m_isAbortRequested = false;
1583 		m_flags = flags;
1584 		setSyncStarted(SyncDirection.SyncTo);
1585 
1586 		clearOperation();
1587 		if ((m_flags & SYNC_FLAG_SHOW_SIGN_IN) != 0 || m_remoteAccess.needSignInRepeat())
1588 			addOperation(new SignInSyncOperation());
1589 		else
1590 			addOperation(new SignInQuietlySyncOperation());
1591 		addOperation(new CheckAppFolderSyncOperation());
1592 		addOperation(new CheckLockFileSyncOperation());
1593 		if ((m_flags & SYNC_FLAG_FORCE) != 0 || hasTarget(SyncTarget.SETTINGS)) {
1594 			if ((m_flags & SYNC_FLAG_QUIETLY) != 0) {
1595 				addOperation(new UploadSettingsSyncOperation(m_coolReader.getSettingsFile(0), REMOTE_SETTINGS_FILE_PATH));
1596 			} else {
1597 				addOperation(new CheckUploadSettingsSyncOperation(m_coolReader.getSettingsFile(0), REMOTE_SETTINGS_FILE_PATH));
1598 			}
1599 		}
1600 		if (null != bookInfo && null != bookInfo.getFileInfo()) {
1601 			if ((m_flags & SYNC_FLAG_FORCE) != 0 || hasTarget(SyncTarget.BOOKMARKS))
1602 				addOperation(new UploadBookmarksSyncOperation(bookInfo));
1603 			if ((m_flags & SYNC_FLAG_FORCE) != 0 || hasTarget(SyncTarget.CURRENTBOOKINFO))
1604 				addOperation(new UploadCurrentBookInfoSyncOperation(bookInfo));
1605 			if ((m_flags & SYNC_FLAG_FORCE) != 0 || hasTarget(SyncTarget.CURRENTBOOKBODY))
1606 				addOperation(new UploadCurrentBookBodySyncOperation(bookInfo));
1607 		} else {
1608 			// bookInfo of fileInfo is null, skipping all operations related to the current book
1609 			log.d("bookInfo or fileInfo is null, skipping all operations related to the current book");
1610 		}
1611 		addOperation(new RemoveLockFileSyncOperation());
1612 		addOperation(m_doneOp);
1613 		startOperations();
1614 	}
1615 
startSyncFromOnly(int flags, SyncTarget... targets)1616 	public void startSyncFromOnly(int flags, SyncTarget... targets) {
1617 		if (m_isBusy)
1618 			return;
1619 		// check target
1620 		boolean all_disabled = true;
1621 		for (SyncTarget target : targets) {
1622 			if (hasTarget(target)) {
1623 				all_disabled = false;
1624 				break;
1625 			}
1626 		}
1627 		if (all_disabled)
1628 			return;
1629 		m_flags = flags;
1630 		// make "Sync From" operations chain and run it
1631 		m_isAbortRequested = false;
1632 		setSyncStarted(SyncDirection.SyncFrom);
1633 
1634 		clearOperation();
1635 		if ((m_flags & SYNC_FLAG_SHOW_SIGN_IN) != 0 || m_remoteAccess.needSignInRepeat())
1636 			addOperation(new SignInSyncOperation());
1637 		else
1638 			addOperation(new SignInQuietlySyncOperation());
1639 		addOperation(new CheckAppFolderSyncOperation());
1640 		addOperation(new CheckLockFileSyncOperation());
1641 		for (SyncTarget target : targets) {
1642 			switch (target) {
1643 				case SETTINGS:
1644 					if ((m_flags & SYNC_FLAG_QUIETLY) != 0)
1645 						addOperation(new DownloadSettingsSyncOperation(REMOTE_SETTINGS_FILE_PATH));
1646 					else
1647 						addOperation(new CheckDownloadSettingsSyncOperation(m_coolReader.getSettingsFile(0), REMOTE_SETTINGS_FILE_PATH));
1648 					break;
1649 				case BOOKMARKS:
1650 					addOperation(new DownloadAllBookmarksSyncOperation());
1651 					break;
1652 				case CURRENTBOOKINFO:
1653 					addOperation(new DownloadCurrentBookInfoSyncOperation());
1654 					break;
1655 				case CURRENTBOOKBODY:
1656 					addOperation(new DownloadAllBooksBodySyncOperation());
1657 					break;
1658 			}
1659 		}
1660 		addOperation(new RemoveLockFileSyncOperation());
1661 		addOperation(m_doneOp);
1662 		startOperations();
1663 	}
1664 
startSyncToOnly(BookInfo bookInfo, int flags, SyncTarget... targets)1665 	public void startSyncToOnly(BookInfo bookInfo, int flags, SyncTarget... targets) {
1666 		if (m_isBusy)
1667 			return;
1668 		// check target
1669 		boolean all_disabled = true;
1670 		for (SyncTarget target : targets) {
1671 			if (hasTarget(target)) {
1672 				all_disabled = false;
1673 				break;
1674 			}
1675 		}
1676 		if (all_disabled)
1677 			return;
1678 		m_flags = flags;
1679 		// make "Sync To" operations chain and run it
1680 		m_isAbortRequested = false;
1681 		setSyncStarted(SyncDirection.SyncTo);
1682 
1683 		clearOperation();
1684 		if ((m_flags & SYNC_FLAG_SHOW_SIGN_IN) != 0 || m_remoteAccess.needSignInRepeat())
1685 			addOperation(new SignInSyncOperation());
1686 		else
1687 			addOperation(new SignInQuietlySyncOperation());
1688 		addOperation(new CheckAppFolderSyncOperation());
1689 		addOperation(new CheckLockFileSyncOperation());
1690 		for (SyncTarget target : targets) {
1691 			switch (target) {
1692 				case SETTINGS:
1693 					if ((m_flags & SYNC_FLAG_QUIETLY) != 0)
1694 						addOperation(new UploadSettingsSyncOperation(m_coolReader.getSettingsFile(0), REMOTE_SETTINGS_FILE_PATH));
1695 					else
1696 						addOperation(new CheckUploadSettingsSyncOperation(m_coolReader.getSettingsFile(0), REMOTE_SETTINGS_FILE_PATH));
1697 					break;
1698 				case BOOKMARKS:
1699 					if (null != bookInfo && null != bookInfo.getFileInfo())
1700 						addOperation(new UploadBookmarksSyncOperation(bookInfo));
1701 					break;
1702 				case CURRENTBOOKINFO:
1703 					if (null != bookInfo && null != bookInfo.getFileInfo())
1704 						addOperation(new UploadCurrentBookInfoSyncOperation(bookInfo));
1705 					break;
1706 				case CURRENTBOOKBODY:
1707 					if (null != bookInfo && null != bookInfo.getFileInfo())
1708 						addOperation(new UploadCurrentBookBodySyncOperation(bookInfo));
1709 					break;
1710 			}
1711 		}
1712 		addOperation(new RemoveLockFileSyncOperation());
1713 		addOperation(m_doneOp);
1714 		startOperations();
1715 	}
1716 
cleanupAndSignOut()1717 	public void cleanupAndSignOut() {
1718 		if (m_isBusy)
1719 			return;
1720 		// make "Cleanup & Sign out" operations chain and run it
1721 		m_isAbortRequested = false;
1722 		setSyncStarted(SyncDirection.SyncTo);
1723 
1724 		clearOperation();
1725 		addOperation(new SignInSyncOperation());
1726 		addOperation(new DeleteAllAppDataSyncOperation());
1727 		addOperation(new SignOutSyncOperation());
1728 		addOperation(m_doneOp);
1729 		startOperations();
1730 	}
1731 
signOut()1732 	public void signOut() {
1733 		if (m_isBusy)
1734 			return;
1735 		// make "Sign Out" operations chain and run it
1736 		m_isAbortRequested = false;
1737 		setSyncStarted(SyncDirection.SyncTo);
1738 
1739 		clearOperation();
1740 		// don't add SignIn operation
1741 		addOperation(new SignOutSyncOperation());
1742 		addOperation(m_doneOp);
1743 		startOperations();
1744 	}
1745 
getCurrentBookBookmarksData(BookInfo bookInfo)1746 	private byte[] getCurrentBookBookmarksData(BookInfo bookInfo) {
1747 		byte[] data = null;
1748 		FileInfo fileInfo = bookInfo.getFileInfo();
1749 		if (null != fileInfo) {
1750 			try {
1751 				ByteArrayOutputStream ostream = new ByteArrayOutputStream();
1752 				GZIPOutputStream gzipOutputStream = new GZIPOutputStream(ostream);
1753 				XmlSerializer serializer = Xml.newSerializer();
1754 				serializer.setOutput(gzipOutputStream, "utf-8");
1755 				serializer.startDocument("UTF-8", true);
1756 				// root tag
1757 				serializer.startTag("", "root");
1758 				serializer.attribute("", "version", Integer.toString(BOOKMARKS_BUNDLE_VERSION));
1759 				// Write file info
1760 				serializer.startTag("", "fileinfo");
1761 				// fileName
1762 				serializer.startTag("", "filename");
1763 				serializer.text(fileInfo.filename);
1764 				serializer.endTag("", "filename");
1765 				// Authors
1766 				serializer.startTag("", "authors");
1767 				serializer.text(fileInfo.authors);
1768 				serializer.endTag("", "authors");
1769 				// Title
1770 				serializer.startTag("", "title");
1771 				serializer.text(fileInfo.title);
1772 				serializer.endTag("", "title");
1773 				// Series
1774 				serializer.startTag("", "series");
1775 				serializer.text(fileInfo.series);
1776 				serializer.endTag("", "series");
1777 				// Series Number
1778 				serializer.startTag("", "seriesNumber");
1779 				serializer.text(Integer.toString(fileInfo.seriesNumber, 10));
1780 				serializer.endTag("", "seriesNumber");
1781 				// File Size
1782 				serializer.startTag("", "size");
1783 				serializer.text(Integer.toString(fileInfo.size, 10));
1784 				serializer.endTag("", "size");
1785 				// File CRC32
1786 				serializer.startTag("", "crc32");
1787 				serializer.text(Long.toString(fileInfo.crc32, 10));
1788 				serializer.endTag("", "crc32");
1789 				serializer.endTag("", "fileinfo");
1790 				// Write bookmarks
1791 				serializer.startTag("", "bookmarks");
1792 				for (Bookmark bk : bookInfo.getAllBookmarks()) {
1793 					serializer.startTag("", "bookmark");
1794 					// Id
1795 					if (null == bk.getId())
1796 						serializer.attribute("", "id", "null");
1797 					else
1798 						serializer.attribute("", "id", bk.getId().toString());
1799 					// Type
1800 					serializer.attribute("", "type", Integer.toString(bk.getType(), 10));
1801 					// Percent
1802 					serializer.attribute("", "percent", Integer.toString(bk.getPercent(), 10));
1803 					// Shortcut
1804 					serializer.attribute("", "shortcut", Integer.toString(bk.getShortcut(), 10));
1805 					// Start Position
1806 					serializer.startTag("", "startpos");
1807 					serializer.text(bk.getStartPos());
1808 					serializer.endTag("", "startpos");
1809 					// End Position
1810 					serializer.startTag("", "endpos");
1811 					serializer.text(bk.getEndPos());
1812 					serializer.endTag("", "endpos");
1813 					// Title Text
1814 					serializer.startTag("", "title");
1815 					serializer.text(bk.getTitleText());
1816 					serializer.endTag("", "title");
1817 					// Position Text
1818 					serializer.startTag("", "pos");
1819 					serializer.text(bk.getPosText());
1820 					serializer.endTag("", "pos");
1821 					// Comment Text
1822 					serializer.startTag("", "comment");
1823 					serializer.text(bk.getCommentText());
1824 					serializer.endTag("", "comment");
1825 					// Timestamp
1826 					serializer.startTag("", "timestamp");
1827 					serializer.text(Long.toString(bk.getTimeStamp(), 10));
1828 					serializer.endTag("", "timestamp");
1829 					// Time elapsed
1830 					serializer.startTag("", "elapsed");
1831 					serializer.text(Long.toString(bk.getTimeElapsed(), 10));
1832 					serializer.endTag("", "elapsed");
1833 
1834 					serializer.endTag("", "bookmark");
1835 				}
1836 				serializer.endTag("", "bookmarks");
1837 				serializer.endTag("", "root");
1838 				serializer.endDocument();
1839 				serializer.flush();
1840 				gzipOutputStream.close();
1841 				ostream.close();
1842 				data = ostream.toByteArray();
1843 			} catch (Exception e) {
1844 				log.e("getCurrentBookBookmarksData() failed: " + e.toString());
1845 			}
1846 		}
1847 		return data;
1848 	}
1849 
syncBookmarks(InputStream inputStream)1850 	private void syncBookmarks(InputStream inputStream) {
1851 		log.v("syncBookmarks()");
1852 		// 1. Read & parse bookmarks from stream
1853 		FileInfo fileInfo = null;
1854 		List<Bookmark> bookmarks = null;
1855 		try {
1856 			SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
1857 			SAXParser saxParser = saxParserFactory.newSAXParser();
1858 			XMLReader xmlReader = saxParser.getXMLReader();
1859 			BookmarksContentHandler contentHandler = new BookmarksContentHandler();
1860 			xmlReader.setContentHandler(contentHandler);
1861 			GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
1862 			xmlReader.parse(new InputSource(gzipInputStream));
1863 			int version = contentHandler.getVersion();
1864 			if (BOOKMARKS_BUNDLE_VERSION == version) {
1865 				fileInfo = contentHandler.getFileInfo();
1866 				bookmarks = contentHandler.getBookmarks();
1867 			} else {
1868 				throw new RuntimeException("incompatible bookmarks version " + version);
1869 			}
1870 		} catch (Exception e) {
1871 			log.e("syncBookmarks() failed: " + e.toString());
1872 		}
1873 		// 2. Sync with ReaderView
1874 		if (null != fileInfo && null != bookmarks) {
1875 			log.v("fileInfo & bookmarks decoded.");
1876 			// Sync with ReaderView and DB
1877 			final List<Bookmark> finalBookmarks = bookmarks;
1878 			final FileInfo finalFileInfo = fileInfo;
1879 			BackgroundThread.instance().executeGUI(() -> m_coolReader.waitForCRDBService(() -> {
1880 				CRDBService.BookSearchCallback searchCallback = fileList -> {
1881 					// Check this files for existence
1882 					ArrayList<FileInfo> newList = new ArrayList<FileInfo>();
1883 					for (FileInfo fi : fileList) {
1884 						if (fi.exists())
1885 							newList.add(fi);
1886 					}
1887 					if (fileList.size() != newList.size())
1888 						fileList = newList;
1889 					if (0 == fileList.size()) {
1890 						// this book not found in db
1891 						// find in filesystem?
1892 						log.e("file \"" + finalFileInfo.filename + "\" not found in database!");
1893 					} else {
1894 						if (fileList.size() > 1) {
1895 							// multiple files found that matches this fileInfo
1896 							// select first or nothing?
1897 							log.e("multiple files with name \"" + finalFileInfo.filename + "\" found, using first.");
1898 							// TODO: show message
1899 						}
1900 						FileInfo dbFileInfo = fileList.get(0);
1901 						BookInfo bookInfo = new BookInfo(dbFileInfo);
1902 						for (Bookmark bk : finalBookmarks) {
1903 							bookInfo.addBookmark(bk);
1904 						}
1905 						log.d("Book \"" + dbFileInfo + "\" found, syncing...");
1906 						if (null != m_onStatusListener)
1907 							m_onStatusListener.onBookmarksLoaded(bookInfo, (m_flags & SYNC_FLAG_ASK_CHANGED) != 0);
1908 					}
1909 				};
1910 				ArrayList<String> fingerprints = new ArrayList<String>();
1911 				fingerprints.add(Long.toString(finalFileInfo.crc32));
1912 				m_coolReader.getDB().findByFingerprints(2, fingerprints, fileList -> {
1913 					if (!fileList.isEmpty()) {
1914 						searchCallback.onBooksFound(fileList);
1915 					} else {
1916 						// fallback, try to find by pattern
1917 						m_coolReader.getDB().findByPatterns(2, finalFileInfo.authors, finalFileInfo.title, finalFileInfo.series, finalFileInfo.filename, searchCallback);
1918 					}
1919 				});
1920 			}));
1921 		}
1922 	}
1923 
syncSetCurrentBook(final FileInfo fileInfo)1924 	private void syncSetCurrentBook(final FileInfo fileInfo) {
1925 		log.v("syncSetCurrentBook()");
1926 		BackgroundThread.instance().executeGUI(() -> m_coolReader.waitForCRDBService(() -> {
1927 			CRDBService.BookSearchCallback searchCallback = fileList -> {
1928 				// Check this files for existence
1929 				ArrayList<FileInfo> newList = new ArrayList<FileInfo>();
1930 				for (FileInfo fi : fileList) {
1931 					if (fi.exists())
1932 						newList.add(fi);
1933 				}
1934 				if (fileList.size() != newList.size())
1935 					fileList = newList;
1936 				if (0 == fileList.size()) {
1937 					// this book not found in db
1938 					// find in filesystem?
1939 					log.e("file \"" + fileInfo.filename + "\" not found in database!");
1940 					if (null != m_onStatusListener) {
1941 						m_onStatusListener.onFileNotFound(fileInfo);
1942 					}
1943 				} else {
1944 					if (fileList.size() > 1) {
1945 						// multiple files found that matches this fileInfo
1946 						// select first or nothing?
1947 						log.e("multiple files with name \"" + fileInfo.filename + "\" found, using first.");
1948 						// TODO: show message
1949 					}
1950 					FileInfo dbFileInfo = fileList.get(0);
1951 					if (null != m_onStatusListener) {
1952 						log.d("Book \"" + dbFileInfo + "\" found, call listener to load this book...");
1953 						m_onStatusListener.onCurrentBookInfoLoaded(fileList.get(0), (m_flags & SYNC_FLAG_ASK_CHANGED) != 0);
1954 					}
1955 				}
1956 			};
1957 			ArrayList<String> fingerprints = new ArrayList<String>();
1958 			fingerprints.add(Long.toString(fileInfo.crc32));
1959 			m_coolReader.getDB().findByFingerprints(2, fingerprints, fileList -> {
1960 				if (!fileList.isEmpty()) {
1961 					searchCallback.onBooksFound(fileList);
1962 				} else {
1963 					// fallback, try to find by pattern
1964 					m_coolReader.getDB().findByPatterns(2, fileInfo.authors, fileInfo.title, fileInfo.series, fileInfo.filename, searchCallback);
1965 				}
1966 			});
1967 		}));
1968 	}
1969 
getDownloadDir()1970 	private File getDownloadDir() {
1971 		FileInfo downloadDir = Services.getScanner().getDownloadDirectory();
1972 		if (null == downloadDir)
1973 			return null;
1974 		String subdir = "cloud-sync";
1975 		File result = new File(downloadDir.getPathName(), subdir);
1976 		result.mkdirs();
1977 		downloadDir.findItemByPathName(result.getAbsolutePath());
1978 		//log.d("getDownloadDir(): returning " + result.getAbsolutePath());
1979 		return result;
1980 	}
1981 
1982 }
1983