From 0d519b3d1617dc07784e9e62e6bfa0c9beb8030a Mon Sep 17 00:00:00 2001 From: len Date: Sat, 19 Mar 2016 15:14:51 +0100 Subject: [PATCH] Reader presenter in Kotlin + remove Icepick --- app/build.gradle | 3 - .../main/java/eu/kanade/tachiyomi/App.java | 2 +- .../ui/base/activity/BaseActivity.kt | 12 - .../ui/base/fragment/BaseFragment.kt | 12 - .../ui/base/presenter/BasePresenter.kt | 12 - .../ui/catalogue/CatalogueFragment.kt | 2 +- .../tachiyomi/ui/reader/ReaderActivity.kt | 2 +- .../tachiyomi/ui/reader/ReaderPresenter.java | 424 ------------------ .../tachiyomi/ui/reader/ReaderPresenter.kt | 421 +++++++++++++++++ 9 files changed, 424 insertions(+), 466 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.java create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt diff --git a/app/build.gradle b/app/build.gradle index 8a7fa86733..cb52096b36 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,7 +105,6 @@ dependencies { final OKHTTP_VERSION = '3.2.0' final RETROFIT_VERSION = '2.0.0' final STORIO_VERSION = '1.8.0' - final ICEPICK_VERSION = '3.2.0' final MOCKITO_VERSION = '1.10.19' compile fileTree(dir: 'libs', include: ['*.jar']) @@ -138,8 +137,6 @@ dependencies { compile 'com.github.bumptech.glide:glide:3.7.0' compile 'com.jakewharton.timber:timber:4.1.1' compile 'ch.acra:acra:4.8.3' - compile "frankiesardo:icepick:$ICEPICK_VERSION" - provided "frankiesardo:icepick-processor:$ICEPICK_VERSION" compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' compile 'eu.davidea:flexible-adapter:4.2.0' compile 'com.nononsenseapps:filepicker:2.5.2' diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.java b/app/src/main/java/eu/kanade/tachiyomi/App.java index fd464c2c0b..e6cb1424eb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.java +++ b/app/src/main/java/eu/kanade/tachiyomi/App.java @@ -58,7 +58,7 @@ public class App extends Application { protected void setupEventBus() { EventBus.builder() - .addIndex(new EventBusIndex()) +// .addIndex(new EventBusIndex()) .logNoSubscriberMessages(false) .installDefaultEventBus(); } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt index 0de8b7f063..a92a216cdd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.base.activity import android.graphics.Color -import android.os.Bundle import android.support.design.widget.Snackbar import android.support.v7.app.AppCompatActivity import android.support.v7.widget.Toolbar @@ -10,21 +9,10 @@ import android.view.View import android.widget.TextView import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.R -import icepick.Icepick import org.greenrobot.eventbus.EventBus open class BaseActivity : AppCompatActivity() { - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - Icepick.restoreInstanceState(this, savedState) - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - Icepick.saveInstanceState(this, outState) - } - protected fun setupToolbar(toolbar: Toolbar) { setSupportActionBar(toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt index 5f229f9b20..260869d9d3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/fragment/BaseFragment.kt @@ -1,23 +1,11 @@ package eu.kanade.tachiyomi.ui.base.fragment -import android.os.Bundle import android.support.v4.app.Fragment import eu.kanade.tachiyomi.ui.base.activity.BaseActivity -import icepick.Icepick import org.greenrobot.eventbus.EventBus open class BaseFragment : Fragment() { - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - Icepick.restoreInstanceState(this, savedState) - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - Icepick.saveInstanceState(this, outState) - } - fun setToolbarTitle(title: String) { baseActivity.setToolbarTitle(title) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt index a605211e23..c361446c97 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt @@ -1,8 +1,6 @@ package eu.kanade.tachiyomi.ui.base.presenter import android.content.Context -import android.os.Bundle -import icepick.Icepick import nucleus.view.ViewWithPresenter import org.greenrobot.eventbus.EventBus @@ -10,16 +8,6 @@ open class BasePresenter> : RxPresenter() { lateinit var context: Context - override fun onCreate(savedState: Bundle?) { - super.onCreate(savedState) - Icepick.restoreInstanceState(this, savedState) - } - - override fun onSave(state: Bundle) { - super.onSave(state) - Icepick.saveInstanceState(this, state) - } - fun registerForEvents() { EventBus.getDefault().register(this) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt index d454eb2cd5..f72b4675e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt @@ -172,7 +172,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) val onItemSelected = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>, view: View? , position: Int, id: Long) { + override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { val source = spinnerAdapter.getItem(position) if (selectedIndex != position || adapter.isEmpty) { // Set previous selection if it's not a valid source and notify the user diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index a567891a66..476fbbdf17 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -144,7 +144,7 @@ class ReaderActivity : BaseRxActivity() { override fun onBackPressed() { presenter.onChapterLeft() - val chapterToUpdate = presenter.mangaSyncChapterToUpdate + val chapterToUpdate = presenter.getMangaSyncChapterToUpdate() if (chapterToUpdate > 0) { if (presenter.prefs.askUpdateMangaSync()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.java deleted file mode 100644 index 896f87da18..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.java +++ /dev/null @@ -1,424 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.util.Pair; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.io.File; -import java.util.List; - -import javax.inject.Inject; - -import eu.kanade.tachiyomi.data.cache.ChapterCache; -import eu.kanade.tachiyomi.data.database.DatabaseHelper; -import eu.kanade.tachiyomi.data.database.models.Chapter; -import eu.kanade.tachiyomi.data.database.models.Manga; -import eu.kanade.tachiyomi.data.database.models.MangaSync; -import eu.kanade.tachiyomi.data.download.DownloadManager; -import eu.kanade.tachiyomi.data.download.model.Download; -import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager; -import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService; -import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService; -import eu.kanade.tachiyomi.data.preference.PreferencesHelper; -import eu.kanade.tachiyomi.data.source.SourceManager; -import eu.kanade.tachiyomi.data.source.base.Source; -import eu.kanade.tachiyomi.data.source.model.Page; -import eu.kanade.tachiyomi.event.ReaderEvent; -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; -import icepick.State; -import rx.Observable; -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; -import rx.schedulers.Schedulers; -import rx.subjects.PublishSubject; -import timber.log.Timber; - -public class ReaderPresenter extends BasePresenter { - - @Inject PreferencesHelper prefs; - @Inject DatabaseHelper db; - @Inject DownloadManager downloadManager; - @Inject MangaSyncManager syncManager; - @Inject SourceManager sourceManager; - @Inject ChapterCache chapterCache; - - @State Manga manga; - @State Chapter activeChapter; - @State int requestedPage; - private Page currentPage; - private Source source; - private Chapter nextChapter; - private Chapter previousChapter; - private List mangaSyncList; - - private PublishSubject retryPageSubject; - private PublishSubject pageInitializerSubject; - - private boolean seamlessMode; - private Subscription appenderSubscription; - - private static final int GET_PAGE_LIST = 1; - private static final int GET_ADJACENT_CHAPTERS = 2; - private static final int GET_MANGA_SYNC = 3; - private static final int PRELOAD_NEXT_CHAPTER = 4; - - @Override - protected void onCreate(Bundle savedState) { - super.onCreate(savedState); - - if (savedState != null) { - source = sourceManager.get(manga.source); - initializeSubjects(); - } - - seamlessMode = prefs.seamlessMode(); - - startableLatestCache(GET_ADJACENT_CHAPTERS, this::getAdjacentChaptersObservable, - (view, pair) -> view.onAdjacentChapters(pair.first, pair.second)); - - startable(PRELOAD_NEXT_CHAPTER, this::getPreloadNextChapterObservable, - next -> {}, - error -> Timber.e("Error preloading chapter")); - - - restartable(GET_MANGA_SYNC, () -> getMangaSyncObservable().subscribe()); - - restartableLatestCache(GET_PAGE_LIST, - () -> getPageListObservable(activeChapter), - (view, chapter) -> view.onChapterReady(manga, activeChapter, currentPage), - (view, error) -> view.onChapterError()); - - if (savedState == null) { - registerForEvents(); - } - } - - @Override - protected void onDestroy() { - unregisterForEvents(); - super.onDestroy(); - } - - @Override - protected void onSave(@NonNull Bundle state) { - onChapterLeft(); - super.onSave(state); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEvent(ReaderEvent event) { - EventBus.getDefault().removeStickyEvent(event); - manga = event.getManga(); - source = sourceManager.get(manga.source); - initializeSubjects(); - loadChapter(event.getChapter()); - if (prefs.autoUpdateMangaSync()) { - start(GET_MANGA_SYNC); - } - } - - private void initializeSubjects() { - // Listen for pages initialization events - pageInitializerSubject = PublishSubject.create(); - add(pageInitializerSubject - .observeOn(Schedulers.io()) - .concatMap(chapter -> { - Observable observable; - if (chapter.isDownloaded()) { - File chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter); - observable = Observable.from(chapter.getPages()) - .flatMap(page -> downloadManager.getDownloadedImage(page, chapterDir)); - } else { - observable = source.getAllImageUrlsFromPageList(chapter.getPages()) - .flatMap(source::getCachedImage, 2) - .doOnCompleted(() -> source.savePageList(chapter.url, chapter.getPages())); - } - return observable.doOnCompleted(() -> { - if (!seamlessMode && activeChapter == chapter) { - preloadNextChapter(); - } - }); - }) - .subscribe()); - - // Listen por retry events - retryPageSubject = PublishSubject.create(); - add(retryPageSubject - .observeOn(Schedulers.io()) - .flatMap(page -> page.getImageUrl() == null ? - source.getImageUrlFromPage(page) : - Observable.just(page)) - .flatMap(source::getCachedImage) - .subscribe()); - } - - // Returns the page list of a chapter - private Observable getPageListObservable(Chapter chapter) { - return (chapter.isDownloaded() ? - // Fetch the page list from disk - Observable.just(downloadManager.getSavedPageList(source, manga, chapter)) : - // Fetch the page list from cache or fallback to network - source.getCachedPageListOrPullFromNetwork(chapter.url) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - ).map(pages -> { - for (Page page : pages) { - page.setChapter(chapter); - } - chapter.setPages(pages); - if (requestedPage >= -1 || currentPage == null) { - if (requestedPage == -1) { - currentPage = pages.get(pages.size() - 1); - } else { - currentPage = pages.get(requestedPage); - } - } - requestedPage = -2; - pageInitializerSubject.onNext(chapter); - return chapter; - }); - } - - private Observable> getAdjacentChaptersObservable() { - return Observable.zip( - db.getPreviousChapter(activeChapter).asRxObservable().take(1), - db.getNextChapter(activeChapter).asRxObservable().take(1), - Pair::create) - .doOnNext(pair -> { - previousChapter = pair.first; - nextChapter = pair.second; - }) - .observeOn(AndroidSchedulers.mainThread()); - } - - // Preload the first pages of the next chapter. Only for non seamless mode - private Observable getPreloadNextChapterObservable() { - return source.getCachedPageListOrPullFromNetwork(nextChapter.url) - .flatMap(pages -> { - nextChapter.setPages(pages); - int pagesToPreload = Math.min(pages.size(), 5); - return Observable.from(pages).take(pagesToPreload); - }) - // Preload up to 5 images - .concatMap(page -> page.getImageUrl() == null ? - source.getImageUrlFromPage(page) : - Observable.just(page)) - // Download the first image - .concatMap(page -> page.getPageNumber() == 0 ? - source.getCachedImage(page) : - Observable.just(page)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnCompleted(this::stopPreloadingNextChapter); - } - - private Observable> getMangaSyncObservable() { - return db.getMangasSync(manga).asRxObservable() - .take(1) - .doOnNext(mangaSync -> this.mangaSyncList = mangaSync); - } - - private void loadChapter(Chapter chapter) { - loadChapter(chapter, 0); - } - - // Loads the given chapter - private void loadChapter(Chapter chapter, int requestedPage) { - if (seamlessMode) { - if (appenderSubscription != null) - remove(appenderSubscription); - } else { - stopPreloadingNextChapter(); - } - - this.activeChapter = chapter; - chapter.status = isChapterDownloaded(chapter) ? Download.DOWNLOADED : Download.NOT_DOWNLOADED; - - // If the chapter is partially read, set the starting page to the last the user read - if (!chapter.read && chapter.last_page_read != 0) - this.requestedPage = chapter.last_page_read; - else - this.requestedPage = requestedPage; - - // Reset next and previous chapter. They have to be fetched again - nextChapter = null; - previousChapter = null; - - start(GET_PAGE_LIST); - start(GET_ADJACENT_CHAPTERS); - } - - public void setActiveChapter(Chapter chapter) { - onChapterLeft(); - this.activeChapter = chapter; - nextChapter = null; - previousChapter = null; - start(GET_ADJACENT_CHAPTERS); - } - - public void appendNextChapter() { - if (nextChapter == null) - return; - - if (appenderSubscription != null) - remove(appenderSubscription); - - nextChapter.status = isChapterDownloaded(nextChapter) ? Download.DOWNLOADED : Download.NOT_DOWNLOADED; - - appenderSubscription = getPageListObservable(nextChapter) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .compose(deliverLatestCache()) - .subscribe(split((view, chapter) -> { - view.onAppendChapter(chapter); - }, (view, error) -> { - view.onChapterAppendError(); - })); - - add(appenderSubscription); - } - - // Check whether the given chapter is downloaded - public boolean isChapterDownloaded(Chapter chapter) { - return downloadManager.isChapterDownloaded(source, manga, chapter); - } - - public void retryPage(Page page) { - if (page != null) { - page.setStatus(Page.QUEUE); - if (page.getImagePath() != null) { - File file = new File(page.getImagePath()); - chapterCache.removeFileFromCache(file.getName()); - } - retryPageSubject.onNext(page); - } - } - - // Called before loading another chapter or leaving the reader. It allows to do operations - // over the chapter read like saving progress - public void onChapterLeft() { - List pages = activeChapter.getPages(); - if (pages == null) - return; - - // Get the last page read - int activePageNumber = activeChapter.last_page_read; - - // Just in case, avoid out of index exceptions - if (activePageNumber >= pages.size()) { - activePageNumber = pages.size() - 1; - } - Page activePage = pages.get(activePageNumber); - - // Cache current page list progress for online chapters to allow a faster reopen - if (!activeChapter.isDownloaded()) { - source.savePageList(activeChapter.url, pages); - } - - // Save current progress of the chapter. Mark as read if the chapter is finished - if (activePage.isLastPage()) { - activeChapter.read = true; - } - db.insertChapter(activeChapter).asRxObservable().subscribe(); - } - - public int getMangaSyncChapterToUpdate() { - if (activeChapter.getPages() == null || mangaSyncList == null || mangaSyncList.isEmpty()) - return 0; - - int lastChapterReadLocal = 0; - // If the current chapter has been read, we check with this one - if (activeChapter.read) - lastChapterReadLocal = (int) Math.floor(activeChapter.chapter_number); - // If not, we check if the previous chapter has been read - else if (previousChapter != null && previousChapter.read) - lastChapterReadLocal = (int) Math.floor(previousChapter.chapter_number); - - // We know the chapter we have to check, but we don't know yet if an update is required. - // This boolean is used to return 0 if no update is required - boolean hasToUpdate = false; - - for (MangaSync mangaSync : mangaSyncList) { - if (lastChapterReadLocal > mangaSync.last_chapter_read) { - mangaSync.last_chapter_read = lastChapterReadLocal; - mangaSync.update = true; - hasToUpdate = true; - } - } - return hasToUpdate ? lastChapterReadLocal : 0; - } - - public void updateMangaSyncLastChapterRead() { - for (MangaSync mangaSync : mangaSyncList) { - MangaSyncService service = syncManager.getService(mangaSync.sync_id); - if (service.isLogged() && mangaSync.update) { - UpdateMangaSyncService.start(getContext(), mangaSync); - } - } - } - - public void setCurrentPage(Page currentPage) { - this.currentPage = currentPage; - } - - public boolean loadNextChapter() { - if (hasNextChapter()) { - onChapterLeft(); - loadChapter(nextChapter, 0); - return true; - } - return false; - } - - public boolean loadPreviousChapter() { - if (hasPreviousChapter()) { - onChapterLeft(); - loadChapter(previousChapter, -1); - return true; - } - return false; - } - - public boolean hasNextChapter() { - return nextChapter != null; - } - - public boolean hasPreviousChapter() { - return previousChapter != null; - } - - private void preloadNextChapter() { - if (hasNextChapter() && !isChapterDownloaded(nextChapter)) { - start(PRELOAD_NEXT_CHAPTER); - } - } - - private void stopPreloadingNextChapter() { - if (!isUnsubscribed(PRELOAD_NEXT_CHAPTER)) { - stop(PRELOAD_NEXT_CHAPTER); - if (nextChapter.getPages() != null) - source.savePageList(nextChapter.url, nextChapter.getPages()); - } - } - - public void updateMangaViewer(int viewer) { - manga.viewer = viewer; - db.insertManga(manga).executeAsBlocking(); - } - - public Manga getManga() { - return manga; - } - - public Page getCurrentPage() { - return currentPage; - } - - public boolean isSeamlessMode() { - return seamlessMode; - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt new file mode 100644 index 0000000000..f21a127389 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -0,0 +1,421 @@ +package eu.kanade.tachiyomi.ui.reader + +import android.os.Bundle +import android.util.Pair +import eu.kanade.tachiyomi.data.cache.ChapterCache +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaSync +import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager +import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.source.SourceManager +import eu.kanade.tachiyomi.data.source.base.Source +import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.event.ReaderEvent +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import rx.subjects.PublishSubject +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +class ReaderPresenter : BasePresenter() { + + @Inject lateinit var prefs: PreferencesHelper + @Inject lateinit var db: DatabaseHelper + @Inject lateinit var downloadManager: DownloadManager + @Inject lateinit var syncManager: MangaSyncManager + @Inject lateinit var sourceManager: SourceManager + @Inject lateinit var chapterCache: ChapterCache + + lateinit var manga: Manga + private set + + lateinit var chapter: Chapter + private set + + lateinit var source: Source + private set + + var requestedPage: Int = 0 + var currentPage: Page? = null + private var nextChapter: Chapter? = null + private var previousChapter: Chapter? = null + private var mangaSyncList: List? = null + + private lateinit var retryPageSubject: PublishSubject + private lateinit var pageInitializerSubject: PublishSubject + + val isSeamlessMode by lazy { prefs.seamlessMode() } + + private var appenderSubscription: Subscription? = null + + private val GET_PAGE_LIST = 1 + private val GET_ADJACENT_CHAPTERS = 2 + private val GET_MANGA_SYNC = 3 + private val PRELOAD_NEXT_CHAPTER = 4 + + private val MANGA_KEY = "manga_key" + private val CHAPTER_KEY = "chapter_key" + private val PAGE_KEY = "page_key" + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + if (savedState != null) { + source = sourceManager.get(manga.source)!! + manga = savedState.getSerializable(MANGA_KEY) as Manga + chapter = savedState.getSerializable(CHAPTER_KEY) as Chapter + requestedPage = savedState.getInt(PAGE_KEY) + initializeSubjects() + } + + startableLatestCache(GET_ADJACENT_CHAPTERS, + { getAdjacentChaptersObservable() }, + { view, pair -> view.onAdjacentChapters(pair.first, pair.second) }) + + startable(PRELOAD_NEXT_CHAPTER, { getPreloadNextChapterObservable() }, + { }, + { error -> Timber.e("Error preloading chapter") }) + + + restartable(GET_MANGA_SYNC, + { getMangaSyncObservable().subscribe() }) + + restartableLatestCache(GET_PAGE_LIST, + { getPageListObservable(chapter) }, + { view, chapter -> view.onChapterReady(manga, chapter, currentPage) }, + { view, error -> view.onChapterError() }) + + if (savedState == null) { + registerForEvents() + } + } + + override fun onDestroy() { + unregisterForEvents() + super.onDestroy() + } + + override fun onSave(state: Bundle) { + onChapterLeft() + state.putSerializable(MANGA_KEY, manga) + state.putSerializable(CHAPTER_KEY, chapter) + state.putSerializable(PAGE_KEY, requestedPage) + super.onSave(state) + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + fun onEvent(event: ReaderEvent) { + EventBus.getDefault().removeStickyEvent(event) + manga = event.manga + source = sourceManager.get(manga.source)!! + initializeSubjects() + loadChapter(event.chapter) + if (prefs.autoUpdateMangaSync()) { + start(GET_MANGA_SYNC) + } + } + + private fun initializeSubjects() { + // Listen for pages initialization events + pageInitializerSubject = PublishSubject.create() + add(pageInitializerSubject.observeOn(Schedulers.io()) + .concatMap { ch -> + val observable: Observable + if (ch.isDownloaded) { + val chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, ch) + observable = Observable.from(ch.pages) + .flatMap { downloadManager.getDownloadedImage(it, chapterDir) } + } else { + observable = source.getAllImageUrlsFromPageList(ch.pages) + .flatMap({ source.getCachedImage(it) }, 2) + .doOnCompleted { source.savePageList(ch.url, ch.pages) } + } + observable.doOnCompleted { + if (!isSeamlessMode && chapter === ch) { + preloadNextChapter() + } + } + }.subscribe()) + + // Listen por retry events + retryPageSubject = PublishSubject.create() + add(retryPageSubject.observeOn(Schedulers.io()) + .flatMap { page -> + if (page.imageUrl == null) + source.getImageUrlFromPage(page) + else + Observable.just(page) + } + .flatMap { source.getCachedImage(it) } + .subscribe()) + } + + // Returns the page list of a chapter + private fun getPageListObservable(chapter: Chapter): Observable { + val observable: Observable> = if (chapter.isDownloaded) + // Fetch the page list from disk + Observable.just(downloadManager.getSavedPageList(source, manga, chapter)!!) + else + // Fetch the page list from cache or fallback to network + source.getCachedPageListOrPullFromNetwork(chapter.url) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + + return observable.map { pages -> + for (page in pages) { + page.chapter = chapter + } + chapter.pages = pages + if (requestedPage >= -1 || currentPage == null) { + if (requestedPage == -1) { + currentPage = pages[pages.size - 1] + } else { + currentPage = pages[requestedPage] + } + } + requestedPage = -2 + pageInitializerSubject.onNext(chapter) + chapter + } + } + + private fun getAdjacentChaptersObservable(): Observable> { + return Observable.zip( + db.getPreviousChapter(chapter).asRxObservable().take(1), + db.getNextChapter(chapter).asRxObservable().take(1), + { a, b -> Pair.create(a, b) }) + .doOnNext { pair -> + previousChapter = pair.first + nextChapter = pair.second + } + .observeOn(AndroidSchedulers.mainThread()) + } + + // Preload the first pages of the next chapter. Only for non seamless mode + private fun getPreloadNextChapterObservable(): Observable { + return source.getCachedPageListOrPullFromNetwork(nextChapter!!.url) + .flatMap { pages -> + nextChapter!!.pages = pages + val pagesToPreload = Math.min(pages.size, 5) + Observable.from(pages).take(pagesToPreload) + } + // Preload up to 5 images + .concatMap { page -> + if (page.imageUrl == null) + source.getImageUrlFromPage(page) + else + Observable.just(page) + } + // Download the first image + .concatMap { page -> + if (page.pageNumber == 0) + source.getCachedImage(page) + else + Observable.just(page) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnCompleted { stopPreloadingNextChapter() } + } + + private fun getMangaSyncObservable(): Observable> { + return db.getMangasSync(manga).asRxObservable() + .take(1) + .doOnNext { mangaSyncList = it } + } + + // Loads the given chapter + private fun loadChapter(chapter: Chapter, requestedPage: Int = 0) { + if (isSeamlessMode) { + if (appenderSubscription != null) + remove(appenderSubscription) + } else { + stopPreloadingNextChapter() + } + + this.chapter = chapter + chapter.status = if (isChapterDownloaded(chapter)) Download.DOWNLOADED else Download.NOT_DOWNLOADED + + // If the chapter is partially read, set the starting page to the last the user read + if (!chapter.read && chapter.last_page_read != 0) + this.requestedPage = chapter.last_page_read + else + this.requestedPage = requestedPage + + // Reset next and previous chapter. They have to be fetched again + nextChapter = null + previousChapter = null + + start(GET_PAGE_LIST) + start(GET_ADJACENT_CHAPTERS) + } + + fun setActiveChapter(chapter: Chapter) { + onChapterLeft() + this.chapter = chapter + nextChapter = null + previousChapter = null + start(GET_ADJACENT_CHAPTERS) + } + + fun appendNextChapter() { + if (nextChapter == null) + return + + if (appenderSubscription != null) + remove(appenderSubscription) + + nextChapter?.let { + if (appenderSubscription != null) + remove(appenderSubscription) + + it.status = if (isChapterDownloaded(it)) Download.DOWNLOADED else Download.NOT_DOWNLOADED + + appenderSubscription = getPageListObservable(it).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .compose(deliverLatestCache()) + .subscribe(split({ view, chapter -> + view.onAppendChapter(chapter) + }, { view, error -> + view.onChapterAppendError() + })) + + add(appenderSubscription) + + } + } + + // Check whether the given chapter is downloaded + fun isChapterDownloaded(chapter: Chapter): Boolean { + return downloadManager.isChapterDownloaded(source, manga, chapter) + } + + fun retryPage(page: Page?) { + if (page != null) { + page.status = Page.QUEUE + if (page.imagePath != null) { + val file = File(page.imagePath) + chapterCache.removeFileFromCache(file.name) + } + retryPageSubject.onNext(page) + } + } + + // Called before loading another chapter or leaving the reader. It allows to do operations + // over the chapter read like saving progress + fun onChapterLeft() { + val pages = chapter.pages ?: return + + // Get the last page read + var activePageNumber = chapter.last_page_read + + // Just in case, avoid out of index exceptions + if (activePageNumber >= pages.size) { + activePageNumber = pages.size - 1 + } + val activePage = pages[activePageNumber] + + // Cache current page list progress for online chapters to allow a faster reopen + if (!chapter.isDownloaded) { + source.savePageList(chapter.url, pages) + } + + // Save current progress of the chapter. Mark as read if the chapter is finished + if (activePage.isLastPage) { + chapter.read = true + } + db.insertChapter(chapter).asRxObservable().subscribe() + } + + // If the current chapter has been read, we check with this one + // If not, we check if the previous chapter has been read + // We know the chapter we have to check, but we don't know yet if an update is required. + // This boolean is used to return 0 if no update is required + fun getMangaSyncChapterToUpdate(): Int { + if (chapter.pages == null || mangaSyncList == null || mangaSyncList!!.isEmpty()) + return 0 + + var lastChapterReadLocal = 0 + if (chapter.read) + lastChapterReadLocal = Math.floor(chapter.chapter_number.toDouble()).toInt() + else if (previousChapter != null && previousChapter!!.read) + lastChapterReadLocal = Math.floor(previousChapter!!.chapter_number.toDouble()).toInt() + var hasToUpdate = false + + for (mangaSync in mangaSyncList!!) { + if (lastChapterReadLocal > mangaSync.last_chapter_read) { + mangaSync.last_chapter_read = lastChapterReadLocal + mangaSync.update = true + hasToUpdate = true + } + } + return if (hasToUpdate) lastChapterReadLocal else 0 + } + + fun updateMangaSyncLastChapterRead() { + for (mangaSync in mangaSyncList!!) { + val service = syncManager.getService(mangaSync.sync_id) + if (service.isLogged && mangaSync.update) { + UpdateMangaSyncService.start(context, mangaSync) + } + } + } + + fun loadNextChapter(): Boolean { + nextChapter?.let { + onChapterLeft() + loadChapter(it, 0) + return true + } + return false + } + + fun loadPreviousChapter(): Boolean { + previousChapter?.let { + onChapterLeft() + loadChapter(it, 0) + return true + } + return false + } + + fun hasNextChapter(): Boolean { + return nextChapter != null + } + + fun hasPreviousChapter(): Boolean { + return previousChapter != null + } + + private fun preloadNextChapter() { + if (hasNextChapter() && !isChapterDownloaded(nextChapter!!)) { + start(PRELOAD_NEXT_CHAPTER) + } + } + + private fun stopPreloadingNextChapter() { + if (!isUnsubscribed(PRELOAD_NEXT_CHAPTER)) { + stop(PRELOAD_NEXT_CHAPTER) + if (nextChapter!!.pages != null) + source.savePageList(nextChapter!!.url, nextChapter!!.pages) + } + } + + fun updateMangaViewer(viewer: Int) { + manga.viewer = viewer + db.insertManga(manga).executeAsBlocking() + } + +}