LibraryUpdateService.kt 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. package eu.kanade.tachiyomi.data.library
  2. import android.app.PendingIntent
  3. import android.app.Service
  4. import android.content.Context
  5. import android.content.Intent
  6. import android.graphics.BitmapFactory
  7. import android.os.IBinder
  8. import android.os.PowerManager
  9. import android.support.v4.app.NotificationCompat
  10. import eu.kanade.tachiyomi.Constants
  11. import eu.kanade.tachiyomi.R
  12. import eu.kanade.tachiyomi.data.database.DatabaseHelper
  13. import eu.kanade.tachiyomi.data.database.models.Category
  14. import eu.kanade.tachiyomi.data.database.models.Chapter
  15. import eu.kanade.tachiyomi.data.database.models.Manga
  16. import eu.kanade.tachiyomi.data.download.DownloadManager
  17. import eu.kanade.tachiyomi.data.download.DownloadService
  18. import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
  19. import eu.kanade.tachiyomi.data.notification.NotificationReceiver
  20. import eu.kanade.tachiyomi.data.preference.PreferencesHelper
  21. import eu.kanade.tachiyomi.data.preference.getOrDefault
  22. import eu.kanade.tachiyomi.source.SourceManager
  23. import eu.kanade.tachiyomi.source.model.SManga
  24. import eu.kanade.tachiyomi.source.online.HttpSource
  25. import eu.kanade.tachiyomi.ui.main.MainActivity
  26. import eu.kanade.tachiyomi.util.*
  27. import rx.Observable
  28. import rx.Subscription
  29. import rx.schedulers.Schedulers
  30. import uy.kohesive.injekt.injectLazy
  31. import java.util.*
  32. import java.util.concurrent.atomic.AtomicInteger
  33. /**
  34. * This class will take care of updating the chapters of the manga from the library. It can be
  35. * started calling the [start] method. If it's already running, it won't do anything.
  36. * While the library is updating, a [PowerManager.WakeLock] will be held until the update is
  37. * completed, preventing the device from going to sleep mode. A notification will display the
  38. * progress of the update, and if case of an unexpected error, this service will be silently
  39. * destroyed.
  40. */
  41. class LibraryUpdateService : Service() {
  42. /**
  43. * Database helper.
  44. */
  45. val db: DatabaseHelper by injectLazy()
  46. /**
  47. * Source manager.
  48. */
  49. val sourceManager: SourceManager by injectLazy()
  50. /**
  51. * Preferences.
  52. */
  53. val preferences: PreferencesHelper by injectLazy()
  54. val downloadManager: DownloadManager by injectLazy()
  55. /**
  56. * Wake lock that will be held until the service is destroyed.
  57. */
  58. private lateinit var wakeLock: PowerManager.WakeLock
  59. /**
  60. * Subscription where the update is done.
  61. */
  62. private var subscription: Subscription? = null
  63. /**
  64. * Pending intent of action that cancels the library update
  65. */
  66. private val cancelPendingIntent by lazy {NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this)}
  67. /**
  68. * Id of the library update notification.
  69. */
  70. private val notificationId: Int
  71. get() = Constants.NOTIFICATION_LIBRARY_ID
  72. private val notificationBitmap by lazy {
  73. BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
  74. }
  75. companion object {
  76. /**
  77. * Key for category to update.
  78. */
  79. const val UPDATE_CATEGORY = "category"
  80. /**
  81. * Key for updating the details instead of the chapters.
  82. */
  83. const val UPDATE_DETAILS = "details"
  84. /**
  85. * Returns the status of the service.
  86. *
  87. * @param context the application context.
  88. * @return true if the service is running, false otherwise.
  89. */
  90. fun isRunning(context: Context): Boolean {
  91. return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java)
  92. }
  93. /**
  94. * Starts the service. It will be started only if there isn't another instance already
  95. * running.
  96. *
  97. * @param context the application context.
  98. * @param category a specific category to update, or null for global update.
  99. * @param details whether to update the details instead of the list of chapters.
  100. */
  101. fun start(context: Context, category: Category? = null, details: Boolean = false) {
  102. if (!isRunning(context)) {
  103. val intent = Intent(context, LibraryUpdateService::class.java).apply {
  104. putExtra(UPDATE_DETAILS, details)
  105. category?.let { putExtra(UPDATE_CATEGORY, it.id) }
  106. }
  107. context.startService(intent)
  108. }
  109. }
  110. /**
  111. * Stops the service.
  112. *
  113. * @param context the application context.
  114. */
  115. fun stop(context: Context) {
  116. context.stopService(Intent(context, LibraryUpdateService::class.java))
  117. }
  118. }
  119. /**
  120. * Method called when the service is created. It injects dagger dependencies and acquire
  121. * the wake lock.
  122. */
  123. override fun onCreate() {
  124. super.onCreate()
  125. createAndAcquireWakeLock()
  126. }
  127. /**
  128. * Method called when the service is destroyed. It destroys the running subscription, resets
  129. * the alarm and release the wake lock.
  130. */
  131. override fun onDestroy() {
  132. subscription?.unsubscribe()
  133. destroyWakeLock()
  134. super.onDestroy()
  135. }
  136. /**
  137. * This method needs to be implemented, but it's not used/needed.
  138. */
  139. override fun onBind(intent: Intent): IBinder? {
  140. return null
  141. }
  142. /**
  143. * Method called when the service receives an intent.
  144. *
  145. * @param intent the start intent from.
  146. * @param flags the flags of the command.
  147. * @param startId the start id of this command.
  148. * @return the start value of the command.
  149. */
  150. override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
  151. if (intent == null) return Service.START_NOT_STICKY
  152. // Unsubscribe from any previous subscription if needed.
  153. subscription?.unsubscribe()
  154. // Update favorite manga. Destroy service when completed or in case of an error.
  155. subscription = Observable
  156. .defer {
  157. val mangaList = getMangaToUpdate(intent)
  158. // Update either chapter list or manga details.
  159. if (!intent.getBooleanExtra(UPDATE_DETAILS, false))
  160. updateChapterList(mangaList)
  161. else
  162. updateDetails(mangaList)
  163. }
  164. .subscribeOn(Schedulers.io())
  165. .subscribe({
  166. }, {
  167. showNotification(getString(R.string.notification_update_error), "")
  168. stopSelf(startId)
  169. }, {
  170. stopSelf(startId)
  171. })
  172. return Service.START_REDELIVER_INTENT
  173. }
  174. /**
  175. * Returns the list of manga to be updated.
  176. *
  177. * @param intent the update intent.
  178. * @return a list of manga to update
  179. */
  180. fun getMangaToUpdate(intent: Intent): List<Manga> {
  181. val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1)
  182. var listToUpdate = if (categoryId != -1)
  183. db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
  184. else {
  185. val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map { it.toInt() }
  186. if (categoriesToUpdate.isNotEmpty())
  187. db.getLibraryMangas().executeAsBlocking()
  188. .filter { it.category in categoriesToUpdate }
  189. .distinctBy { it.id }
  190. else
  191. db.getFavoriteMangas().executeAsBlocking().distinctBy { it.id }
  192. }
  193. if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
  194. listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
  195. }
  196. return listToUpdate
  197. }
  198. /**
  199. * Method that updates the given list of manga. It's called in a background thread, so it's safe
  200. * to do heavy operations or network calls here.
  201. * For each manga it calls [updateManga] and updates the notification showing the current
  202. * progress.
  203. *
  204. * @param mangaToUpdate the list to update
  205. * @return an observable delivering the progress of each update.
  206. */
  207. fun updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> {
  208. // Initialize the variables holding the progress of the updates.
  209. val count = AtomicInteger(0)
  210. val newUpdates = ArrayList<Manga>()
  211. val failedUpdates = ArrayList<Manga>()
  212. // Emit each manga and update it sequentially.
  213. return Observable.from(mangaToUpdate)
  214. // Notify manga that will update.
  215. .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelPendingIntent) }
  216. // Update the chapters of the manga.
  217. .concatMap { manga ->
  218. updateManga(manga)
  219. // If there's any error, return empty update and continue.
  220. .onErrorReturn {
  221. failedUpdates.add(manga)
  222. Pair(emptyList<Chapter>(), emptyList<Chapter>())
  223. }
  224. // Filter out mangas without new chapters (or failed).
  225. .filter { pair -> pair.first.size > 0 }
  226. .doOnNext {
  227. if (preferences.downloadNew()) {
  228. downloadChapters(manga, it.first)
  229. }
  230. }
  231. // Convert to the manga that contains new chapters.
  232. .map { manga }
  233. }
  234. // Add manga with new chapters to the list.
  235. .doOnNext { manga ->
  236. // Set last updated time
  237. manga.last_update = Date().time
  238. db.updateLastUpdated(manga).executeAsBlocking()
  239. // Add to the list
  240. newUpdates.add(manga)
  241. }
  242. // Notify result of the overall update.
  243. .doOnCompleted {
  244. if (newUpdates.isEmpty()) {
  245. cancelNotification()
  246. } else {
  247. if (preferences.downloadNew()) {
  248. DownloadService.start(this)
  249. }
  250. showResultNotification(newUpdates, failedUpdates)
  251. }
  252. }
  253. }
  254. fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
  255. // we need to get the chapters from the db so we have chapter ids
  256. val mangaChapters = db.getChapters(manga).executeAsBlocking()
  257. val dbChapters = chapters.map {
  258. mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!!
  259. }
  260. downloadManager.downloadChapters(manga, dbChapters)
  261. }
  262. /**
  263. * Updates the chapters for the given manga and adds them to the database.
  264. *
  265. * @param manga the manga to update.
  266. * @return a pair of the inserted and removed chapters.
  267. */
  268. fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
  269. val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
  270. return source.fetchChapterList(manga)
  271. .map { syncChaptersWithSource(db, it, manga, source) }
  272. }
  273. /**
  274. * Method that updates the details of the given list of manga. It's called in a background
  275. * thread, so it's safe to do heavy operations or network calls here.
  276. * For each manga it calls [updateManga] and updates the notification showing the current
  277. * progress.
  278. *
  279. * @param mangaToUpdate the list to update
  280. * @return an observable delivering the progress of each update.
  281. */
  282. fun updateDetails(mangaToUpdate: List<Manga>): Observable<Manga> {
  283. // Initialize the variables holding the progress of the updates.
  284. val count = AtomicInteger(0)
  285. // Emit each manga and update it sequentially.
  286. return Observable.from(mangaToUpdate)
  287. // Notify manga that will update.
  288. .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelPendingIntent) }
  289. // Update the details of the manga.
  290. .concatMap { manga ->
  291. val source = sourceManager.get(manga.source) as? HttpSource
  292. ?: return@concatMap Observable.empty<Manga>()
  293. source.fetchMangaDetails(manga)
  294. .map { networkManga ->
  295. manga.copyFrom(networkManga)
  296. db.insertManga(manga).executeAsBlocking()
  297. manga
  298. }
  299. .onErrorReturn { manga }
  300. }
  301. .doOnCompleted {
  302. cancelNotification()
  303. }
  304. }
  305. /**
  306. * Returns the text that will be displayed in the notification when there are new chapters.
  307. *
  308. * @param updates a list of manga that contains new chapters.
  309. * @param failedUpdates a list of manga that failed to update.
  310. * @return the body of the notification to display.
  311. */
  312. private fun getUpdatedMangasBody(updates: List<Manga>, failedUpdates: List<Manga>): String {
  313. return buildString {
  314. if (updates.isEmpty()) {
  315. append(getString(R.string.notification_no_new_chapters))
  316. append("\n")
  317. } else {
  318. append(getString(R.string.notification_new_chapters))
  319. for (manga in updates) {
  320. append("\n")
  321. append(manga.title.chop(45))
  322. }
  323. }
  324. if (!failedUpdates.isEmpty()) {
  325. append("\n\n")
  326. append(getString(R.string.notification_manga_update_failed))
  327. for (manga in failedUpdates) {
  328. append("\n")
  329. append(manga.title.chop(45))
  330. }
  331. }
  332. }
  333. }
  334. /**
  335. * Creates and acquires a wake lock until the library is updated.
  336. */
  337. private fun createAndAcquireWakeLock() {
  338. wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
  339. PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
  340. wakeLock.acquire()
  341. }
  342. /**
  343. * Releases the wake lock if it's held.
  344. */
  345. private fun destroyWakeLock() {
  346. if (wakeLock.isHeld) {
  347. wakeLock.release()
  348. }
  349. }
  350. /**
  351. * Shows the notification with the given title and body.
  352. *
  353. * @param title the title of the notification.
  354. * @param body the body of the notification.
  355. */
  356. private fun showNotification(title: String, body: String) {
  357. notificationManager.notify(notificationId, notification {
  358. setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
  359. setLargeIcon(notificationBitmap)
  360. setContentTitle(title)
  361. setContentText(body)
  362. })
  363. }
  364. /**
  365. * Shows the notification containing the currently updating manga and the progress.
  366. *
  367. * @param manga the manga that's being updated.
  368. * @param current the current progress.
  369. * @param total the total progress.
  370. */
  371. private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) {
  372. notificationManager.notify(notificationId, notification {
  373. setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
  374. setLargeIcon(notificationBitmap)
  375. setContentTitle(manga.title)
  376. setProgress(total, current, false)
  377. setOngoing(true)
  378. addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
  379. })
  380. }
  381. /**
  382. * Shows the notification containing the result of the update done by the service.
  383. *
  384. * @param updates a list of manga with new updates.
  385. * @param failed a list of manga that failed to update.
  386. */
  387. private fun showResultNotification(updates: List<Manga>, failed: List<Manga>) {
  388. val title = getString(R.string.notification_update_completed)
  389. val body = getUpdatedMangasBody(updates, failed)
  390. notificationManager.notify(notificationId, notification {
  391. setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
  392. setLargeIcon(notificationBitmap)
  393. setContentTitle(title)
  394. setStyle(NotificationCompat.BigTextStyle().bigText(body))
  395. setContentIntent(notificationIntent)
  396. setAutoCancel(true)
  397. })
  398. }
  399. /**
  400. * Cancels the notification.
  401. */
  402. private fun cancelNotification() {
  403. notificationManager.cancel(notificationId)
  404. }
  405. /**
  406. * Property that returns an intent to open the main activity.
  407. */
  408. private val notificationIntent: PendingIntent
  409. get() {
  410. val intent = Intent(this, MainActivity::class.java)
  411. intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
  412. return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
  413. }
  414. }