This commit is contained in:
Horis 2024-01-27 20:25:23 +08:00
parent c9d07d08c4
commit 3882904f1d
13 changed files with 176 additions and 151 deletions

View File

@ -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()

View File

@ -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()
)

View File

@ -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

View File

@ -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()
)

View File

@ -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++
}
}
}
}

View File

@ -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}"

View File

@ -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
)
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)
)
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 upNotification() {
override fun startForegroundNotification() {
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)
)
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)
builder
createNotification()
}.onSuccess {
startForeground(NotificationId.AudioPlayService, it.build())
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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 ->

View File

@ -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)