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