Horis 2 лет назад
Родитель
Сommit
76f118def8

+ 9 - 4
app/src/main/java/io/legado/app/base/BaseService.kt

@@ -31,7 +31,12 @@ abstract class BaseService : LifecycleService() {
         super.onCreate()
         LifecycleHelp.onServiceCreate(this)
         checkNotificationPermission()
-        upNotification()
+    }
+
+    @CallSuper
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        startForegroundNotification()
+        return super.onStartCommand(intent, flags, startId)
     }
 
     @CallSuper
@@ -52,9 +57,9 @@ abstract class BaseService : LifecycleService() {
     }
 
     /**
-     * 更新通知
+     * 开启前台服务并发送通知
      */
-    open fun upNotification() {
+    open fun startForegroundNotification() {
 
     }
 
@@ -67,7 +72,7 @@ abstract class BaseService : LifecycleService() {
             .rationale(R.string.notification_permission_rationale)
             .onGranted {
                 if (lifecycleScope.isActive) {
-                    upNotification()
+                    startForegroundNotification()
                 }
             }
             .request()

+ 8 - 2
app/src/main/java/io/legado/app/data/entities/BookChapter.kt

@@ -14,7 +14,13 @@ import io.legado.app.help.RuleBigDataHelp
 import io.legado.app.help.config.AppConfig
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.RuleDataInterface
-import io.legado.app.utils.*
+import io.legado.app.utils.ChineseUtils
+import io.legado.app.utils.GSON
+import io.legado.app.utils.MD5Utils
+import io.legado.app.utils.NetworkUtils
+import io.legado.app.utils.fromJsonObject
+import io.legado.app.utils.replace
+import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.CancellationException
 import kotlinx.parcelize.IgnoredOnParcel
 import kotlinx.parcelize.Parcelize
@@ -110,7 +116,7 @@ data class BookChapter(
                     try {
                         val mDisplayTitle = if (item.isRegex) {
                             displayTitle.replace(
-                                item.pattern.toRegex(),
+                                item.regex,
                                 item.replacement,
                                 item.getValidTimeoutMillisecond()
                             )

+ 8 - 0
app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt

@@ -4,11 +4,13 @@ import android.os.Parcelable
 import android.text.TextUtils
 import androidx.room.ColumnInfo
 import androidx.room.Entity
+import androidx.room.Ignore
 import androidx.room.Index
 import androidx.room.PrimaryKey
 import io.legado.app.R
 import io.legado.app.constant.AppLog
 import io.legado.app.exception.NoStackTraceException
+import kotlinx.parcelize.IgnoredOnParcel
 import kotlinx.parcelize.Parcelize
 import splitties.init.appCtx
 import java.util.regex.Pattern
@@ -68,6 +70,12 @@ data class ReplaceRule(
         return id.hashCode()
     }
 
+    @delegate:Ignore
+    @IgnoredOnParcel
+    val regex: Regex by lazy {
+        pattern.toRegex()
+    }
+
     fun getDisplayNameGroup(): String {
         return if (group.isNullOrBlank()) {
             name

+ 1 - 1
app/src/main/java/io/legado/app/help/book/ContentProcessor.kt

@@ -152,7 +152,7 @@ class ContentProcessor private constructor(
                     try {
                         val tmp = if (item.isRegex) {
                             mContent.replace(
-                                item.pattern.toRegex(),
+                                item.regex,
                                 item.replacement,
                                 item.getValidTimeoutMillisecond()
                             )

+ 0 - 48
app/src/main/java/io/legado/app/help/coroutine/OrderCoroutine.kt

@@ -1,48 +0,0 @@
-package io.legado.app.help.coroutine
-
-import kotlinx.coroutines.*
-import kotlinx.coroutines.Dispatchers.IO
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.PriorityBlockingQueue
-import java.util.concurrent.atomic.AtomicInteger
-
-class OrderCoroutine<T>(val threadCount: Int) {
-    private val taskList = ArrayList<suspend CoroutineScope.() -> T>()
-    private val taskResultMap = ConcurrentHashMap<Int, T>()
-    private val finishTaskIndex = PriorityBlockingQueue<Int>()
-
-    private suspend fun start() = coroutineScope {
-        val taskIndex = AtomicInteger(0)
-        val tasks = taskList.toList()
-        for (i in 1..threadCount) {
-            launch {
-                while (true) {
-                    ensureActive()
-                    val curIndex = taskIndex.getAndIncrement()
-                    val task = tasks.getOrNull(curIndex) ?: return@launch
-                    taskResultMap[curIndex] = task.invoke(this)
-                    finishTaskIndex.add(curIndex)
-                }
-            }
-        }
-    }
-
-    fun submit(block: suspend CoroutineScope.() -> T) {
-        taskList.add(block)
-    }
-
-    suspend fun collect(block: (index: Int, result: T) -> Unit) = withContext(IO) {
-        var index = 0
-        val taskSize = taskList.size
-        launch { start() }
-        while (index < taskSize) {
-            ensureActive()
-            if (finishTaskIndex.peek() == index) {
-                finishTaskIndex.poll()
-                block.invoke(index, taskResultMap.remove(index)!!)
-                index++
-            }
-        }
-    }
-
-}

+ 11 - 0
app/src/main/java/io/legado/app/model/CacheBook.kt

@@ -83,6 +83,17 @@ object CacheBook {
         }
     }
 
+    fun clear() {
+        successDownloadSet.clear()
+        errorDownloadMap.clear()
+    }
+
+    fun close() {
+        cacheBookMap.forEach { it.value.stop() }
+        cacheBookMap.clear()
+        clear()
+    }
+
     val downloadSummary: String
         get() {
             return "正在下载:${onDownloadCount}|等待中:${waitCount}|失败:${errorDownloadMap.count()}|成功:${successDownloadSet.size}"

+ 75 - 63
app/src/main/java/io/legado/app/service/AudioPlayService.kt

@@ -55,6 +55,7 @@ import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
 import splitties.init.appCtx
 import splitties.systemservices.audioManager
+import splitties.systemservices.notificationManager
 import splitties.systemservices.powerManager
 import splitties.systemservices.wifiManager
 
@@ -135,7 +136,7 @@ class AudioPlayService : BaseService(),
                 .get()
         }.onSuccess {
             cover = it
-            upNotification()
+            upAudioPlayNotification()
         }
     }
 
@@ -191,7 +192,7 @@ class AudioPlayService : BaseService(),
             wakeLock.acquire()
             wifiLock?.acquire()
         }
-        upNotification()
+        upAudioPlayNotification()
         if (!requestFocus()) {
             return
         }
@@ -235,7 +236,7 @@ class AudioPlayService : BaseService(),
             upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED)
             AudioPlay.status = Status.PAUSE
             postEvent(EventBus.AUDIO_STATE, Status.PAUSE)
-            upNotification()
+            upAudioPlayNotification()
         } catch (e: Exception) {
             e.printOnDebug()
         }
@@ -263,7 +264,7 @@ class AudioPlayService : BaseService(),
             upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
             AudioPlay.status = Status.PLAY
             postEvent(EventBus.AUDIO_STATE, Status.PLAY)
-            upNotification()
+            upAudioPlayNotification()
         } catch (e: Exception) {
             e.printOnDebug()
             stopSelf()
@@ -335,7 +336,7 @@ class AudioPlayService : BaseService(),
                 AudioPlay.next(this)
             }
         }
-        upNotification()
+        upAudioPlayNotification()
     }
 
     /**
@@ -370,7 +371,7 @@ class AudioPlayService : BaseService(),
      */
     private fun doDs() {
         postEvent(EventBus.AUDIO_DS, timeMinute)
-        upNotification()
+        upAudioPlayNotification()
         dsJob?.cancel()
         dsJob = lifecycleScope.launch {
             while (isActive) {
@@ -384,7 +385,7 @@ class AudioPlayService : BaseService(),
                     }
                 }
                 postEvent(EventBus.AUDIO_DS, timeMinute)
-                upNotification()
+                upAudioPlayNotification()
             }
         }
     }
@@ -583,67 +584,78 @@ class AudioPlayService : BaseService(),
         }
     }
 
+    private fun  createNotification(): NotificationCompat.Builder {
+        var nTitle: String = when {
+            pause -> getString(R.string.audio_pause)
+            timeMinute in 1..60 -> getString(
+                R.string.playing_timer,
+                timeMinute
+            )
 
-    /**
-     * 更新通知
-     */
-    override fun upNotification() {
-        execute {
-            var nTitle: String = when {
-                pause -> getString(R.string.audio_pause)
-                timeMinute in 1..60 -> getString(
-                    R.string.playing_timer,
-                    timeMinute
-                )
-
-                else -> getString(R.string.audio_play_t)
-            }
-            nTitle += ": ${AudioPlay.book?.name}"
-            var nSubtitle = AudioPlay.durChapter?.title
-            if (nSubtitle.isNullOrEmpty()) {
-                nSubtitle = getString(R.string.audio_play_s)
-            }
-            val builder = NotificationCompat
-                .Builder(this@AudioPlayService, AppConst.channelIdReadAloud)
-                .setSmallIcon(R.drawable.ic_volume_up)
-                .setSubText(getString(R.string.audio))
-                .setOngoing(true)
-                .setContentTitle(nTitle)
-                .setContentText(nSubtitle)
-                .setContentIntent(
-                    activityPendingIntent<AudioPlayActivity>("activity")
-                )
-            builder.setLargeIcon(cover)
-            if (pause) {
-                builder.addAction(
-                    R.drawable.ic_play_24dp,
-                    getString(R.string.resume),
-                    servicePendingIntent<AudioPlayService>(IntentAction.resume)
-                )
-            } else {
-                builder.addAction(
-                    R.drawable.ic_pause_24dp,
-                    getString(R.string.pause),
-                    servicePendingIntent<AudioPlayService>(IntentAction.pause)
-                )
-            }
-            builder.addAction(
-                R.drawable.ic_stop_black_24dp,
-                getString(R.string.stop),
-                servicePendingIntent<AudioPlayService>(IntentAction.stop)
+            else -> getString(R.string.audio_play_t)
+        }
+        nTitle += ": ${AudioPlay.book?.name}"
+        var nSubtitle = AudioPlay.durChapter?.title
+        if (nSubtitle.isNullOrEmpty()) {
+            nSubtitle = getString(R.string.audio_play_s)
+        }
+        val builder = NotificationCompat
+            .Builder(this@AudioPlayService, AppConst.channelIdReadAloud)
+            .setSmallIcon(R.drawable.ic_volume_up)
+            .setSubText(getString(R.string.audio))
+            .setOngoing(true)
+            .setContentTitle(nTitle)
+            .setContentText(nSubtitle)
+            .setContentIntent(
+                activityPendingIntent<AudioPlayActivity>("activity")
             )
+        builder.setLargeIcon(cover)
+        if (pause) {
             builder.addAction(
-                R.drawable.ic_time_add_24dp,
-                getString(R.string.set_timer),
-                servicePendingIntent<AudioPlayService>(IntentAction.addTimer)
+                R.drawable.ic_play_24dp,
+                getString(R.string.resume),
+                servicePendingIntent<AudioPlayService>(IntentAction.resume)
             )
-            builder.setStyle(
-                androidx.media.app.NotificationCompat.MediaStyle()
-                    .setShowActionsInCompactView(0, 1, 2)
-                    .setMediaSession(mediaSessionCompat?.sessionToken)
+        } else {
+            builder.addAction(
+                R.drawable.ic_pause_24dp,
+                getString(R.string.pause),
+                servicePendingIntent<AudioPlayService>(IntentAction.pause)
             )
-            builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
-            builder
+        }
+        builder.addAction(
+            R.drawable.ic_stop_black_24dp,
+            getString(R.string.stop),
+            servicePendingIntent<AudioPlayService>(IntentAction.stop)
+        )
+        builder.addAction(
+            R.drawable.ic_time_add_24dp,
+            getString(R.string.set_timer),
+            servicePendingIntent<AudioPlayService>(IntentAction.addTimer)
+        )
+        builder.setStyle(
+            androidx.media.app.NotificationCompat.MediaStyle()
+                .setShowActionsInCompactView(0, 1, 2)
+                .setMediaSession(mediaSessionCompat?.sessionToken)
+        )
+        builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+        return builder
+    }
+
+    private fun  upAudioPlayNotification() {
+        execute {
+            createNotification()
+        }.onSuccess {
+            notificationManager.notify(NotificationId.AudioPlayService, it.build())
+        }
+    }
+
+    /**
+     * 更新通知
+     */
+    override fun startForegroundNotification() {
+        execute {
+            createNotification()
         }.onSuccess {
             startForeground(NotificationId.AudioPlayService, it.build())
         }

+ 18 - 5
app/src/main/java/io/legado/app/service/BaseReadAloudService.kt

@@ -21,7 +21,13 @@ import androidx.media.AudioFocusRequestCompat
 import androidx.media.AudioManagerCompat
 import io.legado.app.R
 import io.legado.app.base.BaseService
-import io.legado.app.constant.*
+import io.legado.app.constant.AppConst
+import io.legado.app.constant.AppLog
+import io.legado.app.constant.EventBus
+import io.legado.app.constant.IntentAction
+import io.legado.app.constant.NotificationId
+import io.legado.app.constant.PreferKey
+import io.legado.app.constant.Status
 import io.legado.app.help.MediaHelp
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.glide.ImageLoader
@@ -30,10 +36,18 @@ import io.legado.app.model.ReadBook
 import io.legado.app.receiver.MediaButtonReceiver
 import io.legado.app.ui.book.read.ReadBookActivity
 import io.legado.app.ui.book.read.page.entities.TextChapter
-import io.legado.app.utils.*
-import kotlinx.coroutines.*
+import io.legado.app.utils.activityPendingIntent
+import io.legado.app.utils.broadcastPendingIntent
+import io.legado.app.utils.getPrefBoolean
+import io.legado.app.utils.observeEvent
+import io.legado.app.utils.postEvent
+import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.Main
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
 import splitties.init.appCtx
 import splitties.systemservices.audioManager
 import splitties.systemservices.notificationManager
@@ -158,7 +172,6 @@ abstract class BaseReadAloudService : BaseService(),
     }
 
     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
-        upNotification()
         when (intent?.action) {
             IntentAction.play -> newReadAloud(
                 intent.getBooleanExtra("play", true),
@@ -516,7 +529,7 @@ abstract class BaseReadAloudService : BaseService(),
     /**
      * 更新通知
      */
-    override fun upNotification() {
+    override fun startForegroundNotification() {
         execute {
             createNotification()
         }.onSuccess {

+ 12 - 9
app/src/main/java/io/legado/app/service/CacheBookService.kt

@@ -24,6 +24,7 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
 import splitties.init.appCtx
+import splitties.systemservices.notificationManager
 import java.util.concurrent.Executors
 import kotlin.math.min
 
@@ -59,13 +60,12 @@ class CacheBookService : BaseService() {
     override fun onCreate() {
         super.onCreate()
         isRun = true
-        CacheBook.successDownloadSet.clear()
-        CacheBook.errorDownloadMap.clear()
+        CacheBook.clear()
         lifecycleScope.launch {
             while (isActive) {
                 delay(1000)
                 notificationContent = CacheBook.downloadSummary
-                upNotification()
+                upCacheBookNotification()
                 postEvent(EventBus.UP_DOWNLOAD, "")
             }
         }
@@ -90,10 +90,7 @@ class CacheBookService : BaseService() {
     override fun onDestroy() {
         isRun = false
         cachePool.close()
-        CacheBook.cacheBookMap.forEach { it.value.stop() }
-        CacheBook.cacheBookMap.clear()
-        CacheBook.successDownloadSet.clear()
-        CacheBook.errorDownloadMap.clear()
+        CacheBook.close()
         super.onDestroy()
         postEvent(EventBus.UP_DOWNLOAD, "")
     }
@@ -134,7 +131,7 @@ class CacheBookService : BaseService() {
             }
             cacheBook.addDownload(start, end2)
             notificationContent = CacheBook.downloadSummary
-            upNotification()
+            upCacheBookNotification()
             if (downloadJob == null) {
                 download()
             }
@@ -175,10 +172,16 @@ class CacheBookService : BaseService() {
         }
     }
 
+    private fun upCacheBookNotification() {
+        notificationBuilder.setContentText(notificationContent)
+        val notification = notificationBuilder.build()
+        notificationManager.notify(NotificationId.CacheBookService, notification)
+    }
+
     /**
      * 更新通知
      */
-    override fun upNotification() {
+    override fun startForegroundNotification() {
         notificationBuilder.setContentText(notificationContent)
         val notification = notificationBuilder.build()
         startForeground(NotificationId.CacheBookService, notification)

+ 4 - 4
app/src/main/java/io/legado/app/service/CheckSourceService.kt

@@ -72,7 +72,7 @@ class CheckSourceService : BaseService() {
                 check(it)
             }
 
-            IntentAction.resume -> upNotification()
+            IntentAction.resume -> startForegroundNotification()
             IntentAction.stop -> stopSelf()
         }
         return super.onStartCommand(intent, flags, startId)
@@ -96,7 +96,7 @@ class CheckSourceService : BaseService() {
         processIndex = 0
         threadCount = min(allIds.size, threadCount)
         notificationMsg = getString(R.string.progress_show, "", 0, allIds.size)
-        upNotification()
+        startForegroundNotification()
         for (i in 0 until threadCount) {
             check()
         }
@@ -252,7 +252,7 @@ class CheckSourceService : BaseService() {
             checkedIds.add(sourceUrl)
             notificationMsg =
                 getString(R.string.progress_show, sourceName, checkedIds.size, allIds.size)
-            upNotification()
+            startForegroundNotification()
             if (processIndex > allIds.size + threadCount - 1) {
                 stopSelf()
             }
@@ -262,7 +262,7 @@ class CheckSourceService : BaseService() {
     /**
      * 更新通知
      */
-    override fun upNotification() {
+    override fun startForegroundNotification() {
         notificationBuilder.setContentText(notificationMsg)
         notificationBuilder.setProgress(allIds.size, checkedIds.size, false)
         postEvent(EventBus.CHECK_SOURCE, notificationMsg)

+ 1 - 1
app/src/main/java/io/legado/app/service/DownloadService.kt

@@ -223,7 +223,7 @@ class DownloadService : BaseService() {
         }
     }
 
-    override fun upNotification() {
+    override fun startForegroundNotification() {
         val notification = NotificationCompat.Builder(this, AppConst.channelIdDownload)
             .setSmallIcon(R.drawable.ic_download)
             .setSubText(getString(R.string.action_download))

+ 25 - 10
app/src/main/java/io/legado/app/service/ExportBookService.kt

@@ -50,12 +50,19 @@ import io.legado.app.utils.readText
 import io.legado.app.utils.servicePendingIntent
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.writeBytes
+import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers.Default
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.async
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.withIndex
 import kotlinx.coroutines.launch
 import me.ag2s.epublib.domain.Author
 import me.ag2s.epublib.domain.Date
@@ -133,7 +140,7 @@ class ExportBookService : BaseService() {
     }
 
     @SuppressLint("MissingPermission")
-    override fun upNotification() {
+    override fun startForegroundNotification() {
         val notification = NotificationCompat.Builder(this, AppConst.channelIdDownload)
             .setSmallIcon(R.drawable.ic_export)
             .setSubText(getString(R.string.export_book))
@@ -322,15 +329,23 @@ class ExportBookService : BaseService() {
         }"
         append(qy, null)
         if (AppConfig.parallelExportBook) {
-            val oc =
-                OrderCoroutine<Pair<String, ArrayList<SrcData>?>>(AppConfig.threadCount)
-            appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
-                oc.submit { getExportData(book, chapter, contentProcessor, useReplace) }
-            }
-            oc.collect { index, result ->
-                postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
-                exportProgress[book.bookUrl] = index
-                append.invoke(result.first, result.second)
+            coroutineScope {
+                flow {
+                    appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
+                        val task = async(Default, start = CoroutineStart.LAZY) {
+                            getExportData(book, chapter, contentProcessor, useReplace)
+                        }
+                        emit(task)
+                    }
+                }.onEach { it.start() }
+                    .buffer(AppConfig.threadCount)
+                    .map { it.await() }
+                    .withIndex()
+                    .collect { (index, result) ->
+                        postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
+                        exportProgress[book.bookUrl] = index
+                        append.invoke(result.first, result.second)
+                    }
             }
         } else {
             appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->

+ 4 - 4
app/src/main/java/io/legado/app/service/WebService.kt

@@ -80,11 +80,11 @@ class WebService : BaseService() {
             if (address == null) {
                 hostAddress = getString(R.string.network_connection_unavailable)
                 notificationContent = hostAddress
-                upNotification()
+                startForegroundNotification()
             } else {
                 hostAddress = getString(R.string.http_ip, address.hostAddress, getPort())
                 notificationContent = hostAddress
-                upNotification()
+                startForegroundNotification()
             }
             postEvent(EventBus.WEB_SERVICE, hostAddress)
         }
@@ -142,7 +142,7 @@ class WebService : BaseService() {
                 isRun = true
                 postEvent(EventBus.WEB_SERVICE, hostAddress)
                 notificationContent = hostAddress
-                upNotification()
+                startForegroundNotification()
             } catch (e: IOException) {
                 toastOnUi(e.localizedMessage ?: "")
                 e.printOnDebug()
@@ -165,7 +165,7 @@ class WebService : BaseService() {
     /**
      * 更新通知
      */
-    override fun upNotification() {
+    override fun startForegroundNotification() {
         val builder = NotificationCompat.Builder(this, AppConst.channelIdWeb)
             .setSmallIcon(R.drawable.ic_web_service_noti)
             .setOngoing(true)