Browse Source

Reader presenter in Kotlin + remove Icepick

len 9 years ago
parent
commit
0d519b3d16

+ 0 - 3
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'

+ 1 - 1
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();
     }

+ 0 - 12
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)

+ 0 - 12
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)
     }

+ 0 - 12
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<V : ViewWithPresenter<*>> : RxPresenter<V>() {
 
     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)
     }

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt

@@ -172,7 +172,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), 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

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt

@@ -144,7 +144,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
     override fun onBackPressed() {
         presenter.onChapterLeft()
 
-        val chapterToUpdate = presenter.mangaSyncChapterToUpdate
+        val chapterToUpdate = presenter.getMangaSyncChapterToUpdate()
 
         if (chapterToUpdate > 0) {
             if (presenter.prefs.askUpdateMangaSync()) {

+ 0 - 424
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.java

@@ -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<ReaderActivity> {
-
-    @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<MangaSync> mangaSyncList;
-
-    private PublishSubject<Page> retryPageSubject;
-    private PublishSubject<Chapter> 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<Chapter> 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<Pair<Chapter, Chapter>> 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<Page> 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<List<MangaSync>> 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<Page> 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;
-    }
-}

+ 421 - 0
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<ReaderActivity>() {
+
+    @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<MangaSync>? = null
+
+    private lateinit var retryPageSubject: PublishSubject<Page>
+    private lateinit var pageInitializerSubject: PublishSubject<Chapter>
+
+    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<Chapter>()
+        add(pageInitializerSubject.observeOn(Schedulers.io())
+                .concatMap { ch ->
+                    val observable: Observable<Page>
+                    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<Page>()
+        add(retryPageSubject.observeOn(Schedulers.io())
+                .flatMap { page ->
+                    if (page.imageUrl == null)
+                        source.getImageUrlFromPage(page)
+                    else
+                        Observable.just<Page>(page)
+                }
+                .flatMap { source.getCachedImage(it) }
+                .subscribe())
+    }
+
+    // Returns the page list of a chapter
+    private fun getPageListObservable(chapter: Chapter): Observable<Chapter> {
+        val observable: Observable<List<Page>> = 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<Pair<Chapter, Chapter>> {
+        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<Page> {
+        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>(page)
+                }
+                // Download the first image
+                .concatMap { page ->
+                    if (page.pageNumber == 0)
+                        source.getCachedImage(page)
+                    else
+                        Observable.just<Page>(page)
+                }
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .doOnCompleted { stopPreloadingNextChapter() }
+    }
+
+    private fun getMangaSyncObservable(): Observable<List<MangaSync>> {
+        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<Chapter>())
+                    .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()
+    }
+
+}