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