add profile process service

This commit is contained in:
Kr328 committed 2020-02-09 00:53:40 +08:00
1 parent 23027b8813
commit f76bed2f8b
6 files changed
+146 -114

No files matched your search

@@ -1,6 +1,9 @@
package com.github.kr328.clash.service
import android.app.*
import android.app.AlarmManager
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@@ -8,7 +11,6 @@ import android.content.ServiceConnection
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.RemoteException
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.github.kr328.clash.service.data.ClashDatabase
@@ -20,92 +22,72 @@ import com.github.kr328.clash.service.util.RandomUtils
import com.github.kr328.clash.service.util.componentName
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.service.util.timeout
import kotlinx.coroutines.*
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
class ProfileBackgroundService : BaseService() {
companion object {
private const val SERVICE_STATUS_CHANNEL = "profile_service_status"
private const val SERVICE_RESULT_CHANNEL = "profile_service_result"
private const val SERVICE_NOTIFICATION_ID = 10000
private const val SERVICE_NOTIFICATION_ID_BASE = 10000
}
private val database by lazy { ClashDatabase.getInstance(this).openClashProfileDao() }
private val channel = Channel<ProfileRequest>(2)
private val queue = mutableListOf<Deferred<ProfileRequest>>()
private val queue = mutableListOf<CompletableDeferred<ProfileRequest>>()
private val profiles: ClashProfileDao by lazy {
ClashDatabase.getInstance(this).openClashProfileDao()
}
private val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {}
override fun onServiceDisconnected(name: ComponentName?) {
channel.close()
stopSelf()
}
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
val service = IProfileService.Stub.asInterface(binder) ?: return stopSelf()
launch {
while (isActive) {
val timeout = timeout(1000 * 60L)
select<Unit> {
channel.onReceive {
val deferred = CompletableDeferred<ProfileRequest>()
it.withCallback(object : IStreamCallback.Stub() {
override fun complete() {
deferred.complete(it)
}
override fun completeExceptionally(reason: String?) {
deferred.completeExceptionally(RemoteException(reason))
}
override fun send(data: ParcelableContainer?) {}
})
service.enqueueRequest(it)
queue.add(deferred)
}
if (queue.isNotEmpty()) {
for (task in queue) {
task.onAwait {
queue.remove(task)
}
}
} else {
timeout.onJoin {
stopSelf()
cancel()
}
}
}
timeout.cancel()
}
}
startProfileProcessor(service)
}
}
override fun onCreate() {
super.onCreate()
createNotificationChannels()
startForeground()
bindService(ProfileProcessService::class.intent, connection, Context.BIND_AUTO_CREATE)
}
override fun onDestroy() {
super.onDestroy()
unbindService(connection)
super.onDestroy()
stopForeground(true)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val request =
intent?.getParcelableExtra<ProfileRequest>(Intents.INTENT_EXTRA_PROFILE_REQUEST)
?: return START_NOT_STICKY
channel.offer(request)
when (intent?.action) {
Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST -> {
val request =
intent.getParcelableExtra<ProfileRequest>(Intents.INTENT_EXTRA_PROFILE_REQUEST)
?: return START_NOT_STICKY
if (request.id != 0L)
channel.offer(request)
}
Intents.INTENT_ACTION_PROFILE_SETUP -> {
resetProfileUpdateAlarm()
}
}
return START_NOT_STICKY
}
@@ -114,6 +96,54 @@ class ProfileBackgroundService : BaseService() {
return Binder()
}
private fun startProfileProcessor(service: IProfileService) {
launch {
while (isActive) {
val timeout = timeout(1000 * 60L)
select<Unit> {
channel.onReceive {
val deferred = CompletableDeferred<ProfileRequest>()
it.withCallback(object : IStreamCallback.Stub() {
override fun complete() {
deferred.complete(it)
updateUpdateComplete(it.id)
}
override fun completeExceptionally(reason: String?) {
deferred.complete(it)
updateUpdateFailure(it.id)
}
override fun send(data: ParcelableContainer?) {
updateUpdating(it.id)
}
})
service.enqueueRequest(it)
queue.add(deferred)
}
if (queue.isNotEmpty()) {
for (task in queue) {
task.onAwait {
queue.remove(task)
}
}
} else {
timeout.onJoin {
stopSelf()
cancel()
}
}
}
timeout.cancel()
}
}
}
private fun resetProfileUpdateAlarm() {
for (entity in profiles.queryProfiles()) {
if (entity.updateInterval <= 0) continue
@@ -150,49 +180,74 @@ class ProfileBackgroundService : BaseService() {
),
NotificationChannel(
SERVICE_RESULT_CHANNEL,
getText(R.string.profile_service_result),
getText(R.string.profile_status_channel),
NotificationManager.IMPORTANCE_DEFAULT
)
)
)
}
private fun createServiceNotification(content: CharSequence): Notification {
return NotificationCompat.Builder(this, SERVICE_STATUS_CHANNEL)
private fun startForeground() {
val notification = NotificationCompat.Builder(this, SERVICE_STATUS_CHANNEL)
.setContentTitle(getText(R.string.profile_service_status_title))
.setContentText(content)
.setSmallIcon(R.drawable.ic_updating)
.setOnlyAlertOnce(true)
.setProgress(Int.MAX_VALUE, 0, true)
.build()
startForeground(SERVICE_NOTIFICATION_ID_BASE, notification)
}
private fun createResultNotification(success: Boolean, content: CharSequence): Notification {
return NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL)
.setContentTitle(getText(R.string.profile_process_result))
.setContentText(content)
.setSmallIcon(if (success) R.drawable.ic_update_completed else R.drawable.ic_update_failure)
private fun updateUpdating(id: Long) {
val notificationId = (id % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt()
val entity = database.queryProfileById(id) ?: return
val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL)
.setContentTitle(getText(R.string.profile_status_title))
.setContentText(getString(R.string.profile_status_updating, entity.name))
.setSmallIcon(R.drawable.ic_update_normal)
.setOngoing(true)
.build()
}
private fun updateNotificationWaiting() {
startForeground(
SERVICE_NOTIFICATION_ID,
createServiceNotification(getText(R.string.profile_service_status_waiting))
)
}
private fun updateNotificationUpdating(profileName: String) {
createServiceNotification(
getString(
R.string.profile_service_status_updating,
profileName
)
)
}
private fun notifyResultNotification(success: Boolean, content: CharSequence) {
NotificationManagerCompat.from(this)
.notify(RandomUtils.nextInt(), createResultNotification(success, content))
.notify(SERVICE_NOTIFICATION_ID_BASE + notificationId, notification)
}
private fun updateUpdateComplete(id: Long) {
val notificationId = (id % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt()
val entity = database.queryProfileById(id)
if (entity == null) {
NotificationManagerCompat.from(this).cancel(notificationId)
return
}
val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL)
.setContentTitle(getText(R.string.profile_status_title))
.setContentText(getString(R.string.profile_status_update_completed, entity.name))
.setSmallIcon(R.drawable.ic_update_normal)
.build()
NotificationManagerCompat.from(this)
.notify(SERVICE_NOTIFICATION_ID_BASE + notificationId, notification)
}
private fun updateUpdateFailure(id: Long) {
val notificationId = (id % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt()
val entity = database.queryProfileById(id)
if (entity == null) {
NotificationManagerCompat.from(this).cancel(notificationId)
return
}
val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL)
.setContentTitle(getText(R.string.profile_status_title))
.setContentText(getString(R.string.profile_status_update_failure, entity.name))
.setSmallIcon(R.drawable.ic_update_normal)
.setOngoing(true)
.build()
NotificationManagerCompat.from(this)
.notify(SERVICE_NOTIFICATION_ID_BASE + notificationId, notification)
}
}
@@ -59,6 +59,8 @@ class ProfileProcessService : BaseService() {
private suspend fun handleRequest(request: ProfileRequest) {
try {
request.callback?.send(null)
when (request.action) {
ProfileRequest.Action.UPDATE_OR_CREATE ->
handleUpdateOrCreate(request)
@@ -74,24 +74,6 @@ class ProfileRequest private constructor(private val bundle: Bundle) : Parcelabl
}
}
override fun equals(other: Any?): Boolean {
if (other !is ProfileRequest)
return false
for (key in bundle.keySet()) {
if (bundle.get(key) != other.bundle.get(key))
return false
}
return true
}
override fun hashCode(): Int {
return bundle.keySet()
.joinToString { it + bundle.get(it)?.hashCode() }
.hashCode()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeBundle(bundle)
}
@@ -1,6 +0,0 @@
package com.github.kr328.clash.service.util
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
val DefaultThreadPool: ExecutorService = Executors.newCachedThreadPool()
+7 -8
View File
@@ -14,14 +14,13 @@
<string name="clash_notification_content">"%1$s↑\t%2$s↓"</string>
<string name="profile_service_status_channel">Profile Service Status</string>
<string name="profile_service_status_title">Processing Profiles</string>
<string name="profile_service_status_waiting">Waiting Request</string>
<string name="profile_service_status_updating">Updating %s</string>
<string name="profile_service_result">Profile Service Result</string>
<string name="profile_process_result">Process Result</string>
<string name="profile_update_completed">Update %s Completed</string>
<string name="profile_update_failure">Update %s Failure</string>
<string name="profile_deleted">Profile %s deleted</string>
<string name="profile_service_status_title">Profile Updater Running</string>
<string name="profile_status_channel">Profile Processing Status</string>
<string name="profile_status_title">Processing Profile</string>
<string name="profile_status_updating">Updating %s</string>
<string name="profile_status_update_failure">Update %s Failure</string>
<string name="profile_status_update_completed">Update %s Completed</string>
<string name="clash_service_label">Clash Core Service</string>
<string name="tun_service_label">Tun Device Service</string>