1 // Main Class 2 package org.coolreader; 3 4 import android.Manifest; 5 import android.app.Activity; 6 import android.content.BroadcastReceiver; 7 import android.content.ContentResolver; 8 import android.content.Context; 9 import android.content.Intent; 10 import android.content.IntentFilter; 11 import android.content.SharedPreferences; 12 import android.content.pm.PackageManager; 13 import android.media.AudioManager; 14 import android.net.Uri; 15 import android.os.Build; 16 import android.os.Bundle; 17 import android.os.Debug; 18 import android.speech.tts.TextToSpeech; 19 import android.view.LayoutInflater; 20 import android.view.View; 21 import android.view.ViewGroup; 22 23 import androidx.documentfile.provider.DocumentFile; 24 25 import org.coolreader.Dictionaries.DictionaryException; 26 import org.coolreader.crengine.AboutDialog; 27 import org.coolreader.crengine.BackgroundThread; 28 import org.coolreader.crengine.BaseActivity; 29 import org.coolreader.crengine.BookInfo; 30 import org.coolreader.crengine.BookInfoEditDialog; 31 import org.coolreader.crengine.Bookmark; 32 import org.coolreader.crengine.BookmarksDlg; 33 import org.coolreader.crengine.BrowserViewLayout; 34 import org.coolreader.crengine.CRRootView; 35 import org.coolreader.crengine.CRToolBar; 36 import org.coolreader.crengine.DeviceInfo; 37 import org.coolreader.crengine.Engine; 38 import org.coolreader.crengine.ErrorDialog; 39 import org.coolreader.crengine.FileBrowser; 40 import org.coolreader.crengine.FileInfo; 41 import org.coolreader.crengine.FileInfoOperationListener; 42 import org.coolreader.crengine.InterfaceTheme; 43 import org.coolreader.crengine.L; 44 import org.coolreader.crengine.LogcatSaver; 45 import org.coolreader.crengine.Logger; 46 import org.coolreader.crengine.N2EpdController; 47 import org.coolreader.crengine.OPDSCatalogEditDialog; 48 import org.coolreader.crengine.OptionsDialog; 49 import org.coolreader.crengine.PositionProperties; 50 import org.coolreader.crengine.Properties; 51 import org.coolreader.crengine.ReaderAction; 52 import org.coolreader.crengine.ReaderView; 53 import org.coolreader.crengine.ReaderViewLayout; 54 import org.coolreader.crengine.Services; 55 import org.coolreader.crengine.TTSToolbarDlg; 56 import org.coolreader.crengine.Utils; 57 import org.coolreader.donations.CRDonationService; 58 import org.coolreader.sync2.OnSyncStatusListener; 59 import org.coolreader.sync2.Synchronizer; 60 import org.coolreader.sync2.googledrive.GoogleDriveRemoteAccess; 61 import org.coolreader.tts.OnTTSCreatedListener; 62 import org.koekak.android.ebookdownloader.SonyBookSelector; 63 64 import java.io.File; 65 import java.io.FileOutputStream; 66 import java.io.InputStream; 67 import java.io.OutputStream; 68 import java.lang.reflect.Field; 69 import java.text.SimpleDateFormat; 70 import java.util.ArrayList; 71 import java.util.Date; 72 import java.util.Locale; 73 import java.util.Map; 74 import java.util.Timer; 75 import java.util.TimerTask; 76 77 public class CoolReader extends BaseActivity { 78 public static final Logger log = L.create("cr"); 79 80 private ReaderView mReaderView; 81 private ReaderViewLayout mReaderFrame; 82 private FileBrowser mBrowser; 83 private View mBrowserTitleBar; 84 private CRToolBar mBrowserToolBar; 85 private BrowserViewLayout mBrowserFrame; 86 private CRRootView mHomeFrame; 87 private Engine mEngine; 88 //View startupView; 89 //CRDB mDB; 90 private ViewGroup mCurrentFrame; 91 private ViewGroup mPreviousFrame; 92 93 private boolean mSyncGoogleDriveEnabled = false; 94 private boolean mSyncGoogleDriveEnabledPrev = false; 95 private boolean mCloudSyncAskConfirmations = true; 96 private boolean mSyncGoogleDriveEnabledSettings = false; 97 private boolean mSyncGoogleDriveEnabledBookmarks = false; 98 private boolean mSyncGoogleDriveEnabledCurrentBookInfo = false; 99 private boolean mSyncGoogleDriveEnabledCurrentBookBody = false; 100 private int mCloudSyncBookmarksKeepAlive = 14; 101 private int mSyncGoogleDriveAutoSavePeriod = 0; 102 private int mSyncGoogleDriveErrorsCount = 0; 103 private Synchronizer mGoogleDriveSync; 104 private Timer mGoogleDriveAutoSaveTimer = null; 105 // can be add more synchronizers 106 private boolean mSuppressSettingsCopyToCloud; 107 108 private String mOptionAppearance = "0"; 109 110 private String mFileToOpenFromExt = null; 111 112 private int mOpenDocumentTreeCommand = ODT_CMD_NO_SPEC; 113 private FileInfo mOpenDocumentTreeArg = null; 114 115 private boolean isFirstStart = true; 116 private boolean phoneStateChangeHandlerInstalled = false; 117 private int initialBatteryState = -1; 118 private BroadcastReceiver intentReceiver; 119 120 private boolean justCreated = false; 121 private boolean activityIsRunning = false; 122 123 private boolean dataDirIsRemoved = false; 124 125 private static final int REQUEST_CODE_STORAGE_PERM = 1; 126 private static final int REQUEST_CODE_READ_PHONE_STATE_PERM = 2; 127 private static final int REQUEST_CODE_GOOGLE_DRIVE_SIGN_IN = 3; 128 private static final int REQUEST_CODE_OPEN_DOCUMENT_TREE = 11; 129 130 // open document tree activity commands 131 private static final int ODT_CMD_NO_SPEC = -1; 132 private static final int ODT_CMD_DEL_FILE = 1; 133 private static final int ODT_CMD_DEL_FOLDER = 2; 134 private static final int ODT_CMD_SAVE_LOGCAT = 3; 135 136 /** 137 * Called when the activity is first created. 138 */ 139 @Override onCreate(Bundle savedInstanceState)140 protected void onCreate(Bundle savedInstanceState) { 141 startServices(); 142 143 log.i("CoolReader.onCreate() entered"); 144 super.onCreate(savedInstanceState); 145 146 isFirstStart = true; 147 justCreated = true; 148 activityIsRunning = false; 149 150 // Can request only one set of permissions at a time 151 // Then request all permission at a time. 152 requestStoragePermissions(); 153 154 // apply settings 155 onSettingsChanged(settings(), null); 156 157 mEngine = Engine.getInstance(this); 158 159 //requestWindowFeature(Window.FEATURE_NO_TITLE); 160 161 //========================================== 162 // Battery state listener 163 intentReceiver = new BroadcastReceiver() { 164 165 @Override 166 public void onReceive(Context context, Intent intent) { 167 int level = intent.getIntExtra("level", 0); 168 if (mReaderView != null) 169 mReaderView.setBatteryState(level); 170 else 171 initialBatteryState = level; 172 } 173 174 }; 175 registerReceiver(intentReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 176 177 setVolumeControlStream(AudioManager.STREAM_MUSIC); 178 179 if (initialBatteryState >= 0 && mReaderView != null) 180 mReaderView.setBatteryState(initialBatteryState); 181 182 //========================================== 183 // Donations related code 184 try { 185 186 mDonationService = new CRDonationService(this); 187 mDonationService.bind(); 188 SharedPreferences pref = getSharedPreferences(DONATIONS_PREF_FILE, 0); 189 try { 190 mTotalDonations = pref.getFloat(DONATIONS_PREF_TOTAL_AMOUNT, 0.0f); 191 } catch (Exception e) { 192 log.e("exception while reading total donations from preferences", e); 193 } 194 } catch (VerifyError e) { 195 log.e("Exception while trying to initialize billing service for donations"); 196 } 197 198 N2EpdController.n2MainActivity = this; 199 200 showRootWindow(); 201 202 if (null != Engine.getExternalSettingsDirName()) { 203 // if external data directory created or already exist. 204 if (!Engine.DATADIR_IS_EXIST_AT_START && getExtDataDirCreateTime() > 0) { 205 dataDirIsRemoved = true; 206 log.e("DataDir removed by other application!"); 207 } 208 } 209 210 log.i("CoolReader.onCreate() exiting"); 211 } 212 213 public final static boolean CLOSE_BOOK_ON_STOP = false; 214 215 boolean mDestroyed = false; 216 217 @Override onDestroy()218 protected void onDestroy() { 219 220 log.i("CoolReader.onDestroy() entered"); 221 if (!CLOSE_BOOK_ON_STOP && mReaderView != null) 222 mReaderView.close(); 223 224 if (tts != null) { 225 tts.shutdown(); 226 tts = null; 227 ttsInitialized = false; 228 ttsError = false; 229 } 230 231 232 if (mHomeFrame != null) 233 mHomeFrame.onClose(); 234 mDestroyed = true; 235 236 //if ( mReaderView!=null ) 237 // mReaderView.close(); 238 239 //if ( mHistory!=null && mDB!=null ) { 240 //history.saveToDB(); 241 //} 242 243 244 // if ( BackgroundThread.instance()!=null ) { 245 // BackgroundThread.instance().quit(); 246 // } 247 248 //mEngine = null; 249 if (intentReceiver != null) { 250 unregisterReceiver(intentReceiver); 251 intentReceiver = null; 252 } 253 254 //=========================== 255 // Donations support code 256 if (mDonationService != null) 257 mDonationService.unbind(); 258 259 if (mReaderView != null) { 260 mReaderView.destroy(); 261 } 262 mReaderView = null; 263 264 log.i("CoolReader.onDestroy() exiting"); 265 super.onDestroy(); 266 267 Services.stopServices(); 268 } 269 getReaderView()270 public ReaderView getReaderView() { 271 return mReaderView; 272 } 273 274 @Override applyAppSetting(String key, String value)275 public void applyAppSetting(String key, String value) { 276 super.applyAppSetting(key, value); 277 boolean flg = "1".equals(value); 278 if (key.equals(PROP_APP_DICTIONARY)) { 279 setDict(value); 280 } else if (key.equals(PROP_APP_DICTIONARY_2)) { 281 setDict2(value); 282 } else if (key.equals(PROP_TOOLBAR_APPEARANCE)) { 283 setToolbarAppearance(value); 284 } else if (key.equals(PROP_APP_BOOK_SORT_ORDER)) { 285 if (mBrowser != null) 286 mBrowser.setSortOrder(value); 287 } else if (key.equals(PROP_APP_SHOW_COVERPAGES)) { 288 if (mBrowser != null) 289 mBrowser.setCoverPagesEnabled(flg); 290 } else if (key.equals(PROP_APP_BOOK_PROPERTY_SCAN_ENABLED)) { 291 Services.getScanner().setDirScanEnabled(flg); 292 } else if (key.equals(PROP_FONT_FACE)) { 293 if (mBrowser != null) 294 mBrowser.setCoverPageFontFace(value); 295 } else if (key.equals(PROP_APP_COVERPAGE_SIZE)) { 296 if (mBrowser != null) 297 mBrowser.setCoverPageSizeOption(Utils.parseInt(value, 0, 0, 2)); 298 } else if (key.equals(PROP_APP_FILE_BROWSER_SIMPLE_MODE)) { 299 if (mBrowser != null) 300 mBrowser.setSimpleViewMode(flg); 301 } else if (key.equals(PROP_APP_CLOUDSYNC_GOOGLEDRIVE_ENABLED)) { 302 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 303 mSyncGoogleDriveEnabledPrev = mSyncGoogleDriveEnabled; 304 mSyncGoogleDriveEnabled = flg; 305 updateGoogleDriveSynchronizer(); 306 } 307 } else if (key.equals(PROP_APP_CLOUDSYNC_CONFIRMATIONS)) { 308 mCloudSyncAskConfirmations = flg; 309 } else if (key.equals(PROP_APP_CLOUDSYNC_GOOGLEDRIVE_SETTINGS)) { 310 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 311 mSyncGoogleDriveEnabledSettings = flg; 312 updateGoogleDriveSynchronizer(); 313 } 314 } else if (key.equals(PROP_APP_CLOUDSYNC_GOOGLEDRIVE_BOOKMARKS)) { 315 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 316 mSyncGoogleDriveEnabledBookmarks = flg; 317 updateGoogleDriveSynchronizer(); 318 } 319 } else if (key.equals(PROP_APP_CLOUDSYNC_GOOGLEDRIVE_CURRENTBOOK_INFO)) { 320 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 321 mSyncGoogleDriveEnabledCurrentBookInfo = flg; 322 updateGoogleDriveSynchronizer(); 323 } 324 } else if (key.equals(PROP_APP_CLOUDSYNC_GOOGLEDRIVE_CURRENTBOOK_BODY)) { 325 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 326 mSyncGoogleDriveEnabledCurrentBookBody = flg; 327 updateGoogleDriveSynchronizer(); 328 } 329 } else if (key.equals(PROP_APP_CLOUDSYNC_GOOGLEDRIVE_AUTOSAVEPERIOD)) { 330 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 331 mSyncGoogleDriveAutoSavePeriod = Utils.parseInt(value, 0, 0, 30); 332 updateGoogleDriveSynchronizer(); 333 } 334 } else if (key.equals(PROP_APP_CLOUDSYNC_DATA_KEEPALIVE)) { 335 mCloudSyncBookmarksKeepAlive = Utils.parseInt(value, 14, 0, 365); 336 updateGoogleDriveSynchronizer(); 337 } else if (key.equals(PROP_APP_FILE_BROWSER_HIDE_EMPTY_GENRES)) { 338 if (null != mBrowser) { 339 mBrowser.setHideEmptyGenres(flg); 340 } 341 } else if (key.equals(PROP_APP_TTS_ENGINE)) { 342 ttsEnginePackage = value; 343 if (null != mReaderView && mReaderView.isTTSActive() && null != tts) { 344 // Stop current TTS process & create new 345 mReaderView.stopTTS(); 346 if (tts != null) { 347 // Cleanup previous TTS 348 tts.shutdown(); 349 tts = null; 350 ttsInitialized = false; 351 ttsError = false; 352 } 353 initTTS(tts -> { 354 TTSToolbarDlg dlg = mReaderView.getTTSToolbar(); 355 dlg.changeTTS(tts); 356 }); 357 } 358 } 359 // 360 } 361 buildGoogleDriveSynchronizer()362 private void buildGoogleDriveSynchronizer() { 363 if (null != mGoogleDriveSync) 364 return; 365 // build synchronizer instance 366 // DeviceInfo.getSDKLevel() not applicable here -> compile error about Android API compatibility 367 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 368 GoogleDriveRemoteAccess googleDriveRemoteAccess = new GoogleDriveRemoteAccess(this, 30); 369 mGoogleDriveSync = new Synchronizer(this, googleDriveRemoteAccess, getString(R.string.app_name), REQUEST_CODE_GOOGLE_DRIVE_SIGN_IN); 370 mGoogleDriveSync.setOnSyncStatusListener(new OnSyncStatusListener() { 371 @Override 372 public void onSyncStarted(Synchronizer.SyncDirection direction, boolean showProgress, boolean interactively) { 373 if (Synchronizer.SyncDirection.SyncFrom == direction) { 374 log.d("Starting synchronization from Google Drive"); 375 } else if (Synchronizer.SyncDirection.SyncTo == direction) { 376 log.d("Starting synchronization to Google Drive"); 377 } 378 if (null != mReaderView) { 379 if (showProgress) { 380 mReaderView.showCloudSyncProgress(100); 381 } 382 } 383 } 384 385 @Override 386 public void OnSyncProgress(Synchronizer.SyncDirection direction, boolean showProgress, int current, int total, boolean interactively) { 387 log.v("sync progress: current=" + current + "; total=" + total); 388 if (null != mReaderView) { 389 if (showProgress) { 390 int total_ = total; 391 if (current > total_) 392 total_ = current; 393 mReaderView.showCloudSyncProgress(10000 * current / total_); 394 } 395 } 396 } 397 398 @Override 399 public void onSyncCompleted(Synchronizer.SyncDirection direction, boolean showProgress, boolean interactively) { 400 if (Synchronizer.SyncDirection.SyncFrom == direction) { 401 log.d("Google Drive SyncFrom successfully completed"); 402 } else if (Synchronizer.SyncDirection.SyncTo == direction) { 403 log.d("Google Drive SyncTo successfully completed"); 404 } 405 if (interactively) 406 showToast(R.string.googledrive_sync_completed); 407 if (showProgress) { 408 if (null != mReaderView) { 409 // Hide sync indicator 410 mReaderView.hideCloudSyncProgress(); 411 } 412 } 413 if (mSyncGoogleDriveEnabled) 414 mSyncGoogleDriveErrorsCount = 0; 415 } 416 417 @Override 418 public void onSyncError(Synchronizer.SyncDirection direction, String errorString) { 419 // Hide sync indicator 420 if (null != mReaderView) { 421 mReaderView.hideCloudSyncProgress(); 422 } 423 if (null != errorString) 424 showToast(R.string.googledrive_sync_failed_with, errorString); 425 else 426 showToast(R.string.googledrive_sync_failed); 427 if (mSyncGoogleDriveEnabled) { 428 mSyncGoogleDriveErrorsCount++; 429 if (mSyncGoogleDriveErrorsCount >= 3) { 430 showToast(R.string.googledrive_sync_failed_disabled); 431 log.e("More than 3 sync failures in a row, auto sync disabled."); 432 mSyncGoogleDriveEnabled = false; 433 } 434 } 435 } 436 437 @Override 438 public void onAborted(Synchronizer.SyncDirection direction) { 439 // Hide sync indicator 440 if (null != mReaderView) { 441 mReaderView.hideCloudSyncProgress(); 442 } 443 showToast(R.string.googledrive_sync_aborted); 444 } 445 446 @Override 447 public void onSettingsLoaded(Properties settings, boolean interactively) { 448 // Apply downloaded (filtered) settings 449 mSuppressSettingsCopyToCloud = true; 450 mergeSettings(settings, true); 451 } 452 453 @Override 454 public void onBookmarksLoaded(BookInfo bookInfo, boolean interactively) { 455 waitForCRDBService(() -> { 456 // TODO: ask the user whether to import new bookmarks. 457 BookInfo currentBook = null; 458 int currentPos = -1; 459 if (null != mReaderView) { 460 currentBook = mReaderView.getBookInfo(); 461 if (null != currentBook) 462 currentPos = currentBook.getLastPosition().getPercent(); 463 } 464 Services.getHistory().updateBookInfo(bookInfo); 465 getDB().saveBookInfo(bookInfo); 466 if (null != currentBook) { 467 FileInfo currentFileInfo = currentBook.getFileInfo(); 468 if (null != currentFileInfo) { 469 if (currentFileInfo.baseEquals((bookInfo.getFileInfo()))) { 470 // if the book indicated by the bookInfo is currently open. 471 Bookmark lastPos = bookInfo.getLastPosition(); 472 if (null != lastPos) { 473 if (!interactively) { 474 mReaderView.goToBookmark(lastPos); 475 } else { 476 if (Math.abs(currentPos - lastPos.getPercent()) > 10) { // 0.1% 477 askQuestion(R.string.cloud_synchronization_from_, R.string.sync_confirmation_new_reading_position, 478 () -> mReaderView.goToBookmark(lastPos), null); 479 } 480 } 481 } 482 } 483 } 484 } 485 }); 486 } 487 488 @Override 489 public void onCurrentBookInfoLoaded(FileInfo fileInfo, boolean interactively) { 490 FileInfo current = null; 491 if (null != mReaderView) { 492 BookInfo bookInfo = mReaderView.getBookInfo(); 493 if (null != bookInfo) 494 current = bookInfo.getFileInfo(); 495 } 496 if (!fileInfo.baseEquals(current)) { 497 if (!interactively) { 498 loadDocument(fileInfo, false); 499 } else { 500 String shortBookInfo = ""; 501 if (null != fileInfo.authors && !fileInfo.authors.isEmpty()) 502 shortBookInfo = "\"" + fileInfo.authors + ", "; 503 else 504 shortBookInfo = "\""; 505 shortBookInfo += fileInfo.title + "\""; 506 String question = getString(R.string.sync_confirmation_other_book, shortBookInfo); 507 askQuestion(getString(R.string.cloud_synchronization_from_), question, () -> loadDocument(fileInfo, false), null); 508 } 509 } 510 } 511 512 @Override 513 public void onFileNotFound(FileInfo fileInfo) { 514 if (null == fileInfo) 515 return; 516 String docInfo = "Unknown"; 517 if (null != fileInfo.title && !fileInfo.authors.isEmpty()) 518 docInfo = fileInfo.title; 519 if (null != fileInfo.authors && !fileInfo.authors.isEmpty()) 520 docInfo = fileInfo.authors + ", " + docInfo; 521 if (null != fileInfo.filename && !fileInfo.filename.isEmpty()) 522 docInfo += " (" + fileInfo.filename + ")"; 523 showToast(R.string.sync_info_no_such_document, docInfo); 524 } 525 526 }); 527 } 528 } 529 updateGoogleDriveSynchronizer()530 private void updateGoogleDriveSynchronizer() { 531 // DeviceInfo.getSDKLevel() not applicable here -> lint error about Android API compatibility 532 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 533 if (mSyncGoogleDriveEnabled) { 534 if (null == mGoogleDriveSync) { 535 log.d("Google Drive sync is enabled."); 536 buildGoogleDriveSynchronizer(); 537 } 538 mGoogleDriveSync.setTarget(Synchronizer.SyncTarget.SETTINGS, mSyncGoogleDriveEnabledSettings); 539 mGoogleDriveSync.setTarget(Synchronizer.SyncTarget.BOOKMARKS, mSyncGoogleDriveEnabledBookmarks); 540 mGoogleDriveSync.setTarget(Synchronizer.SyncTarget.CURRENTBOOKINFO, mSyncGoogleDriveEnabledCurrentBookInfo); 541 mGoogleDriveSync.setTarget(Synchronizer.SyncTarget.CURRENTBOOKBODY, mSyncGoogleDriveEnabledCurrentBookBody); 542 mGoogleDriveSync.setBookmarksKeepAlive(mCloudSyncBookmarksKeepAlive); 543 if (null != mGoogleDriveAutoSaveTimer) { 544 mGoogleDriveAutoSaveTimer.cancel(); 545 mGoogleDriveAutoSaveTimer = null; 546 } 547 if (mSyncGoogleDriveAutoSavePeriod > 0) { 548 mGoogleDriveAutoSaveTimer = new Timer(); 549 mGoogleDriveAutoSaveTimer.schedule(new TimerTask() { 550 @Override 551 public void run() { 552 if (activityIsRunning && null != mGoogleDriveSync) { 553 mGoogleDriveSync.startSyncTo(getCurrentBookInfo(), Synchronizer.SYNC_FLAG_QUIETLY | Synchronizer.SYNC_FLAG_SHOW_PROGRESS); 554 } 555 } 556 }, mSyncGoogleDriveAutoSavePeriod * 60000, mSyncGoogleDriveAutoSavePeriod * 60000); 557 } 558 } else { 559 if (null != mGoogleDriveAutoSaveTimer) { 560 mGoogleDriveAutoSaveTimer.cancel(); 561 mGoogleDriveAutoSaveTimer = null; 562 } 563 if (mSyncGoogleDriveEnabledPrev && null != mGoogleDriveSync) { 564 log.d("Google Drive autosync is disabled."); 565 if (false) { 566 // TODO: Don't remove authorization on Google Account here, move this into OptionsDialog 567 // ask user: cleanup & sign out 568 askConfirmation(R.string.googledrive_disabled_cleanup_question, 569 () -> { 570 if (null != mGoogleDriveSync) { 571 mGoogleDriveSync.abort(() -> { 572 if (null != mGoogleDriveSync) { 573 mGoogleDriveSync.cleanupAndSignOut(); 574 mGoogleDriveSync = null; 575 } 576 }); 577 } 578 }, 579 () -> { 580 if (null != mGoogleDriveSync) { 581 mGoogleDriveSync.abort(() -> { 582 if (null != mGoogleDriveSync) { 583 mGoogleDriveSync.signOut(); 584 mGoogleDriveSync = null; 585 } 586 }); 587 } 588 } 589 ); 590 } 591 } 592 } 593 } 594 } 595 forceSyncToGoogleDrive()596 public void forceSyncToGoogleDrive() { 597 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 598 if (null == mGoogleDriveSync) 599 buildGoogleDriveSynchronizer(); 600 mGoogleDriveSync.setBookmarksKeepAlive(mCloudSyncBookmarksKeepAlive); 601 mGoogleDriveSync.startSyncTo(getCurrentBookInfo(), Synchronizer.SYNC_FLAG_SHOW_SIGN_IN | Synchronizer.SYNC_FLAG_FORCE | Synchronizer.SYNC_FLAG_SHOW_PROGRESS | Synchronizer.SYNC_FLAG_ASK_CHANGED); 602 } 603 } 604 forceSyncFromGoogleDrive()605 public void forceSyncFromGoogleDrive() { 606 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 607 if (null == mGoogleDriveSync) 608 buildGoogleDriveSynchronizer(); 609 mGoogleDriveSync.setBookmarksKeepAlive(mCloudSyncBookmarksKeepAlive); 610 mGoogleDriveSync.startSyncFrom(Synchronizer.SYNC_FLAG_SHOW_SIGN_IN | Synchronizer.SYNC_FLAG_FORCE | Synchronizer.SYNC_FLAG_SHOW_PROGRESS | Synchronizer.SYNC_FLAG_ASK_CHANGED); 611 } 612 } 613 getCurrentBookInfo()614 private BookInfo getCurrentBookInfo() { 615 BookInfo bookInfo = null; 616 if (mReaderView != null) { 617 bookInfo = mReaderView.getBookInfo(); 618 if (null != bookInfo && null == bookInfo.getFileInfo()) { 619 // nullify if fileInfo is null 620 bookInfo = null; 621 } 622 } 623 return bookInfo; 624 } 625 626 @Override setFullscreen(boolean fullscreen)627 public void setFullscreen(boolean fullscreen) { 628 super.setFullscreen(fullscreen); 629 if (mReaderFrame != null) 630 mReaderFrame.updateFullscreen(fullscreen); 631 } 632 633 @Override onNewIntent(Intent intent)634 protected void onNewIntent(Intent intent) { 635 log.i("onNewIntent : " + intent); 636 if (mDestroyed) { 637 log.e("engine is already destroyed"); 638 return; 639 } 640 processIntent(intent); 641 } 642 processIntent(Intent intent)643 private boolean processIntent(Intent intent) { 644 log.d("intent=" + intent); 645 if (intent == null) 646 return false; 647 String fileToOpen = null; 648 mFileToOpenFromExt = null; 649 Uri uri = null; 650 if (Intent.ACTION_VIEW.equals(intent.getAction())) { 651 uri = intent.getData(); 652 intent.setData(null); 653 if (uri != null) { 654 fileToOpen = filePathFromUri(uri); 655 } 656 } 657 if (fileToOpen == null && intent.getExtras() != null) { 658 log.d("extras=" + intent.getExtras()); 659 fileToOpen = intent.getExtras().getString(OPEN_FILE_PARAM); 660 } 661 if (fileToOpen != null) { 662 mFileToOpenFromExt = fileToOpen; 663 log.d("FILE_TO_OPEN = " + fileToOpen); 664 final String finalFileToOpen = fileToOpen; 665 loadDocument(fileToOpen, null, () -> BackgroundThread.instance().postGUI(() -> { 666 // if document not loaded show error & then root window 667 ErrorDialog errDialog = new ErrorDialog(CoolReader.this, CoolReader.this.getString(R.string.error), CoolReader.this.getString(R.string.cant_open_file, finalFileToOpen)); 668 errDialog.setOnDismissListener(dialog -> showRootWindow()); 669 errDialog.show(); 670 }, 500), true); 671 return true; 672 } else if (null != uri) { 673 // TODO: calculate fingerprint for uri and find fileInfo in DB 674 log.d("URI_TO_OPEN = " + uri); 675 final String uriString = uri.toString(); 676 mFileToOpenFromExt = uriString; 677 loadDocumentFromUri(uri, () -> showToast(R.string.opened_from_stream), () -> BackgroundThread.instance().postGUI(() -> { 678 // if document not loaded show error & then root window 679 ErrorDialog errDialog = new ErrorDialog(CoolReader.this, CoolReader.this.getString(R.string.error), CoolReader.this.getString(R.string.cant_open_file, uriString)); 680 errDialog.setOnDismissListener(dialog -> showRootWindow()); 681 errDialog.show(); 682 }, 500)); 683 return true; 684 } else { 685 log.d("No file to open"); 686 return false; 687 } 688 } 689 filePathFromUri(Uri uri)690 private String filePathFromUri(Uri uri) { 691 if (null == uri) 692 return null; 693 String filePath = null; 694 String scheme = uri.getScheme(); 695 String host = uri.getHost(); 696 if ("file".equals(scheme)) { 697 filePath = uri.getPath(); 698 // patch for opening of books from ReLaunch (under Nook Simple Touch) 699 if (null != filePath) { 700 if (filePath.contains("%2F")) 701 filePath = filePath.replace("%2F", "/"); 702 } 703 } else if ("content".equals(scheme)) { 704 if (uri.getEncodedPath().contains("%00")) 705 filePath = uri.getEncodedPath(); 706 else 707 filePath = uri.getPath(); 708 if (null != filePath) { 709 // parse uri from system filemanager 710 if (filePath.contains("%00")) { 711 // splitter between archive file name and inner file. 712 filePath = filePath.replace("%00", "@/"); 713 filePath = Uri.decode(filePath); 714 } 715 if ("com.android.externalstorage.documents".equals(host)) { 716 // application "Files" by Google, package="com.android.externalstorage.documents" 717 if (filePath.matches("^/document/.*:.*$")) { 718 // decode special uri form: /document/primary:<somebody> 719 // /document/XXXX-XXXX:<somebody> 720 String shortcut = filePath.replaceFirst("^/document/(.*):.*$", "$1"); 721 String mountRoot = Engine.getMountRootByShortcut(shortcut); 722 if (mountRoot != null) { 723 filePath = filePath.replaceFirst("^/document/.*:(.*)$", mountRoot + "/$1"); 724 } 725 } 726 } else if ("com.google.android.apps.nbu.files.provider".equals(host)) { 727 // application "Files" by Google, package="com.google.android.apps.nbu.files" 728 if (filePath.startsWith("/1////")) { 729 // skip "/1///" 730 filePath = filePath.substring(5); 731 filePath = Uri.decode(filePath); 732 } else if (filePath.startsWith("/1/file:///")) { 733 // skip "/1/file://" 734 filePath = filePath.substring(10); 735 filePath = Uri.decode(filePath); 736 } 737 } else { 738 // Try some common conversions... 739 if (filePath.startsWith("/file%3A%2F%2F")) { 740 filePath = filePath.substring(14); 741 filePath = Uri.decode(filePath); 742 if (filePath.contains("%20")) { 743 filePath = filePath.replace("%20", " "); 744 } 745 } 746 } 747 } 748 } 749 if (null != filePath) { 750 File file; 751 int pos = filePath.indexOf("@/"); 752 if (pos > 0) 753 file = new File(filePath.substring(0, pos)); 754 else 755 file = new File(filePath); 756 if (!file.exists()) 757 filePath = null; 758 } 759 return filePath; 760 } 761 762 @Override onPause()763 protected void onPause() { 764 activityIsRunning = false; 765 if (mReaderView != null) { 766 mReaderView.onAppPause(); 767 } 768 Services.getCoverpageManager().removeCoverpageReadyListener(mHomeFrame); 769 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 770 if (mSyncGoogleDriveEnabled && mGoogleDriveSync != null && !mGoogleDriveSync.isBusy()) { 771 mGoogleDriveSync.startSyncTo(getCurrentBookInfo(), Synchronizer.SYNC_FLAG_QUIETLY | Synchronizer.SYNC_FLAG_SHOW_PROGRESS); 772 } 773 } 774 super.onPause(); 775 } 776 777 @Override onPostCreate(Bundle savedInstanceState)778 protected void onPostCreate(Bundle savedInstanceState) { 779 log.i("CoolReader.onPostCreate()"); 780 super.onPostCreate(savedInstanceState); 781 } 782 783 @Override onPostResume()784 protected void onPostResume() { 785 log.i("CoolReader.onPostResume()"); 786 super.onPostResume(); 787 } 788 789 // private boolean restarted = false; 790 @Override onRestart()791 protected void onRestart() { 792 log.i("CoolReader.onRestart()"); 793 //restarted = true; 794 super.onRestart(); 795 } 796 797 @Override onRestoreInstanceState(Bundle savedInstanceState)798 protected void onRestoreInstanceState(Bundle savedInstanceState) { 799 log.i("CoolReader.onRestoreInstanceState()"); 800 super.onRestoreInstanceState(savedInstanceState); 801 } 802 803 @Override onResume()804 protected void onResume() { 805 if (null == mFileToOpenFromExt) 806 log.i("CoolReader.onResume()"); 807 else 808 log.i("CoolReader.onResume(), mFileToOpenFromExt=" + mFileToOpenFromExt); 809 super.onResume(); 810 //Properties props = SettingsManager.instance(this).get(); 811 812 if (mReaderView != null) 813 mReaderView.onAppResume(); 814 815 if (DeviceInfo.EINK_SCREEN) { 816 if (DeviceInfo.EINK_SONY) { 817 SharedPreferences pref = getSharedPreferences(PREF_FILE, 0); 818 String res = pref.getString(PREF_LAST_BOOK, null); 819 if (res != null && res.length() > 0) { 820 SonyBookSelector selector = new SonyBookSelector(this); 821 long l = selector.getContentId(res); 822 if (l != 0) { 823 selector.setReadingTime(l); 824 selector.requestBookSelection(l); 825 } 826 } 827 } 828 } 829 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 830 if (mSyncGoogleDriveEnabled && mGoogleDriveSync != null) { 831 // when the program starts, the local settings file is already updated, so the local file is always newer than the remote one 832 // Therefore, the synchronization mode is quiet, i.e. without comparing modification times and without prompting the user for action. 833 // If the file is opened from an external file manager, we must disable the "currently reading book" sync operation with google drive. 834 if (!mGoogleDriveSync.isBusy()) { 835 if (null == mFileToOpenFromExt) 836 mGoogleDriveSync.startSyncFrom(Synchronizer.SYNC_FLAG_SHOW_SIGN_IN | Synchronizer.SYNC_FLAG_QUIETLY | Synchronizer.SYNC_FLAG_SHOW_PROGRESS | (mCloudSyncAskConfirmations ? Synchronizer.SYNC_FLAG_ASK_CHANGED : 0) ); 837 else 838 mGoogleDriveSync.startSyncFromOnly(Synchronizer.SYNC_FLAG_SHOW_SIGN_IN | Synchronizer.SYNC_FLAG_QUIETLY | Synchronizer.SYNC_FLAG_SHOW_PROGRESS | (mCloudSyncAskConfirmations ? Synchronizer.SYNC_FLAG_ASK_CHANGED : 0), Synchronizer.SyncTarget.SETTINGS, Synchronizer.SyncTarget.BOOKMARKS); 839 } else { 840 log.d("Synchronizer is busy!"); 841 } 842 } 843 } 844 activityIsRunning = true; 845 } 846 847 @Override onSaveInstanceState(Bundle outState)848 protected void onSaveInstanceState(Bundle outState) { 849 log.i("CoolReader.onSaveInstanceState()"); 850 super.onSaveInstanceState(outState); 851 } 852 853 static final boolean LOAD_LAST_DOCUMENT_ON_START = true; 854 855 @Override onStart()856 protected void onStart() { 857 log.i("CoolReader.onStart() version=" + getVersion()); 858 super.onStart(); 859 860 // BackgroundThread.instance().postGUI(new Runnable() { 861 // public void run() { 862 // // fixing font settings 863 // Properties settings = mReaderView.getSettings(); 864 // if (SettingsManager.instance(CoolReader.this).fixFontSettings(settings)) { 865 // log.i("Missing font settings were fixed"); 866 // mBrowser.setCoverPageFontFace(settings.getProperty(ReaderView.PROP_FONT_FACE, DeviceInfo.DEF_FONT_FACE)); 867 // mReaderView.setSettings(settings, null); 868 // } 869 // } 870 // }); 871 872 if (mHomeFrame == null) { 873 waitForCRDBService(() -> { 874 Services.getHistory().loadFromDB(getDB(), 200); 875 876 mHomeFrame = new CRRootView(CoolReader.this); 877 Services.getCoverpageManager().addCoverpageReadyListener(mHomeFrame); 878 mHomeFrame.requestFocus(); 879 880 showRootWindow(); 881 setSystemUiVisibility(); 882 883 notifySettingsChanged(); 884 885 showNotifications(); 886 }); 887 } 888 889 if (isBookOpened()) { 890 showOpenedBook(); 891 return; 892 } 893 894 if (!isFirstStart) 895 return; 896 isFirstStart = false; 897 898 if (justCreated) { 899 justCreated = false; 900 if (!processIntent(getIntent())) 901 showLastLocation(); 902 } 903 if (dataDirIsRemoved) { 904 // show message 905 ErrorDialog dlg = new ErrorDialog(this, getString(R.string.error), getString(R.string.datadir_is_removed, Engine.getExternalSettingsDirName())); 906 dlg.show(); 907 } 908 if (Engine.getExternalSettingsDirName() != null) { 909 setExtDataDirCreateTime(new Date()); 910 } else { 911 setExtDataDirCreateTime(null); 912 } 913 stopped = false; 914 915 log.i("CoolReader.onStart() exiting"); 916 } 917 918 919 private boolean stopped = false; 920 921 @Override onStop()922 protected void onStop() { 923 log.i("CoolReader.onStop() entering"); 924 // Donations support code 925 super.onStop(); 926 stopped = true; 927 // will close book at onDestroy() 928 if (CLOSE_BOOK_ON_STOP) 929 mReaderView.close(); 930 931 log.i("CoolReader.onStop() exiting"); 932 } 933 requestStoragePermissions()934 private void requestStoragePermissions() { 935 // check or request permission for storage 936 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 937 int readExtStoragePermissionCheck = checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE); 938 int writeExtStoragePermissionCheck = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); 939 ArrayList<String> needPerms = new ArrayList<>(); 940 if (PackageManager.PERMISSION_GRANTED != readExtStoragePermissionCheck) { 941 needPerms.add(Manifest.permission.READ_EXTERNAL_STORAGE); 942 } else { 943 log.i("READ_EXTERNAL_STORAGE permission already granted."); 944 } 945 if (PackageManager.PERMISSION_GRANTED != writeExtStoragePermissionCheck) { 946 needPerms.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); 947 } else { 948 log.i("WRITE_EXTERNAL_STORAGE permission already granted."); 949 } 950 if (!needPerms.isEmpty()) { 951 // TODO: Show an explanation to the user 952 // Show an explanation to the user *asynchronously* -- don't block 953 // this thread waiting for the user's response! After the user 954 // sees the explanation, try again to request the permission. 955 String[] templ = new String[0]; 956 log.i("Some permissions DENIED, requesting from user these permissions: " + needPerms.toString()); 957 // request permission from user 958 requestPermissions(needPerms.toArray(templ), REQUEST_CODE_STORAGE_PERM); 959 } 960 } 961 } 962 requestReadPhoneStatePermissions()963 private void requestReadPhoneStatePermissions() { 964 // check or request permission to read phone state 965 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 966 int phoneStatePermissionCheck = checkSelfPermission(Manifest.permission.READ_PHONE_STATE); 967 if (PackageManager.PERMISSION_GRANTED != phoneStatePermissionCheck) { 968 log.i("READ_PHONE_STATE permission DENIED, requesting from user"); 969 // TODO: Show an explanation to the user 970 // Show an explanation to the user *asynchronously* -- don't block 971 // this thread waiting for the user's response! After the user 972 // sees the explanation, try again to request the permission. 973 // request permission from user 974 requestPermissions(new String[]{Manifest.permission.READ_PHONE_STATE}, REQUEST_CODE_READ_PHONE_STATE_PERM); 975 } else { 976 log.i("READ_PHONE_STATE permission already granted."); 977 } 978 } 979 } 980 onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)981 public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 982 log.i("CoolReader.onRequestPermissionsResult()"); 983 if (REQUEST_CODE_STORAGE_PERM == requestCode) { // external storage read & write permissions 984 int ext_sd_perm_count = 0; 985 //boolean read_phone_state_granted = false; 986 for (int i = 0; i < permissions.length; i++) { 987 if (grantResults[i] == PackageManager.PERMISSION_GRANTED) 988 log.i("Permission " + permissions[i] + " GRANTED"); 989 else 990 log.i("Permission " + permissions[i] + " DENIED"); 991 if (permissions[i].compareTo(Manifest.permission.READ_EXTERNAL_STORAGE) == 0 && grantResults[i] == PackageManager.PERMISSION_GRANTED) 992 ext_sd_perm_count++; 993 else if (permissions[i].compareTo(Manifest.permission.WRITE_EXTERNAL_STORAGE) == 0 && grantResults[i] == PackageManager.PERMISSION_GRANTED) 994 ext_sd_perm_count++; 995 } 996 if (2 == ext_sd_perm_count) { 997 log.i("read&write to storage permissions GRANTED, adding sd card mount point..."); 998 Services.refreshServices(this); 999 rebaseSettings(); 1000 waitForCRDBService(() -> { 1001 getDBService().setPathCorrector(Engine.getInstance(CoolReader.this).getPathCorrector()); 1002 getDB().reopenDatabase(); 1003 Services.getHistory().loadFromDB(getDB(), 200); 1004 }); 1005 mHomeFrame.refreshView(); 1006 } 1007 if (Engine.getExternalSettingsDirName() != null) { 1008 setExtDataDirCreateTime(new Date()); 1009 } else { 1010 setExtDataDirCreateTime(null); 1011 } 1012 } else if (REQUEST_CODE_READ_PHONE_STATE_PERM == requestCode) { 1013 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 1014 log.i("read phone state permission is GRANTED, registering phone activity handler..."); 1015 PhoneStateReceiver.setPhoneActivityHandler(() -> { 1016 if (mReaderView != null) { 1017 mReaderView.stopTTS(); 1018 mReaderView.save(); 1019 } 1020 }); 1021 phoneStateChangeHandlerInstalled = true; 1022 } else { 1023 log.i("Read phone state permission is DENIED!"); 1024 } 1025 } 1026 } 1027 1028 private static Debug.MemoryInfo info = new Debug.MemoryInfo(); 1029 private static Field[] infoFields = Debug.MemoryInfo.class.getFields(); 1030 dumpFields(Field[] fields, Object obj)1031 private static String dumpFields(Field[] fields, Object obj) { 1032 StringBuilder buf = new StringBuilder(); 1033 try { 1034 for (Field f : fields) { 1035 if (buf.length() > 0) 1036 buf.append(", "); 1037 buf.append(f.getName()); 1038 buf.append("="); 1039 buf.append(f.get(obj)); 1040 } 1041 } catch (Exception e) { 1042 1043 } 1044 return buf.toString(); 1045 } 1046 dumpHeapAllocation()1047 public static void dumpHeapAllocation() { 1048 Debug.getMemoryInfo(info); 1049 log.d("nativeHeapAlloc=" + Debug.getNativeHeapAllocatedSize() + ", nativeHeapSize=" + Debug.getNativeHeapSize() + ", info: " + dumpFields(infoFields, info)); 1050 } 1051 1052 1053 @Override setCurrentTheme(InterfaceTheme theme)1054 public void setCurrentTheme(InterfaceTheme theme) { 1055 super.setCurrentTheme(theme); 1056 if (mHomeFrame != null) 1057 mHomeFrame.onThemeChange(theme); 1058 if (mBrowser != null) 1059 mBrowser.onThemeChanged(); 1060 if (mBrowserFrame != null) 1061 mBrowserFrame.onThemeChanged(theme); 1062 //getWindow().setBackgroundDrawable(theme.getActionBarBackgroundDrawableBrowser()); 1063 } 1064 directoryUpdated(FileInfo dir, FileInfo selected)1065 public void directoryUpdated(FileInfo dir, FileInfo selected) { 1066 if (dir.isOPDSRoot()) 1067 mHomeFrame.refreshOnlineCatalogs(); 1068 else if (dir.isRecentDir()) 1069 mHomeFrame.refreshRecentBooks(); 1070 if (mBrowser != null) 1071 mBrowser.refreshDirectory(dir, selected); 1072 } 1073 directoryUpdated(FileInfo dir)1074 public void directoryUpdated(FileInfo dir) { 1075 directoryUpdated(dir, null); 1076 } 1077 1078 @Override onSettingsChanged(Properties props, Properties oldProps)1079 public void onSettingsChanged(Properties props, Properties oldProps) { 1080 Properties changedProps = oldProps != null ? props.diff(oldProps) : props; 1081 if (mHomeFrame != null) { 1082 mHomeFrame.refreshOnlineCatalogs(); 1083 } 1084 if (mReaderFrame != null) { 1085 mReaderFrame.updateSettings(props); 1086 if (mReaderView != null) 1087 mReaderView.updateSettings(props); 1088 } 1089 for (Map.Entry<Object, Object> entry : changedProps.entrySet()) { 1090 String key = (String) entry.getKey(); 1091 String value = (String) entry.getValue(); 1092 applyAppSetting(key, value); 1093 } 1094 // Show/Hide soft navbar after OptionDialog is closed. 1095 applyFullscreen(getWindow()); 1096 if (!justCreated) { 1097 // Only after onStart()! 1098 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 1099 if (mSyncGoogleDriveEnabled && !mSyncGoogleDriveEnabledPrev && null != mGoogleDriveSync) { 1100 // if cloud sync has just been enabled in options dialog 1101 mGoogleDriveSync.startSyncFrom(Synchronizer.SYNC_FLAG_SHOW_SIGN_IN | Synchronizer.SYNC_FLAG_QUIETLY | Synchronizer.SYNC_FLAG_SHOW_PROGRESS | (mCloudSyncAskConfirmations ? Synchronizer.SYNC_FLAG_ASK_CHANGED : 0) ); 1102 mSyncGoogleDriveEnabledPrev = mSyncGoogleDriveEnabled; 1103 return; 1104 } 1105 if (changedProps.size() > 0) { 1106 // After options dialog is closed, sync new settings to the cloud with delay 1107 BackgroundThread.instance().postGUI(() -> { 1108 if (mSyncGoogleDriveEnabled && mSyncGoogleDriveEnabledSettings && null != mGoogleDriveSync) { 1109 if (mSuppressSettingsCopyToCloud) { 1110 // Immediately after downloading settings from Google Drive 1111 // prevent uploading settings file 1112 mSuppressSettingsCopyToCloud = false; 1113 } else if (!mGoogleDriveSync.isBusy()) { 1114 // After setting changed in OptionsDialog 1115 log.d("Some settings is changed, uploading to cloud..."); 1116 mGoogleDriveSync.startSyncToOnly(null, Synchronizer.SYNC_FLAG_SHOW_SIGN_IN | Synchronizer.SYNC_FLAG_QUIETLY | Synchronizer.SYNC_FLAG_SHOW_PROGRESS, Synchronizer.SyncTarget.SETTINGS); 1117 } 1118 } 1119 }, 500); 1120 } 1121 } 1122 } 1123 } 1124 allowLowBrightness()1125 protected boolean allowLowBrightness() { 1126 // override to force higher brightness in non-reading mode (to avoid black screen on some devices when brightness level set to small value) 1127 return mCurrentFrame == mReaderFrame; 1128 } 1129 1130 getPreviousFrame()1131 public ViewGroup getPreviousFrame() { 1132 return mPreviousFrame; 1133 } 1134 isPreviousFrameHome()1135 public boolean isPreviousFrameHome() { 1136 return mPreviousFrame != null && mPreviousFrame == mHomeFrame; 1137 } 1138 setCurrentFrame(ViewGroup newFrame)1139 private void setCurrentFrame(ViewGroup newFrame) { 1140 if (mCurrentFrame != newFrame) { 1141 mPreviousFrame = mCurrentFrame; 1142 log.i("New current frame: " + newFrame.getClass().toString()); 1143 mCurrentFrame = newFrame; 1144 setContentView(mCurrentFrame); 1145 mCurrentFrame.requestFocus(); 1146 if (mCurrentFrame != mReaderFrame) 1147 releaseBacklightControl(); 1148 if (mCurrentFrame == mHomeFrame) { 1149 // update recent books 1150 mHomeFrame.refreshRecentBooks(); 1151 setLastLocationRoot(); 1152 mCurrentFrame.invalidate(); 1153 } 1154 if (mCurrentFrame == mBrowserFrame) { 1155 // update recent books directory 1156 mBrowser.refreshDirectory(Services.getScanner().getRecentDir(), null); 1157 } 1158 onUserActivity(); 1159 } 1160 } 1161 showReader()1162 public void showReader() { 1163 runInReader(() -> { 1164 // do nothing 1165 }); 1166 } 1167 showRootWindow()1168 public void showRootWindow() { 1169 setCurrentFrame(mHomeFrame); 1170 if (activityIsRunning) { 1171 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 1172 // Save bookmarks and current reading position on the cloud 1173 if (mSyncGoogleDriveEnabled && null != mGoogleDriveSync && !mGoogleDriveSync.isBusy()) { 1174 mGoogleDriveSync.startSyncToOnly(getCurrentBookInfo(), Synchronizer.SYNC_FLAG_QUIETLY, Synchronizer.SyncTarget.BOOKMARKS); 1175 } 1176 } 1177 } 1178 } 1179 runInReader(final Runnable task)1180 private void runInReader(final Runnable task) { 1181 waitForCRDBService(() -> { 1182 if (mReaderFrame != null) { 1183 task.run(); 1184 setCurrentFrame(mReaderFrame); 1185 if (mReaderView != null && mReaderView.getSurface() != null) { 1186 mReaderView.getSurface().setFocusable(true); 1187 mReaderView.getSurface().setFocusableInTouchMode(true); 1188 mReaderView.getSurface().requestFocus(); 1189 } else { 1190 log.w("runInReader: mReaderView or mReaderView.getSurface() is null"); 1191 } 1192 } else { 1193 mReaderView = new ReaderView(CoolReader.this, mEngine, settings()); 1194 mReaderFrame = new ReaderViewLayout(CoolReader.this, mReaderView); 1195 mReaderFrame.getToolBar().setOnActionHandler(item -> { 1196 if (mReaderView != null) 1197 mReaderView.onAction(item); 1198 return true; 1199 }); 1200 task.run(); 1201 setCurrentFrame(mReaderFrame); 1202 if (mReaderView.getSurface() != null) { 1203 mReaderView.getSurface().setFocusable(true); 1204 mReaderView.getSurface().setFocusableInTouchMode(true); 1205 mReaderView.getSurface().requestFocus(); 1206 } 1207 if (initialBatteryState >= 0) 1208 mReaderView.setBatteryState(initialBatteryState); 1209 } 1210 }); 1211 } 1212 isBrowserCreated()1213 public boolean isBrowserCreated() { 1214 return mBrowserFrame != null; 1215 } 1216 runInBrowser(final Runnable task)1217 private void runInBrowser(final Runnable task) { 1218 waitForCRDBService(() -> { 1219 if (mBrowserFrame == null) { 1220 mBrowser = new FileBrowser(CoolReader.this, Services.getEngine(), Services.getScanner(), Services.getHistory(), settings().getBool(PROP_APP_FILE_BROWSER_HIDE_EMPTY_GENRES, false)); 1221 mBrowser.setCoverPagesEnabled(settings().getBool(ReaderView.PROP_APP_SHOW_COVERPAGES, true)); 1222 mBrowser.setCoverPageFontFace(settings().getProperty(ReaderView.PROP_FONT_FACE, DeviceInfo.DEF_FONT_FACE)); 1223 mBrowser.setCoverPageSizeOption(settings().getInt(ReaderView.PROP_APP_COVERPAGE_SIZE, 1)); 1224 mBrowser.setSortOrder(settings().getProperty(ReaderView.PROP_APP_BOOK_SORT_ORDER)); 1225 mBrowser.setSimpleViewMode(settings().getBool(ReaderView.PROP_APP_FILE_BROWSER_SIMPLE_MODE, false)); 1226 mBrowser.init(); 1227 1228 LayoutInflater inflater = LayoutInflater.from(CoolReader.this);// activity.getLayoutInflater(); 1229 1230 mBrowserTitleBar = inflater.inflate(R.layout.browser_status_bar, null); 1231 setBrowserTitle("Cool Reader browser window"); 1232 1233 mBrowserToolBar = new CRToolBar(CoolReader.this, ReaderAction.createList( 1234 ReaderAction.FILE_BROWSER_UP, 1235 ReaderAction.CURRENT_BOOK, 1236 ReaderAction.OPTIONS, 1237 ReaderAction.FILE_BROWSER_ROOT, 1238 ReaderAction.RECENT_BOOKS, 1239 ReaderAction.CURRENT_BOOK_DIRECTORY, 1240 ReaderAction.OPDS_CATALOGS, 1241 ReaderAction.SEARCH, 1242 ReaderAction.SCAN_DIRECTORY_RECURSIVE, 1243 ReaderAction.FILE_BROWSER_SORT_ORDER, 1244 ReaderAction.EXIT 1245 ), false); 1246 mBrowserToolBar.setBackgroundResource(R.drawable.ui_status_background_browser_dark); 1247 mBrowserToolBar.setOnActionHandler(item -> { 1248 switch (item.cmd) { 1249 case DCMD_EXIT: 1250 // 1251 finish(); 1252 break; 1253 case DCMD_FILE_BROWSER_ROOT: 1254 showRootWindow(); 1255 break; 1256 case DCMD_FILE_BROWSER_UP: 1257 mBrowser.showParentDirectory(); 1258 break; 1259 case DCMD_OPDS_CATALOGS: 1260 mBrowser.showOPDSRootDirectory(); 1261 break; 1262 case DCMD_RECENT_BOOKS_LIST: 1263 mBrowser.showRecentBooks(); 1264 break; 1265 case DCMD_SEARCH: 1266 mBrowser.showFindBookDialog(); 1267 break; 1268 case DCMD_CURRENT_BOOK: 1269 showCurrentBook(); 1270 break; 1271 case DCMD_OPTIONS_DIALOG: 1272 showBrowserOptionsDialog(); 1273 break; 1274 case DCMD_SCAN_DIRECTORY_RECURSIVE: 1275 mBrowser.scanCurrentDirectoryRecursive(); 1276 break; 1277 case DCMD_FILE_BROWSER_SORT_ORDER: 1278 mBrowser.showSortOrderMenu(); 1279 break; 1280 default: 1281 // do nothing 1282 break; 1283 } 1284 return false; 1285 }); 1286 mBrowserFrame = new BrowserViewLayout(CoolReader.this, mBrowser, mBrowserToolBar, mBrowserTitleBar); 1287 1288 // if (getIntent() == null) 1289 // mBrowser.showDirectory(Services.getScanner().getDownloadDirectory(), null); 1290 } 1291 task.run(); 1292 setCurrentFrame(mBrowserFrame); 1293 }); 1294 1295 } 1296 showBrowser()1297 public void showBrowser() { 1298 runInBrowser(() -> { 1299 // do nothing, browser is shown 1300 }); 1301 } 1302 showManual()1303 public void showManual() { 1304 loadDocument("@manual", null, null, false); 1305 } 1306 1307 public static final String OPEN_FILE_PARAM = "FILE_TO_OPEN"; 1308 loadDocument(final String item, final Runnable doneCallback, final Runnable errorCallback, final boolean forceSync)1309 public void loadDocument(final String item, final Runnable doneCallback, final Runnable errorCallback, final boolean forceSync) { 1310 runInReader(() -> mReaderView.loadDocument(item, forceSync ? () -> { 1311 if (null != doneCallback) 1312 doneCallback.run(); 1313 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 1314 // Save last opened document on cloud 1315 if (mSyncGoogleDriveEnabled && null != mGoogleDriveSync && !mGoogleDriveSync.isBusy()) { 1316 ArrayList<Synchronizer.SyncTarget> targets = new ArrayList<Synchronizer.SyncTarget>(); 1317 if (mSyncGoogleDriveEnabledCurrentBookInfo) 1318 targets.add(Synchronizer.SyncTarget.CURRENTBOOKINFO); 1319 if (mSyncGoogleDriveEnabledCurrentBookBody) 1320 targets.add(Synchronizer.SyncTarget.CURRENTBOOKBODY); 1321 if (!targets.isEmpty()) 1322 mGoogleDriveSync.startSyncToOnly(getCurrentBookInfo(), Synchronizer.SYNC_FLAG_SHOW_SIGN_IN | Synchronizer.SYNC_FLAG_QUIETLY | Synchronizer.SYNC_FLAG_SHOW_PROGRESS, targets.toArray(new Synchronizer.SyncTarget[0])); 1323 } 1324 } 1325 } : doneCallback, errorCallback)); 1326 } 1327 loadDocumentFromUri(Uri uri, Runnable doneCallback, Runnable errorCallback)1328 public void loadDocumentFromUri(Uri uri, Runnable doneCallback, Runnable errorCallback) { 1329 runInReader(() -> { 1330 ContentResolver contentResolver = getContentResolver(); 1331 InputStream inputStream; 1332 try { 1333 inputStream = contentResolver.openInputStream(uri); 1334 // Don't save the last opened document from the stream in the cloud, since we still cannot open it later in this program. 1335 mReaderView.loadDocumentFromStream(inputStream, uri.getPath(), doneCallback, errorCallback); 1336 } catch (Exception e) { 1337 errorCallback.run(); 1338 } 1339 }); 1340 } 1341 loadDocument(FileInfo item, boolean forceSync)1342 public void loadDocument(FileInfo item, boolean forceSync) { 1343 loadDocument(item, null, null, forceSync); 1344 } 1345 loadDocument(FileInfo item, Runnable doneCallback, Runnable errorCallback, boolean forceSync)1346 public void loadDocument(FileInfo item, Runnable doneCallback, Runnable errorCallback, boolean forceSync) { 1347 log.d("Activities.loadDocument(" + item.pathname + ")"); 1348 loadDocument(item.getPathName(), doneCallback, errorCallback, forceSync); 1349 } 1350 1351 /** 1352 * When current book is opened, switch to previous book. 1353 * 1354 * @param errorCallback 1355 */ loadPreviousDocument(Runnable errorCallback)1356 public void loadPreviousDocument(Runnable errorCallback) { 1357 BookInfo bi = Services.getHistory().getPreviousBook(); 1358 if (bi != null && bi.getFileInfo() != null) { 1359 log.i("loadPreviousDocument() is called, prevBookName = " + bi.getFileInfo().getPathName()); 1360 loadDocument(bi.getFileInfo(), null, errorCallback, true); 1361 return; 1362 } 1363 errorCallback.run(); 1364 } 1365 showOpenedBook()1366 public void showOpenedBook() { 1367 showReader(); 1368 } 1369 1370 public static final String OPEN_DIR_PARAM = "DIR_TO_OPEN"; 1371 showBrowser(final FileInfo dir)1372 public void showBrowser(final FileInfo dir) { 1373 runInBrowser(() -> mBrowser.showDirectory(dir, null)); 1374 } 1375 showBrowser(final String dir)1376 public void showBrowser(final String dir) { 1377 runInBrowser(() -> mBrowser.showDirectory(Services.getScanner().pathToFileInfo(dir), null)); 1378 } 1379 showRecentBooks()1380 public void showRecentBooks() { 1381 log.d("Activities.showRecentBooks() is called"); 1382 runInBrowser(() -> mBrowser.showRecentBooks()); 1383 } 1384 showOnlineCatalogs()1385 public void showOnlineCatalogs() { 1386 log.d("Activities.showOnlineCatalogs() is called"); 1387 runInBrowser(() -> mBrowser.showOPDSRootDirectory()); 1388 } 1389 showDirectory(FileInfo path)1390 public void showDirectory(FileInfo path) { 1391 log.d("Activities.showDirectory(" + path + ") is called"); 1392 showBrowser(path); 1393 } 1394 showCatalog(final FileInfo path)1395 public void showCatalog(final FileInfo path) { 1396 log.d("Activities.showCatalog(" + path + ") is called"); 1397 runInBrowser(() -> mBrowser.showDirectory(path, null)); 1398 } 1399 1400 setBrowserTitle(String title)1401 public void setBrowserTitle(String title) { 1402 if (mBrowserFrame != null) 1403 mBrowserFrame.setBrowserTitle(title); 1404 } 1405 1406 1407 // Dictionary support 1408 1409 findInDictionary(String s)1410 public void findInDictionary(String s) { 1411 if (s != null && s.length() != 0) { 1412 int start, end; 1413 1414 // Skip over non-letter characters at the beginning and end of the search string 1415 for (start = 0; start < s.length(); start++) 1416 if (Character.isLetterOrDigit(s.charAt(start))) 1417 break; 1418 for (end = s.length() - 1; end >= start; end--) 1419 if (Character.isLetterOrDigit(s.charAt(end))) 1420 break; 1421 1422 if (end > start) { 1423 final String pattern = s.substring(start, end + 1); 1424 1425 BackgroundThread.instance().postBackground(() -> BackgroundThread.instance() 1426 .postGUI(() -> findInDictionaryInternal(pattern), 100)); 1427 } 1428 } 1429 } 1430 findInDictionaryInternal(String s)1431 private void findInDictionaryInternal(String s) { 1432 log.d("lookup in dictionary: " + s); 1433 try { 1434 mDictionaries.findInDictionary(s); 1435 } catch (DictionaryException e) { 1436 showToast(e.getMessage()); 1437 } 1438 } 1439 showDictionary()1440 public void showDictionary() { 1441 findInDictionaryInternal(null); 1442 } 1443 1444 @Override onActivityResult(int requestCode, int resultCode, Intent intent)1445 protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 1446 try { 1447 mDictionaries.onActivityResult(requestCode, resultCode, intent); 1448 } catch (DictionaryException e) { 1449 showToast(e.getMessage()); 1450 } 1451 if (mDonationService != null) { 1452 mDonationService.onActivityResult(requestCode, resultCode, intent); 1453 } 1454 if (requestCode == REQUEST_CODE_GOOGLE_DRIVE_SIGN_IN) { 1455 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 1456 if (null != mGoogleDriveSync) { 1457 mGoogleDriveSync.onActivityResultHandler(requestCode, resultCode, intent); 1458 } 1459 } 1460 } else if (requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) { 1461 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 1462 if (resultCode == Activity.RESULT_OK) { 1463 switch (mOpenDocumentTreeCommand) { 1464 case ODT_CMD_DEL_FILE: 1465 if (mOpenDocumentTreeArg != null && !mOpenDocumentTreeArg.isDirectory) { 1466 Uri sdCardUri = intent.getData(); 1467 DocumentFile documentFile = null; 1468 if (null != sdCardUri) 1469 documentFile = Utils.getDocumentFile(mOpenDocumentTreeArg, this, sdCardUri); 1470 if (null != documentFile) { 1471 if (documentFile.delete()) { 1472 Services.getHistory().removeBookInfo(getDB(), mOpenDocumentTreeArg, true, true); 1473 final FileInfo dirToUpdate = mOpenDocumentTreeArg.parent; 1474 if (null != dirToUpdate) 1475 BackgroundThread.instance().postGUI(() -> directoryUpdated(dirToUpdate), 700); 1476 updateExtSDURI(mOpenDocumentTreeArg, sdCardUri); 1477 } else { 1478 showToast(R.string.could_not_delete_file, mOpenDocumentTreeArg); 1479 } 1480 } else { 1481 showToast(R.string.could_not_delete_on_sd); 1482 } 1483 } 1484 break; 1485 case ODT_CMD_DEL_FOLDER: 1486 if (mOpenDocumentTreeArg != null && mOpenDocumentTreeArg.isDirectory) { 1487 Uri sdCardUri = intent.getData(); 1488 DocumentFile documentFile = null; 1489 if (null != sdCardUri) 1490 documentFile = Utils.getDocumentFile(mOpenDocumentTreeArg, this, sdCardUri); 1491 if (null != documentFile) { 1492 if (documentFile.exists()) { 1493 updateExtSDURI(mOpenDocumentTreeArg, sdCardUri); 1494 deleteFolder(mOpenDocumentTreeArg); 1495 } 1496 } else { 1497 showToast(R.string.could_not_delete_on_sd); 1498 } 1499 } 1500 break; 1501 case ODT_CMD_SAVE_LOGCAT: 1502 if (mOpenDocumentTreeArg != null) { 1503 Uri uri = intent.getData(); 1504 if (null != uri) { 1505 DocumentFile docFolder = DocumentFile.fromTreeUri(this, uri); 1506 if (null != docFolder) { 1507 DocumentFile file = docFolder.createFile("text/x-log", mOpenDocumentTreeArg.filename); 1508 if (null != file) { 1509 try { 1510 OutputStream ostream = getContentResolver().openOutputStream(file.getUri()); 1511 if (null != ostream) { 1512 saveLogcat(file.getName(), ostream); 1513 ostream.close(); 1514 } else { 1515 log.e("logcat: failed to open stream!"); 1516 } 1517 } catch (Exception e) { 1518 log.e("logcat: " + e); 1519 } 1520 } else { 1521 log.e("logcat: can't create file!"); 1522 } 1523 } 1524 } else { 1525 log.d("logcat creation canceled by user"); 1526 } 1527 } 1528 break; 1529 } 1530 mOpenDocumentTreeArg = null; 1531 } 1532 } 1533 } //if (requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE) 1534 } 1535 setDict(String id)1536 public void setDict(String id) { 1537 mDictionaries.setDict(id); 1538 } 1539 setDict2(String id)1540 public void setDict2(String id) { 1541 mDictionaries.setDict2(id); 1542 } 1543 setToolbarAppearance(String id)1544 public void setToolbarAppearance(String id) { 1545 mOptionAppearance = id; 1546 } 1547 getToolbarAppearance()1548 public String getToolbarAppearance() { 1549 return mOptionAppearance; 1550 } 1551 showAboutDialog()1552 public void showAboutDialog() { 1553 AboutDialog dlg = new AboutDialog(this); 1554 dlg.show(); 1555 } 1556 1557 1558 private CRDonationService mDonationService = null; 1559 private DonationListener mDonationListener = null; 1560 private double mTotalDonations = 0; 1561 getDonationService()1562 public CRDonationService getDonationService() { 1563 return mDonationService; 1564 } 1565 isDonationSupported()1566 public boolean isDonationSupported() { 1567 return mDonationService.isBillingSupported(); 1568 } 1569 setDonationListener(DonationListener listener)1570 public void setDonationListener(DonationListener listener) { 1571 mDonationListener = listener; 1572 } 1573 1574 public static interface DonationListener { onDonationTotalChanged(double total)1575 void onDonationTotalChanged(double total); 1576 } 1577 getTotalDonations()1578 public double getTotalDonations() { 1579 return mTotalDonations; 1580 } 1581 makeDonation(final double amount)1582 public boolean makeDonation(final double amount) { 1583 final String itemName = "donation" + (amount >= 1 ? String.valueOf((int) amount) : String.valueOf(amount)); 1584 log.i("makeDonation is called, itemName=" + itemName); 1585 if (!mDonationService.isBillingSupported()) 1586 return false; 1587 BackgroundThread.instance().postBackground(() -> mDonationService.purchase(itemName, 1588 (success, productId, totalDonations) -> BackgroundThread.instance().postGUI(() -> { 1589 try { 1590 if (success) { 1591 log.i("Donation purchased: " + productId + ", total amount: " + mTotalDonations); 1592 mTotalDonations += amount; 1593 SharedPreferences pref = getSharedPreferences(DONATIONS_PREF_FILE, 0); 1594 pref.edit().putString(DONATIONS_PREF_TOTAL_AMOUNT, String.valueOf(mTotalDonations)).commit(); 1595 } else { 1596 showToast("Donation purchase failed"); 1597 } 1598 if (mDonationListener != null) 1599 mDonationListener.onDonationTotalChanged(mTotalDonations); 1600 } catch (Exception e) { 1601 // ignore 1602 } 1603 }))); 1604 return true; 1605 } 1606 1607 private static String DONATIONS_PREF_FILE = "cr3donations"; 1608 private static String DONATIONS_PREF_TOTAL_AMOUNT = "total"; 1609 1610 1611 // ======================================================================================== 1612 // TTS 1613 private TextToSpeech tts; 1614 private boolean ttsInitialized; 1615 private boolean ttsError; 1616 private String ttsEnginePackage; 1617 private Timer initTTSTimer; 1618 1619 private final static long INIT_TTS_TIMEOUT = 10000; // 10 sec. 1620 initTTS(final OnTTSCreatedListener listener)1621 public boolean initTTS(final OnTTSCreatedListener listener) { 1622 if (!phoneStateChangeHandlerInstalled) { 1623 boolean readPhoneStateIsAvailable; 1624 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 1625 readPhoneStateIsAvailable = checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED; 1626 } else 1627 readPhoneStateIsAvailable = true; 1628 if (!readPhoneStateIsAvailable) { 1629 // assumed Build.VERSION.SDK_INT >= Build.VERSION_CODES.M 1630 requestReadPhoneStatePermissions(); 1631 } else { 1632 // On Android API less than 23 phone read state permission is granted 1633 // after application install (permission requested while application installing). 1634 log.i("read phone state permission already GRANTED, registering phone activity handler..."); 1635 PhoneStateReceiver.setPhoneActivityHandler(() -> { 1636 if (mReaderView != null) { 1637 mReaderView.stopTTS(); 1638 mReaderView.save(); 1639 } 1640 }); 1641 phoneStateChangeHandlerInstalled = true; 1642 } 1643 } 1644 if (ttsInitialized && tts != null) { 1645 BackgroundThread.instance().executeGUI(() -> listener.onCreated(tts)); 1646 return true; 1647 } 1648 showToast("Initializing TTS"); 1649 TextToSpeech.OnInitListener onInitListener = status -> { 1650 //tts.shutdown(); 1651 initTTSTimer.cancel(); 1652 initTTSTimer = null; 1653 L.i("TTS init status: " + status); 1654 if (status == TextToSpeech.SUCCESS) { 1655 ttsInitialized = true; 1656 BackgroundThread.instance().executeGUI(() -> listener.onCreated(tts)); 1657 } else { 1658 ttsError = true; 1659 BackgroundThread.instance().executeGUI(() -> showToast("Cannot initialize TTS")); 1660 } 1661 }; 1662 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH && null != ttsEnginePackage && ttsEnginePackage.length() > 0) 1663 tts = new TextToSpeech(this, onInitListener, ttsEnginePackage); 1664 else 1665 tts = new TextToSpeech(this, onInitListener); 1666 initTTSTimer = new Timer(); 1667 initTTSTimer.schedule(new TimerTask() { 1668 @Override 1669 public void run() { 1670 // TTS engine init hangs, remove it from settings 1671 log.e("TTS engine \"" + ttsEnginePackage + "\" init failure, disabling!"); 1672 BackgroundThread.instance().executeGUI(() -> showToast(R.string.tts_init_failure, ttsEnginePackage)); 1673 setSetting(PROP_APP_TTS_ENGINE, "", false); 1674 ttsEnginePackage = ""; 1675 try { 1676 mReaderView.getTTSToolbar().stopAndClose(); 1677 } catch (Exception ignored) {} 1678 initTTSTimer.cancel(); 1679 initTTSTimer = null; 1680 } 1681 }, INIT_TTS_TIMEOUT); 1682 return true; 1683 } 1684 1685 // ============================================================ 1686 private AudioManager am; 1687 private int maxVolume; 1688 getAudioManager()1689 public AudioManager getAudioManager() { 1690 if (am == null) { 1691 am = (AudioManager) getSystemService(AUDIO_SERVICE); 1692 maxVolume = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 1693 } 1694 return am; 1695 } 1696 getVolume()1697 public int getVolume() { 1698 AudioManager am = getAudioManager(); 1699 if (am != null) { 1700 return am.getStreamVolume(AudioManager.STREAM_MUSIC) * 100 / maxVolume; 1701 } 1702 return 0; 1703 } 1704 setVolume(int volume)1705 public void setVolume(int volume) { 1706 AudioManager am = getAudioManager(); 1707 if (am != null) { 1708 am.setStreamVolume(AudioManager.STREAM_MUSIC, volume * maxVolume / 100, 0); 1709 } 1710 } 1711 showOptionsDialog(final OptionsDialog.Mode mode)1712 public void showOptionsDialog(final OptionsDialog.Mode mode) { 1713 BackgroundThread.instance().postBackground(() -> { 1714 final String[] mFontFaces = Engine.getFontFaceList(); 1715 BackgroundThread.instance().executeGUI(() -> { 1716 OptionsDialog dlg = new OptionsDialog(CoolReader.this, mode, mReaderView, mFontFaces, null); 1717 dlg.show(); 1718 }); 1719 }); 1720 } 1721 updateCurrentPositionStatus(FileInfo book, Bookmark position, PositionProperties props)1722 public void updateCurrentPositionStatus(FileInfo book, Bookmark position, PositionProperties props) { 1723 mReaderFrame.getStatusBar().updateCurrentPositionStatus(book, position, props); 1724 } 1725 1726 1727 @Override setDimmingAlpha(int dimmingAlpha)1728 protected void setDimmingAlpha(int dimmingAlpha) { 1729 if (mReaderView != null) 1730 mReaderView.setDimmingAlpha(dimmingAlpha); 1731 } 1732 showReaderMenu()1733 public void showReaderMenu() { 1734 // 1735 if (mReaderFrame != null) { 1736 mReaderFrame.showMenu(); 1737 } 1738 } 1739 1740 sendBookFragment(BookInfo bookInfo, String text)1741 public void sendBookFragment(BookInfo bookInfo, String text) { 1742 final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND); 1743 emailIntent.setType("text/plain"); 1744 emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, bookInfo.getFileInfo().getAuthors() + " " + bookInfo.getFileInfo().getTitle()); 1745 emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, text); 1746 startActivity(Intent.createChooser(emailIntent, null)); 1747 } 1748 showBookmarksDialog()1749 public void showBookmarksDialog() { 1750 BackgroundThread.instance().executeGUI(() -> { 1751 BookmarksDlg dlg = new BookmarksDlg(CoolReader.this, mReaderView); 1752 dlg.show(); 1753 }); 1754 } 1755 openURL(String url)1756 public void openURL(String url) { 1757 try { 1758 Intent i = new Intent(Intent.ACTION_VIEW); 1759 i.setData(Uri.parse(url)); 1760 startActivity(i); 1761 } catch (Exception e) { 1762 log.e("Exception " + e + " while trying to open URL " + url); 1763 showToast("Cannot open URL " + url); 1764 } 1765 } 1766 1767 isBookOpened()1768 public boolean isBookOpened() { 1769 if (mReaderView == null) 1770 return false; 1771 return mReaderView.isBookLoaded(); 1772 } 1773 closeBookIfOpened(FileInfo book)1774 public void closeBookIfOpened(FileInfo book) { 1775 if (mReaderView == null) 1776 return; 1777 mReaderView.closeIfOpened(book); 1778 } 1779 askDeleteBook(final FileInfo item)1780 public void askDeleteBook(final FileInfo item) { 1781 askConfirmation(R.string.win_title_confirm_book_delete, () -> { 1782 closeBookIfOpened(item); 1783 FileInfo file = Services.getScanner().findFileInTree(item); 1784 if (file == null) 1785 file = item; 1786 final FileInfo finalFile = file; 1787 if (file.deleteFile()) { 1788 waitForCRDBService(() -> { 1789 Services.getHistory().removeBookInfo(getDB(), finalFile, true, true); 1790 BackgroundThread.instance().postGUI(() -> directoryUpdated(finalFile.parent, null), 700); 1791 }); 1792 } else { 1793 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 1794 DocumentFile documentFile = null; 1795 Uri sdCardUri = getExtSDURIByFileInfo(file); 1796 if (sdCardUri != null) 1797 documentFile = Utils.getDocumentFile(file, this, sdCardUri); 1798 if (null != documentFile) { 1799 if (documentFile.delete()) { 1800 waitForCRDBService(() -> { 1801 Services.getHistory().removeBookInfo(getDB(), finalFile, true, true); 1802 BackgroundThread.instance().postGUI(() -> directoryUpdated(finalFile.parent), 700); 1803 }); 1804 } else { 1805 showToast(R.string.could_not_delete_file, file); 1806 } 1807 } else { 1808 showToast(R.string.choose_root_sd); 1809 mOpenDocumentTreeArg = file; 1810 mOpenDocumentTreeCommand = ODT_CMD_DEL_FILE; 1811 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); 1812 startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT_TREE); 1813 } 1814 } else { 1815 showToast(R.string.could_not_delete_file, file); 1816 } 1817 } 1818 }); 1819 } 1820 askDeleteRecent(final FileInfo item)1821 public void askDeleteRecent(final FileInfo item) { 1822 askConfirmation(R.string.win_title_confirm_history_record_delete, () -> waitForCRDBService(() -> { 1823 Services.getHistory().removeBookInfo(getDB(), item, true, false); 1824 directoryUpdated(Services.getScanner().createRecentRoot()); 1825 })); 1826 } 1827 askDeleteCatalog(final FileInfo item)1828 public void askDeleteCatalog(final FileInfo item) { 1829 askConfirmation(R.string.win_title_confirm_catalog_delete, () -> { 1830 if (item != null && item.isOPDSDir()) { 1831 waitForCRDBService(() -> { 1832 getDB().removeOPDSCatalog(item.id); 1833 directoryUpdated(Services.getScanner().createOPDSRoot()); 1834 }); 1835 } 1836 }); 1837 } 1838 1839 int mFolderDeleteRetryCount = 0; askDeleteFolder(final FileInfo item)1840 public void askDeleteFolder(final FileInfo item) { 1841 askConfirmation(R.string.win_title_confirm_folder_delete, () -> { 1842 mFolderDeleteRetryCount = 0; 1843 deleteFolder(item); 1844 }); 1845 } 1846 deleteFolder(final FileInfo item)1847 private void deleteFolder(final FileInfo item) { 1848 if (mFolderDeleteRetryCount > 3) 1849 return; 1850 if (item != null && item.isDirectory && !item.isOPDSDir() && !item.isOnlineCatalogPluginDir()) { 1851 FileInfoOperationListener bookDeleteCallback = (fileInfo, errorStatus) -> { 1852 if (0 == errorStatus && null != fileInfo.format) { 1853 BackgroundThread.instance().executeGUI(() -> { 1854 waitForCRDBService(() -> Services.getHistory().removeBookInfo(getDB(), fileInfo, true, true)); 1855 }); 1856 } 1857 }; 1858 BackgroundThread.instance().postBackground(() -> Utils.deleteFolder(item, bookDeleteCallback, (fileInfo, errorStatus) -> { 1859 if (0 == errorStatus) { 1860 BackgroundThread.instance().executeGUI(() -> directoryUpdated(fileInfo.parent)); 1861 } else { 1862 // Can't be deleted using standard Java I/O, 1863 // Try DocumentFile interface... 1864 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 1865 Uri sdCardUri = getExtSDURIByFileInfo(item); 1866 if (null != sdCardUri) { 1867 Utils.deleteFolderDocTree(item, this, sdCardUri, bookDeleteCallback, (fileInfo2, errorStatus2) -> { 1868 BackgroundThread.instance().executeGUI(() -> { 1869 if (0 == errorStatus2) { 1870 directoryUpdated(fileInfo2.parent); 1871 } else { 1872 showToast(R.string.choose_root_sd); 1873 mFolderDeleteRetryCount++; 1874 mOpenDocumentTreeCommand = ODT_CMD_DEL_FOLDER; 1875 mOpenDocumentTreeArg = item; 1876 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); 1877 startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT_TREE); 1878 } 1879 }); 1880 }); 1881 } else { 1882 BackgroundThread.instance().executeGUI(() -> { 1883 showToast(R.string.choose_root_sd); 1884 mFolderDeleteRetryCount++; 1885 mOpenDocumentTreeCommand = ODT_CMD_DEL_FOLDER; 1886 mOpenDocumentTreeArg = item; 1887 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); 1888 startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT_TREE); 1889 }); 1890 } 1891 } 1892 } 1893 })); 1894 } 1895 } 1896 createLogcatFile()1897 public void createLogcatFile() { 1898 final SimpleDateFormat format = new SimpleDateFormat("'cr3-'yyyy-MM-dd_HH_mm_ss'.log'", Locale.US); 1899 FileInfo dir = Services.getScanner().getSharedDownloadDirectory(); 1900 if (null == dir) { 1901 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 1902 log.d("logcat: no access to download directory, opening document tree..."); 1903 askConfirmation(R.string.confirmation_select_folder_for_log, () -> { 1904 mOpenDocumentTreeCommand = ODT_CMD_SAVE_LOGCAT; 1905 mOpenDocumentTreeArg = new FileInfo(format.format(new Date())); 1906 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); 1907 startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT_TREE); 1908 }); 1909 } else { 1910 log.e("Can't create logcat file: no access to download directory!"); 1911 } 1912 } else { 1913 try { 1914 File outputFile = new File(dir.pathname, format.format(new Date())); 1915 FileOutputStream ostream = new FileOutputStream(outputFile); 1916 saveLogcat(outputFile.getCanonicalPath(), ostream); 1917 } catch (Exception e) { 1918 log.e("createLogcatFile: " + e); 1919 } 1920 } 1921 } 1922 saveLogcat(String fileName, OutputStream ostream)1923 private void saveLogcat(String fileName, OutputStream ostream) { 1924 Date since = getLastLogcatDate(); 1925 Date now = new Date(); 1926 if (LogcatSaver.saveLogcat(since, ostream)) { 1927 setLastLogcatDate(now); 1928 log.i("logcat saved to file " + fileName); 1929 //showToast("Logcat saved to " + fileName); 1930 showMessage(getString(R.string.win_title_log), getString(R.string.notice_log_saved_to_, fileName)); 1931 } else { 1932 log.e("Failed to save logcat to " + fileName); 1933 showToast("Failed to save logcat to " + fileName); 1934 } 1935 } 1936 saveSetting(String name, String value)1937 public void saveSetting(String name, String value) { 1938 if (mReaderView != null) 1939 mReaderView.saveSetting(name, value); 1940 } 1941 editBookInfo(final FileInfo currDirectory, final FileInfo item)1942 public void editBookInfo(final FileInfo currDirectory, final FileInfo item) { 1943 waitForCRDBService(() -> Services.getHistory().getOrCreateBookInfo(getDB(), item, bookInfo -> { 1944 if (bookInfo == null) 1945 bookInfo = new BookInfo(item); 1946 BookInfoEditDialog dlg = new BookInfoEditDialog(CoolReader.this, currDirectory, bookInfo, 1947 currDirectory.isRecentDir()); 1948 dlg.show(); 1949 })); 1950 } 1951 editOPDSCatalog(FileInfo opds)1952 public void editOPDSCatalog(FileInfo opds) { 1953 if (opds == null) { 1954 opds = new FileInfo(); 1955 opds.isDirectory = true; 1956 opds.pathname = FileInfo.OPDS_DIR_PREFIX + "http://"; 1957 opds.filename = "New Catalog"; 1958 opds.isListed = true; 1959 opds.isScanned = true; 1960 opds.parent = Services.getScanner().getOPDSRoot(); 1961 } 1962 OPDSCatalogEditDialog dlg = new OPDSCatalogEditDialog(CoolReader.this, opds, 1963 () -> refreshOPDSRootDirectory(true)); 1964 dlg.show(); 1965 } 1966 refreshOPDSRootDirectory(boolean showInBrowser)1967 public void refreshOPDSRootDirectory(boolean showInBrowser) { 1968 if (mBrowser != null) 1969 mBrowser.refreshOPDSRootDirectory(showInBrowser); 1970 if (mHomeFrame != null) 1971 mHomeFrame.refreshOnlineCatalogs(); 1972 } 1973 1974 1975 private SharedPreferences mPreferences; 1976 private final static String BOOK_LOCATION_PREFIX = "@book:"; 1977 private final static String DIRECTORY_LOCATION_PREFIX = "@dir:"; 1978 getPrefs()1979 private SharedPreferences getPrefs() { 1980 if (mPreferences == null) 1981 mPreferences = getSharedPreferences(PREF_FILE, 0); 1982 return mPreferences; 1983 } 1984 setLastBook(String path)1985 public void setLastBook(String path) { 1986 setLastLocation(BOOK_LOCATION_PREFIX + path); 1987 } 1988 setLastDirectory(String path)1989 public void setLastDirectory(String path) { 1990 setLastLocation(DIRECTORY_LOCATION_PREFIX + path); 1991 } 1992 setLastLocationRoot()1993 public void setLastLocationRoot() { 1994 setLastLocation(FileInfo.ROOT_DIR_TAG); 1995 } 1996 1997 /** 1998 * Store last location - to resume after program restart. 1999 * 2000 * @param location is file name, directory, or special folder tag 2001 */ setLastLocation(String location)2002 public void setLastLocation(String location) { 2003 try { 2004 String oldLocation = getPrefs().getString(PREF_LAST_LOCATION, null); 2005 if (oldLocation != null && oldLocation.equals(location)) 2006 return; // not changed 2007 SharedPreferences.Editor editor = getPrefs().edit(); 2008 editor.putString(PREF_LAST_LOCATION, location); 2009 editor.commit(); 2010 } catch (Exception e) { 2011 // ignore 2012 } 2013 } 2014 2015 private static final int NOTIFICATION_READER_MENU_MASK = 0x01; 2016 private static final int NOTIFICATION_LOGCAT_MASK = 0x02; 2017 private static final int NOTIFICATION_MASK_ALL = NOTIFICATION_READER_MENU_MASK | 2018 NOTIFICATION_LOGCAT_MASK; 2019 setLastNotificationMask(int notificationId)2020 public void setLastNotificationMask(int notificationId) { 2021 try { 2022 SharedPreferences.Editor editor = getPrefs().edit(); 2023 editor.putInt(PREF_LAST_NOTIFICATION_MASK, notificationId); 2024 editor.commit(); 2025 } catch (Exception e) { 2026 // ignore 2027 } 2028 } 2029 getLastNotificationMask()2030 public int getLastNotificationMask() { 2031 int res = getPrefs().getInt(PREF_LAST_NOTIFICATION_MASK, 0); 2032 log.i("getLastNotification() = " + res); 2033 return res; 2034 } 2035 2036 showNotifications()2037 public void showNotifications() { 2038 int lastNoticeMask = getLastNotificationMask(); 2039 if ((lastNoticeMask & NOTIFICATION_MASK_ALL) == NOTIFICATION_MASK_ALL) 2040 return; 2041 if (DeviceInfo.getSDKLevel() >= DeviceInfo.HONEYCOMB) { 2042 if ((lastNoticeMask & NOTIFICATION_READER_MENU_MASK) == 0) { 2043 notification1(); 2044 return; 2045 } 2046 } 2047 if ((lastNoticeMask & NOTIFICATION_LOGCAT_MASK) == 0) { 2048 notification2(); 2049 } 2050 } 2051 notification1()2052 public void notification1() { 2053 if (hasHardwareMenuKey()) 2054 return; // don't show notice if hard key present 2055 showNotice(R.string.note1_reader_menu, 2056 () -> { 2057 setSetting(PROP_TOOLBAR_LOCATION, String.valueOf(VIEWER_TOOLBAR_SHORT_SIDE), false); 2058 setLastNotificationMask(getLastNotificationMask() | NOTIFICATION_READER_MENU_MASK); 2059 showNotifications(); 2060 }, 2061 () -> { 2062 setSetting(PROP_TOOLBAR_LOCATION, String.valueOf(VIEWER_TOOLBAR_NONE), false); 2063 setLastNotificationMask(getLastNotificationMask() | NOTIFICATION_READER_MENU_MASK); 2064 showNotifications(); 2065 } 2066 ); 2067 } 2068 notification2()2069 public void notification2() { 2070 showNotice(R.string.note2_logcat, 2071 () -> { 2072 setLastNotificationMask(getLastNotificationMask() | NOTIFICATION_LOGCAT_MASK); 2073 showNotifications(); 2074 } 2075 ); 2076 } 2077 2078 /** 2079 * Get last stored location. 2080 * 2081 * @return 2082 */ getLastLocation()2083 private String getLastLocation() { 2084 String res = getPrefs().getString(PREF_LAST_LOCATION, null); 2085 if (res == null) { 2086 // import last book value from previous releases 2087 res = getPrefs().getString(PREF_LAST_BOOK, null); 2088 if (res != null) { 2089 res = BOOK_LOCATION_PREFIX + res; 2090 try { 2091 getPrefs().edit().remove(PREF_LAST_BOOK).commit(); 2092 } catch (Exception e) { 2093 // ignore 2094 } 2095 } 2096 } 2097 log.i("getLastLocation() = " + res); 2098 return res; 2099 } 2100 2101 /** 2102 * Open location - book, root view, folder... 2103 */ showLastLocation()2104 public void showLastLocation() { 2105 String location = getLastLocation(); 2106 if (location == null) 2107 location = FileInfo.ROOT_DIR_TAG; 2108 if (location.startsWith(BOOK_LOCATION_PREFIX)) { 2109 location = location.substring(BOOK_LOCATION_PREFIX.length()); 2110 loadDocument(location, null, () -> BackgroundThread.instance().postGUI(() -> { 2111 // if document not loaded show error & then root window 2112 ErrorDialog errDialog = new ErrorDialog(CoolReader.this, "Error", "Can't open file!"); 2113 errDialog.setOnDismissListener(dialog -> showRootWindow()); 2114 errDialog.show(); 2115 }, 1000), false); 2116 return; 2117 } 2118 if (location.startsWith(DIRECTORY_LOCATION_PREFIX)) { 2119 location = location.substring(DIRECTORY_LOCATION_PREFIX.length()); 2120 showBrowser(location); 2121 return; 2122 } 2123 if (location.equals(FileInfo.RECENT_DIR_TAG)) { 2124 showBrowser(location); 2125 return; 2126 } 2127 // TODO: support other locations as well 2128 showRootWindow(); 2129 } 2130 setExtDataDirCreateTime(Date d)2131 public void setExtDataDirCreateTime(Date d) { 2132 try { 2133 SharedPreferences.Editor editor = getPrefs().edit(); 2134 editor.putLong(PREF_EXT_DATADIR_CREATETIME, (null != d) ? d.getTime() : 0); 2135 editor.commit(); 2136 } catch (Exception e) { 2137 // ignore 2138 } 2139 } 2140 getExtDataDirCreateTime()2141 public long getExtDataDirCreateTime() { 2142 long res = getPrefs().getLong(PREF_EXT_DATADIR_CREATETIME, 0); 2143 log.i("getExtDataDirCreateTime() = " + res); 2144 return res; 2145 } 2146 updateExtSDURI(FileInfo fi, Uri extSDUri)2147 private boolean updateExtSDURI(FileInfo fi, Uri extSDUri) { 2148 String prefKey = null; 2149 String filePath = null; 2150 if (fi.isArchive && fi.arcname != null) { 2151 filePath = fi.arcname; 2152 } else 2153 filePath = fi.pathname; 2154 if (null != filePath) { 2155 File f = new File(filePath); 2156 filePath = f.getAbsolutePath(); 2157 String[] parts = filePath.split("\\/"); 2158 if (parts.length >= 3) { 2159 // For example, 2160 // parts[0] = "" 2161 // parts[1] = "storage" 2162 // parts[2] = "1501-3F19" 2163 // then prefKey = "/storage/1501-3F19" 2164 prefKey = "uri_for_/" + parts[1] + "/" + parts[2]; 2165 } 2166 } 2167 if (null != prefKey) { 2168 SharedPreferences prefs = getPrefs(); 2169 return prefs.edit().putString(prefKey, extSDUri.toString()).commit(); 2170 } 2171 return false; 2172 } 2173 getExtSDURIByFileInfo(FileInfo fi)2174 private Uri getExtSDURIByFileInfo(FileInfo fi) { 2175 Uri uri = null; 2176 String prefKey = null; 2177 String filePath = null; 2178 if (fi.isArchive && fi.arcname != null) { 2179 filePath = fi.arcname; 2180 } else 2181 filePath = fi.pathname; 2182 if (null != filePath) { 2183 File f = new File(filePath); 2184 filePath = f.getAbsolutePath(); 2185 String[] parts = filePath.split("\\/"); 2186 if (parts.length >= 3) { 2187 prefKey = "uri_for_/" + parts[1] + "/" + parts[2]; 2188 } 2189 } 2190 if (null != prefKey) { 2191 SharedPreferences prefs = getPrefs(); 2192 String strUri = prefs.getString(prefKey, null); 2193 if (null != strUri) 2194 uri = Uri.parse(strUri); 2195 } 2196 return uri; 2197 } 2198 getLastLogcatDate()2199 private Date getLastLogcatDate() { 2200 long dateMillis = getPrefs().getLong(PREF_LAST_LOGCAT, 0); 2201 return new Date(dateMillis); 2202 } 2203 setLastLogcatDate(Date date)2204 private void setLastLogcatDate(Date date) { 2205 SharedPreferences.Editor editor = getPrefs().edit(); 2206 editor.putLong(PREF_LAST_LOGCAT, date.getTime()); 2207 editor.commit(); 2208 } 2209 showCurrentBook()2210 public void showCurrentBook() { 2211 BookInfo bi = Services.getHistory().getLastBook(); 2212 if (bi != null) 2213 loadDocument(bi.getFileInfo(), false); 2214 } 2215 2216 } 2217