ReaderPresenter.kt 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. package eu.kanade.tachiyomi.ui.reader
  2. import android.os.Bundle
  3. import android.os.Environment
  4. import android.webkit.MimeTypeMap
  5. import eu.kanade.tachiyomi.R
  6. import eu.kanade.tachiyomi.data.cache.ChapterCache
  7. import eu.kanade.tachiyomi.data.cache.CoverCache
  8. import eu.kanade.tachiyomi.data.database.DatabaseHelper
  9. import eu.kanade.tachiyomi.data.database.models.Chapter
  10. import eu.kanade.tachiyomi.data.database.models.History
  11. import eu.kanade.tachiyomi.data.database.models.Manga
  12. import eu.kanade.tachiyomi.data.database.models.Track
  13. import eu.kanade.tachiyomi.data.download.DownloadManager
  14. import eu.kanade.tachiyomi.data.preference.PreferencesHelper
  15. import eu.kanade.tachiyomi.data.track.TrackManager
  16. import eu.kanade.tachiyomi.source.LocalSource
  17. import eu.kanade.tachiyomi.source.SourceManager
  18. import eu.kanade.tachiyomi.source.model.Page
  19. import eu.kanade.tachiyomi.source.online.HttpSource
  20. import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
  21. import eu.kanade.tachiyomi.util.DiskUtil
  22. import eu.kanade.tachiyomi.util.RetryWithDelay
  23. import eu.kanade.tachiyomi.util.SharedData
  24. import eu.kanade.tachiyomi.util.toast
  25. import rx.Observable
  26. import rx.Subscription
  27. import rx.android.schedulers.AndroidSchedulers
  28. import rx.schedulers.Schedulers
  29. import timber.log.Timber
  30. import uy.kohesive.injekt.Injekt
  31. import uy.kohesive.injekt.api.get
  32. import java.io.File
  33. import java.net.URLConnection
  34. import java.util.Comparator
  35. import java.util.Date
  36. /**
  37. * Presenter of [ReaderActivity].
  38. */
  39. class ReaderPresenter(
  40. val prefs: PreferencesHelper = Injekt.get(),
  41. val db: DatabaseHelper = Injekt.get(),
  42. val downloadManager: DownloadManager = Injekt.get(),
  43. val trackManager: TrackManager = Injekt.get(),
  44. val sourceManager: SourceManager = Injekt.get(),
  45. val chapterCache: ChapterCache = Injekt.get(),
  46. val coverCache: CoverCache = Injekt.get()
  47. ) : BasePresenter<ReaderActivity>() {
  48. private val context = prefs.context
  49. /**
  50. * Manga being read.
  51. */
  52. lateinit var manga: Manga
  53. private set
  54. /**
  55. * Active chapter.
  56. */
  57. lateinit var chapter: ReaderChapter
  58. private set
  59. /**
  60. * Previous chapter of the active.
  61. */
  62. private var prevChapter: ReaderChapter? = null
  63. /**
  64. * Next chapter of the active.
  65. */
  66. private var nextChapter: ReaderChapter? = null
  67. /**
  68. * Source of the manga.
  69. */
  70. private val source by lazy { sourceManager.getOrStub(manga.source) }
  71. /**
  72. * Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
  73. * time in a background thread to avoid blocking the UI.
  74. */
  75. private val chapterList by lazy {
  76. val dbChapters = db.getChapters(manga).executeAsBlocking().map { it.toModel() }
  77. val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
  78. Manga.SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
  79. Manga.SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
  80. else -> throw NotImplementedError("Unknown sorting method")
  81. }
  82. dbChapters.sortedWith(Comparator<Chapter> { c1, c2 -> sortFunction(c1, c2) })
  83. }
  84. /**
  85. * Map of chapters that have been loaded in the reader.
  86. */
  87. private val loadedChapters = hashMapOf<Long?, ReaderChapter>()
  88. /**
  89. * List of manga services linked to the active manga, or null if auto syncing is not enabled.
  90. */
  91. private var trackList: List<Track>? = null
  92. /**
  93. * Chapter loader whose job is to obtain the chapter list and initialize every page.
  94. */
  95. private val loader by lazy { ChapterLoader(downloadManager, manga, source) }
  96. /**
  97. * Subscription for appending a chapter to the reader (seamless mode).
  98. */
  99. private var appenderSubscription: Subscription? = null
  100. /**
  101. * Subscription for retrieving the adjacent chapters to the current one.
  102. */
  103. private var adjacentChaptersSubscription: Subscription? = null
  104. /**
  105. * Whether the active chapter has been loaded.
  106. */
  107. private var chapterLoaded = false
  108. companion object {
  109. /**
  110. * Id of the restartable that loads the active chapter.
  111. */
  112. private const val LOAD_ACTIVE_CHAPTER = 1
  113. }
  114. override fun onCreate(savedState: Bundle?) {
  115. super.onCreate(savedState)
  116. if (savedState == null) {
  117. val event = SharedData.get(ReaderEvent::class.java) ?: return
  118. manga = event.manga
  119. chapter = event.chapter.toModel()
  120. } else {
  121. manga = savedState.getSerializable(ReaderPresenter::manga.name) as Manga
  122. chapter = savedState.getSerializable(ReaderPresenter::chapter.name) as ReaderChapter
  123. }
  124. // Send the active manga to the view to initialize the reader.
  125. Observable.just(manga)
  126. .subscribeLatestCache({ view, manga -> view.onMangaOpen(manga) })
  127. // Retrieve the sync list if auto syncing is enabled.
  128. if (prefs.autoUpdateTrack()) {
  129. add(db.getTracks(manga).asRxSingle()
  130. .subscribe({ trackList = it }))
  131. }
  132. restartableLatestCache(LOAD_ACTIVE_CHAPTER,
  133. { loadChapterObservable(chapter) },
  134. { view, _ -> view.onChapterReady(this.chapter) },
  135. { view, error -> view.onChapterError(error) })
  136. if (savedState == null) {
  137. loadChapter(chapter)
  138. }
  139. }
  140. override fun onSave(state: Bundle) {
  141. chapter.requestedPage = chapter.last_page_read
  142. state.putSerializable(ReaderPresenter::manga.name, manga)
  143. state.putSerializable(ReaderPresenter::chapter.name, chapter)
  144. super.onSave(state)
  145. }
  146. override fun onDestroy() {
  147. loader.cleanup()
  148. onChapterLeft()
  149. super.onDestroy()
  150. }
  151. /**
  152. * Converts a chapter to a [ReaderChapter] if needed.
  153. */
  154. private fun Chapter.toModel(): ReaderChapter {
  155. if (this is ReaderChapter) return this
  156. return ReaderChapter(this)
  157. }
  158. /**
  159. * Returns an observable that loads the given chapter, discarding any previous work.
  160. *
  161. * @param chapter the now active chapter.
  162. */
  163. private fun loadChapterObservable(chapter: ReaderChapter): Observable<ReaderChapter> {
  164. loader.restart()
  165. return loader.loadChapter(chapter)
  166. .subscribeOn(Schedulers.io())
  167. .observeOn(AndroidSchedulers.mainThread())
  168. .doOnNext { chapterLoaded = true }
  169. }
  170. /**
  171. * Obtains the adjacent chapters of the given one in a background thread, and notifies the view
  172. * when they are known.
  173. *
  174. * @param chapter the current active chapter.
  175. */
  176. private fun getAdjacentChapters(chapter: ReaderChapter) {
  177. // Keep only one subscription
  178. adjacentChaptersSubscription?.let { remove(it) }
  179. adjacentChaptersSubscription = Observable
  180. .fromCallable { getAdjacentChaptersStrategy(chapter) }
  181. .doOnNext { pair ->
  182. prevChapter = loadedChapters.getOrElse(pair.first?.id) { pair.first }
  183. nextChapter = loadedChapters.getOrElse(pair.second?.id) { pair.second }
  184. }
  185. .subscribeOn(Schedulers.io())
  186. .observeOn(AndroidSchedulers.mainThread())
  187. .subscribeLatestCache({ view, pair ->
  188. view.onAdjacentChapters(pair.first, pair.second)
  189. })
  190. }
  191. /**
  192. * Returns the previous and next chapters of the given one in a [Pair] according to the sorting
  193. * strategy set for the manga.
  194. *
  195. * @param chapter the current active chapter.
  196. * @param previousChapterAmount the desired number of chapters preceding the current active chapter (Default: 1).
  197. * @param nextChapterAmount the desired number of chapters succeeding the current active chapter (Default: 1).
  198. */
  199. private fun getAdjacentChaptersStrategy(chapter: ReaderChapter, previousChapterAmount: Int = 1, nextChapterAmount: Int = 1) = when (manga.sorting) {
  200. Manga.SORTING_SOURCE -> {
  201. val currChapterIndex = chapterList.indexOfFirst { chapter.id == it.id }
  202. val nextChapter = chapterList.getOrNull(currChapterIndex + nextChapterAmount)
  203. val prevChapter = chapterList.getOrNull(currChapterIndex - previousChapterAmount)
  204. Pair(prevChapter, nextChapter)
  205. }
  206. Manga.SORTING_NUMBER -> {
  207. val currChapterIndex = chapterList.indexOfFirst { chapter.id == it.id }
  208. val chapterNumber = chapter.chapter_number
  209. var prevChapter: ReaderChapter? = null
  210. for (i in (currChapterIndex - previousChapterAmount) downTo 0) {
  211. val c = chapterList[i]
  212. if (c.chapter_number < chapterNumber && c.chapter_number >= chapterNumber - previousChapterAmount) {
  213. prevChapter = c
  214. break
  215. }
  216. }
  217. var nextChapter: ReaderChapter? = null
  218. for (i in (currChapterIndex + nextChapterAmount) until chapterList.size) {
  219. val c = chapterList[i]
  220. if (c.chapter_number > chapterNumber && c.chapter_number <= chapterNumber + nextChapterAmount) {
  221. nextChapter = c
  222. break
  223. }
  224. }
  225. Pair(prevChapter, nextChapter)
  226. }
  227. else -> throw NotImplementedError("Unknown sorting method")
  228. }
  229. /**
  230. * Loads the given chapter and sets it as the active one. This method also accepts a requested
  231. * page, which will be set as active when it's displayed in the view.
  232. *
  233. * @param chapter the chapter to load.
  234. * @param requestedPage the requested page from the view.
  235. */
  236. private fun loadChapter(chapter: ReaderChapter, requestedPage: Int = 0) {
  237. // Cleanup any append.
  238. appenderSubscription?.let { remove(it) }
  239. this.chapter = loadedChapters.getOrPut(chapter.id) { chapter }
  240. // If the chapter is partially read, set the starting page to the last the user read
  241. // otherwise use the requested page.
  242. chapter.requestedPage = if (!chapter.read) chapter.last_page_read else requestedPage
  243. // Reset next and previous chapter. They have to be fetched again
  244. nextChapter = null
  245. prevChapter = null
  246. chapterLoaded = false
  247. start(LOAD_ACTIVE_CHAPTER)
  248. getAdjacentChapters(chapter)
  249. }
  250. /**
  251. * Changes the active chapter, but doesn't load anything. Called when changing chapters from
  252. * the reader with the seamless mode.
  253. *
  254. * @param chapter the chapter to set as active.
  255. */
  256. fun setActiveChapter(chapter: ReaderChapter) {
  257. onChapterLeft()
  258. this.chapter = chapter
  259. nextChapter = null
  260. prevChapter = null
  261. getAdjacentChapters(chapter)
  262. }
  263. /**
  264. * Appends the next chapter to the reader, if possible.
  265. */
  266. fun appendNextChapter() {
  267. appenderSubscription?.let { remove(it) }
  268. val nextChapter = nextChapter ?: return
  269. val chapterToLoad = loadedChapters.getOrPut(nextChapter.id) { nextChapter }
  270. appenderSubscription = loader.loadChapter(chapterToLoad)
  271. .subscribeOn(Schedulers.io())
  272. .retryWhen(RetryWithDelay(1, { 3000 }))
  273. .observeOn(AndroidSchedulers.mainThread())
  274. .subscribeLatestCache({ view, chapter ->
  275. view.onAppendChapter(chapter)
  276. }, { view, _ ->
  277. view.onChapterAppendError()
  278. })
  279. }
  280. /**
  281. * Retries a page that failed to load due to network error or corruption.
  282. *
  283. * @param page the page that failed.
  284. */
  285. fun retryPage(page: Page?) {
  286. if (page != null && source is HttpSource) {
  287. page.status = Page.QUEUE
  288. val imageUrl = page.imageUrl
  289. if (imageUrl != null && !page.chapter.isDownloaded) {
  290. val key = DiskUtil.hashKeyForDisk(page.url)
  291. chapterCache.removeFileFromCache(key)
  292. }
  293. loader.retryPage(page)
  294. }
  295. }
  296. /**
  297. * Called before loading another chapter or leaving the reader. It allows to do operations
  298. * over the chapter read like saving progress
  299. */
  300. fun onChapterLeft() {
  301. // Reference these locally because they are needed later from another thread.
  302. val chapter = chapter
  303. val pages = chapter.pages ?: return
  304. Observable.fromCallable {
  305. // Cache current page list progress for online chapters to allow a faster reopen
  306. if (!chapter.isDownloaded) {
  307. source.let {
  308. if (it is HttpSource) chapterCache.putPageListToCache(chapter, pages)
  309. }
  310. }
  311. try {
  312. if (chapter.read) {
  313. val removeAfterReadSlots = prefs.removeAfterReadSlots()
  314. when (removeAfterReadSlots) {
  315. // Setting disabled
  316. -1 -> { /* Empty function */ }
  317. // Remove current read chapter
  318. 0 -> deleteChapter(chapter, manga)
  319. // Remove previous chapter specified by user in settings.
  320. else -> getAdjacentChaptersStrategy(chapter, removeAfterReadSlots)
  321. .first?.let { deleteChapter(it, manga) }
  322. }
  323. }
  324. } catch (error: Exception) {
  325. // TODO find out why it crashes
  326. Timber.e(error)
  327. }
  328. db.updateChapterProgress(chapter).executeAsBlocking()
  329. try {
  330. val history = History.create(chapter).apply { last_read = Date().time }
  331. db.updateHistoryLastRead(history).executeAsBlocking()
  332. } catch (error: Exception) {
  333. // TODO find out why it crashes
  334. Timber.e(error)
  335. }
  336. }
  337. .subscribeOn(Schedulers.io())
  338. .subscribe()
  339. }
  340. /**
  341. * Called when the active page changes in the reader.
  342. *
  343. * @param page the active page
  344. */
  345. fun onPageChanged(page: Page) {
  346. val chapter = page.chapter
  347. chapter.last_page_read = page.index
  348. if (chapter.pages!!.last() === page) {
  349. chapter.read = true
  350. }
  351. if (!chapter.isDownloaded && page.status == Page.QUEUE) {
  352. loader.loadPriorizedPage(page)
  353. }
  354. }
  355. /**
  356. * Delete selected chapter
  357. *
  358. * @param chapter chapter that is selected
  359. * @param manga manga that belongs to chapter
  360. */
  361. fun deleteChapter(chapter: ReaderChapter, manga: Manga) {
  362. chapter.isDownloaded = false
  363. chapter.pages?.forEach { it.status == Page.QUEUE }
  364. downloadManager.deleteChapter(chapter, manga, source)
  365. }
  366. /**
  367. * Returns the chapter to be marked as last read in sync services or 0 if no update required.
  368. */
  369. fun getTrackChapterToUpdate(): Int {
  370. val trackList = trackList
  371. if (chapter.pages == null || trackList == null || trackList.isEmpty())
  372. return 0
  373. val prevChapter = prevChapter
  374. // Get the last chapter read from the reader.
  375. val lastChapterRead = if (chapter.read)
  376. Math.floor(chapter.chapter_number.toDouble()).toInt()
  377. else if (prevChapter != null && prevChapter.read)
  378. Math.floor(prevChapter.chapter_number.toDouble()).toInt()
  379. else
  380. return 0
  381. return if (trackList.any { lastChapterRead > it.last_chapter_read })
  382. lastChapterRead
  383. else
  384. 0
  385. }
  386. /**
  387. * Starts the service that updates the last chapter read in sync services
  388. */
  389. fun updateTrackLastChapterRead(lastChapterRead: Int) {
  390. trackList?.forEach { track ->
  391. val service = trackManager.getService(track.sync_id)
  392. if (service != null && service.isLogged && lastChapterRead > track.last_chapter_read) {
  393. track.last_chapter_read = lastChapterRead
  394. // We wan't these to execute even if the presenter is destroyed and leaks for a
  395. // while. The view can still be garbage collected.
  396. Observable.defer { service.update(track) }
  397. .map { db.insertTrack(track).executeAsBlocking() }
  398. .subscribeOn(Schedulers.io())
  399. .observeOn(AndroidSchedulers.mainThread())
  400. .subscribe({}, { Timber.e(it) })
  401. }
  402. }
  403. }
  404. /**
  405. * Loads the next chapter.
  406. *
  407. * @return true if the next chapter is being loaded, false if there is no next chapter.
  408. */
  409. fun loadNextChapter(): Boolean {
  410. // Avoid skipping chapters.
  411. if (!chapterLoaded) return true
  412. nextChapter?.let {
  413. onChapterLeft()
  414. loadChapter(it, 0)
  415. return true
  416. }
  417. return false
  418. }
  419. /**
  420. * Loads the next chapter.
  421. *
  422. * @return true if the previous chapter is being loaded, false if there is no previous chapter.
  423. */
  424. fun loadPreviousChapter(): Boolean {
  425. // Avoid skipping chapters.
  426. if (!chapterLoaded) return true
  427. prevChapter?.let {
  428. onChapterLeft()
  429. loadChapter(it, if (it.read) -1 else 0)
  430. return true
  431. }
  432. return false
  433. }
  434. /**
  435. * Returns true if there's a next chapter.
  436. */
  437. fun hasNextChapter(): Boolean {
  438. return nextChapter != null
  439. }
  440. /**
  441. * Returns true if there's a previous chapter.
  442. */
  443. fun hasPreviousChapter(): Boolean {
  444. return prevChapter != null
  445. }
  446. /**
  447. * Updates the viewer for this manga.
  448. *
  449. * @param viewer the id of the viewer to set.
  450. */
  451. fun updateMangaViewer(viewer: Int) {
  452. manga.viewer = viewer
  453. db.insertManga(manga).executeAsBlocking()
  454. }
  455. /**
  456. * Update cover with page file.
  457. */
  458. internal fun setImageAsCover(page: Page) {
  459. try {
  460. if (manga.source == LocalSource.ID) {
  461. val input = context.contentResolver.openInputStream(page.uri)
  462. LocalSource.updateCover(context, manga, input)
  463. context.toast(R.string.cover_updated)
  464. return
  465. }
  466. val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found")
  467. if (manga.favorite) {
  468. val input = context.contentResolver.openInputStream(page.uri)
  469. coverCache.copyToCache(thumbUrl, input)
  470. context.toast(R.string.cover_updated)
  471. } else {
  472. context.toast(R.string.notification_first_add_to_library)
  473. }
  474. } catch (error: Exception) {
  475. context.toast(R.string.notification_cover_update_failed)
  476. Timber.e(error)
  477. }
  478. }
  479. /**
  480. * Save page to local storage.
  481. */
  482. internal fun savePage(page: Page) {
  483. if (page.status != Page.READY)
  484. return
  485. // Used to show image notification.
  486. val imageNotifier = SaveImageNotifier(context)
  487. // Remove the notification if it already exists (user feedback).
  488. imageNotifier.onClear()
  489. // Pictures directory.
  490. val pictureDirectory = Environment.getExternalStorageDirectory().absolutePath +
  491. File.separator + Environment.DIRECTORY_PICTURES +
  492. File.separator + context.getString(R.string.app_name)
  493. // Copy file in background.
  494. Observable
  495. .fromCallable {
  496. // Folder where the image will be saved.
  497. val destDir = File(pictureDirectory)
  498. destDir.mkdirs()
  499. // Find out file mime type.
  500. val mime = context.contentResolver.getType(page.uri)
  501. ?: context.contentResolver.openInputStream(page.uri).buffered().use {
  502. URLConnection.guessContentTypeFromStream(it)
  503. }
  504. // Build destination file.
  505. val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
  506. val filename = DiskUtil.buildValidFilename(
  507. "${manga.title} - ${chapter.name}") + " - ${page.number}.$ext"
  508. val destFile = File(destDir, filename)
  509. context.contentResolver.openInputStream(page.uri).use { input ->
  510. destFile.outputStream().use { output ->
  511. input.copyTo(output)
  512. }
  513. }
  514. DiskUtil.scanMedia(context, destFile)
  515. imageNotifier.onComplete(destFile)
  516. }
  517. .subscribeOn(Schedulers.io())
  518. .observeOn(AndroidSchedulers.mainThread())
  519. .subscribe({
  520. context.toast(R.string.picture_saved)
  521. }, { error ->
  522. Timber.e(error)
  523. imageNotifier.onError(error.message)
  524. })
  525. }
  526. }