service refactored

This commit is contained in:
Kr328
2020-04-13 10:46:02 +08:00
parent 8c9b7e4a01
commit daac6e1e5c
55 changed files with 842 additions and 820 deletions

View File

@@ -2,7 +2,7 @@ package com.github.kr328.clash
import android.app.Application
import android.content.Context
import com.github.kr328.clash.core.Global
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.dump.LogcatDumper
import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.remote.Remote

View File

@@ -11,7 +11,7 @@ import androidx.appcompat.app.AlertDialog
import com.github.kr328.clash.core.utils.Log
import com.github.kr328.clash.design.common.TextInput
import com.github.kr328.clash.remote.withProfile
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.data.ProfileEntity
import com.github.kr328.clash.service.ipc.IStreamCallback
import com.github.kr328.clash.service.ipc.ParcelableContainer
import com.github.kr328.clash.service.transact.ProfileRequest
@@ -251,11 +251,11 @@ class ProfileEditActivity : BaseActivity() {
val source = intent?.getParcelableExtra<Intent>("intent")?.toUri(0)?.run(Uri::parse)
val type = when (intent?.getStringExtra("type")) {
Constants.URL_PROVIDER_TYPE_FILE ->
ClashProfileEntity.TYPE_FILE
ProfileEntity.TYPE_FILE
Constants.URL_PROVIDER_TYPE_URL ->
ClashProfileEntity.TYPE_URL
ProfileEntity.TYPE_URL
Constants.URL_PROVIDER_TYPE_EXTERNAL ->
ClashProfileEntity.TYPE_EXTERNAL
ProfileEntity.TYPE_EXTERNAL
else -> throw IllegalArgumentException()
}

View File

@@ -8,7 +8,7 @@ import com.github.kr328.clash.adapter.ProfileAdapter
import com.github.kr328.clash.remote.withProfile
import com.github.kr328.clash.common.ids.Intents
import com.github.kr328.clash.service.ProfileBackgroundService
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.data.ProfileEntity
import com.github.kr328.clash.service.transact.ProfileRequest
import com.github.kr328.clash.service.util.componentName
import com.github.kr328.clash.service.util.intent
@@ -96,7 +96,7 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C
reloadMutex.unlock()
}
override fun onProfileClicked(entity: ClashProfileEntity) {
override fun onProfileClicked(entity: ProfileEntity) {
launch {
withProfile {
setActiveProfile(entity.id)
@@ -104,7 +104,7 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C
}
}
override fun onMenuClicked(entity: ClashProfileEntity) {
override fun onMenuClicked(entity: ProfileEntity) {
ProfilesMenu(this, entity, this).show()
}
@@ -112,7 +112,7 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C
startActivity(CreateProfileActivity::class.intent)
}
private fun deleteProfile(entity: ClashProfileEntity) = launch {
private fun deleteProfile(entity: ProfileEntity) = launch {
val request = ProfileRequest().action(ProfileRequest.Action.REMOVE).withId(entity.id)
withProfile {
@@ -120,7 +120,7 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C
}
}
private fun resetProviders(entity: ClashProfileEntity) = launch {
private fun resetProviders(entity: ProfileEntity) = launch {
val request = ProfileRequest().action(ProfileRequest.Action.CLEAR).withId(entity.id)
withProfile {
@@ -128,13 +128,13 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C
}
}
private fun openPropertiesEditor(entity: ClashProfileEntity, duplicate: Boolean) {
private fun openPropertiesEditor(entity: ProfileEntity, duplicate: Boolean) {
val type = when (entity.type) {
ClashProfileEntity.TYPE_FILE ->
ProfileEntity.TYPE_FILE ->
Constants.URL_PROVIDER_TYPE_FILE
ClashProfileEntity.TYPE_URL ->
ProfileEntity.TYPE_URL ->
Constants.URL_PROVIDER_TYPE_URL
ClashProfileEntity.TYPE_EXTERNAL ->
ProfileEntity.TYPE_EXTERNAL ->
Constants.URL_PROVIDER_TYPE_EXTERNAL
else -> throw IllegalArgumentException("Invalid type ${entity.type}")
}
@@ -154,7 +154,7 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C
startActivity(editor)
}
private fun openEditor(entity: ClashProfileEntity) = launch {
private fun openEditor(entity: ProfileEntity) = launch {
val uri = withProfile {
requestProfileEditUri(entity.id)
} ?: return@launch
@@ -169,39 +169,39 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C
)
}
private fun startUpdate(entity: ClashProfileEntity) {
private fun startUpdate(entity: ProfileEntity) {
val request = ProfileRequest()
.action(ProfileRequest.Action.UPDATE_OR_CREATE)
.withId(entity.id)
val intent = Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST)
val intent = Intent(Intents.INTENT_ACTION_PROFILE_REQUEST_UPDATE)
.setComponent(ProfileBackgroundService::class.componentName)
.putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, request)
startForegroundServiceCompat(intent)
}
override fun onOpenEditor(entity: ClashProfileEntity) {
override fun onOpenEditor(entity: ProfileEntity) {
openEditor(entity)
}
override fun onUpdate(entity: ClashProfileEntity) {
override fun onUpdate(entity: ProfileEntity) {
startUpdate(entity)
}
override fun onOpenProperties(entity: ClashProfileEntity) {
override fun onOpenProperties(entity: ProfileEntity) {
openPropertiesEditor(entity, false)
}
override fun onDuplicate(entity: ClashProfileEntity) {
override fun onDuplicate(entity: ProfileEntity) {
openPropertiesEditor(entity, true)
}
override fun onResetProvider(entity: ClashProfileEntity) {
override fun onResetProvider(entity: ProfileEntity) {
resetProviders(entity)
}
override fun onDelete(entity: ClashProfileEntity) {
override fun onDelete(entity: ProfileEntity) {
deleteProfile(entity)
}
}

View File

@@ -9,7 +9,7 @@ import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.data.ProfileEntity
import com.github.kr328.clash.utils.IntervalUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -17,12 +17,12 @@ import kotlinx.coroutines.withContext
class ProfileAdapter(private val context: Context, private val callback: Callback) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
interface Callback {
fun onProfileClicked(entity: ClashProfileEntity)
fun onMenuClicked(entity: ClashProfileEntity)
fun onProfileClicked(entity: ProfileEntity)
fun onMenuClicked(entity: ProfileEntity)
fun onNewProfile()
}
private var entities: List<ClashProfileEntity> = emptyList()
private var entities: List<ProfileEntity> = emptyList()
class EntityHolder(view: View) : RecyclerView.ViewHolder(view) {
val root: View = view.findViewById(R.id.root)
@@ -37,7 +37,7 @@ class ProfileAdapter(private val context: Context, private val callback: Callbac
val root: View = view.findViewById(R.id.root)
}
suspend fun setEntitiesAsync(new: List<ClashProfileEntity>) {
suspend fun setEntitiesAsync(new: List<ProfileEntity>) {
val old = withContext(Dispatchers.Main) {
entities
}
@@ -120,11 +120,11 @@ class ProfileAdapter(private val context: Context, private val callback: Callbac
private fun getTypeName(type: Int): CharSequence {
return when (type) {
ClashProfileEntity.TYPE_FILE ->
ProfileEntity.TYPE_FILE ->
context.getText(R.string.file)
ClashProfileEntity.TYPE_URL ->
ProfileEntity.TYPE_URL ->
context.getText(R.string.url)
ClashProfileEntity.TYPE_EXTERNAL ->
ProfileEntity.TYPE_EXTERNAL ->
context.getText(R.string.external)
else ->
context.getText(R.string.unknown)

View File

@@ -1,17 +1,17 @@
package com.github.kr328.clash.remote
import com.github.kr328.clash.service.IProfileService
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.data.ProfileEntity
import com.github.kr328.clash.service.transact.ProfileRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ProfileClient(private val service: IProfileService) {
suspend fun queryProfiles(): Array<ClashProfileEntity> = withContext(Dispatchers.IO) {
suspend fun queryProfiles(): Array<ProfileEntity> = withContext(Dispatchers.IO) {
service.queryProfiles()
}
suspend fun queryActiveProfile(): ClashProfileEntity? = withContext(Dispatchers.IO) {
suspend fun queryActiveProfile(): ProfileEntity? = withContext(Dispatchers.IO) {
service.queryActiveProfile()
}

View File

@@ -6,21 +6,21 @@ import android.view.ViewGroup
import androidx.annotation.ColorInt
import com.github.kr328.clash.R
import com.github.kr328.clash.design.view.CommonUiLayout
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.data.ProfileEntity
import com.google.android.material.bottomsheet.BottomSheetDialog
class ProfilesMenu(
context: Context,
private val entity: ClashProfileEntity,
private val entity: ProfileEntity,
private val callback: Callback
) : BottomSheetDialog(context) {
interface Callback {
fun onOpenEditor(entity: ClashProfileEntity)
fun onUpdate(entity: ClashProfileEntity)
fun onOpenProperties(entity: ClashProfileEntity)
fun onDuplicate(entity: ClashProfileEntity)
fun onResetProvider(entity: ClashProfileEntity)
fun onDelete(entity: ClashProfileEntity)
fun onOpenEditor(entity: ProfileEntity)
fun onUpdate(entity: ProfileEntity)
fun onOpenProperties(entity: ProfileEntity)
fun onDuplicate(entity: ProfileEntity)
fun onResetProvider(entity: ProfileEntity)
fun onDelete(entity: ProfileEntity)
}
init {
@@ -37,7 +37,7 @@ class ProfilesMenu(
}
menu.build {
if (entity.type != ClashProfileEntity.TYPE_FILE) {
if (entity.type != ProfileEntity.TYPE_FILE) {
option(
title = context.getString(R.string.update),
icon = context.getDrawable(R.drawable.ic_update)

View File

@@ -1,4 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.kr328.clash.common">
<uses-permission android:name="android.permission.INTERNET" />
<permission
android:name="${applicationId}.permission.RECEIVE_BROADCASTS"
android:label="@string/receive_clash_broadcasts"
android:description="@string/receive_broadcasts_of_clash"
android:protectionLevel="privileged|signature" />
</manifest>

View File

@@ -1,11 +1,12 @@
package com.github.kr328.clash.core
package com.github.kr328.clash.common
import android.app.Application
object Global {
lateinit var application: Application
private set
fun init(application: Application) {
this.application = application
Global.application = application
}
}

View File

@@ -0,0 +1,6 @@
package com.github.kr328.clash.common
object Permissions {
val PERMISSION_ACCESS_CLASH: String
get() = Global.application.packageName + ".permission.RECEIVE_BROADCASTS"
}

View File

@@ -9,7 +9,7 @@ object Intents {
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.stopped"
const val INTENT_ACTION_PROFILE_CHANGED =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.changed"
const val INTENT_ACTION_PROFILE_ENQUEUE_REQUEST =
const val INTENT_ACTION_PROFILE_REQUEST_UPDATE =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.enqueue.request"
const val INTENT_ACTION_PROFILE_SETUP =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.setup"
@@ -24,8 +24,6 @@ object Intents {
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.stop.reason"
const val INTENT_EXTRA_PROFILE_REQUEST =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.profile.request"
const val INTENT_EXTRA_START_TUN =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.start.tun"
const val INTENT_EXTRA_PROFILE_ID =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.profile.id"
}

View File

@@ -2,4 +2,6 @@ package com.github.kr328.clash.common.ids
object NotificationChannels {
const val CLASH_STATUS = "clash_status_channel"
const val PROFILE_STATUS = "profile_status_channel"
const val PROFILE_RESULT = "profile_result_channel"
}

View File

@@ -3,4 +3,11 @@ package com.github.kr328.clash.common.ids
object NotificationIds {
const val CLASH_STATUS = 1
const val CLASH_VPN = 2
const val PROFILE_STATUS = 3
private val PROFILE_RESULT = 10000..20000
fun generateProfileResultId(profileId: Long): Int {
val bound = PROFILE_RESULT.last - PROFILE_RESULT.first
return (profileId % bound + PROFILE_RESULT.first).toInt()
}
}

View File

@@ -0,0 +1,7 @@
package com.github.kr328.clash.common.ids
object PendingIds {
fun generateProfileResultId(profileId: Long): Int {
return NotificationIds.generateProfileResultId(profileId)
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="receive_clash_broadcasts">Receive Clash Broadcasts</string>
<string name="receive_broadcasts_of_clash">Receive broadcasts of clash services</string>
</resources>

View File

@@ -1,8 +1,8 @@
package com.github.kr328.clash.core
import android.content.Context
import bridge.Bridge
import bridge.TunCallback
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.core.event.LogEvent
import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.model.Proxy
@@ -18,13 +18,8 @@ import java.io.InputStream
object Clash {
private val logReceivers = mutableMapOf<String, (LogEvent) -> Unit>()
private var initialized = false
@Synchronized
fun initialize(context: Context) {
if (initialized)
return
initialized = true
fun init() {
val context = Global.application
val bytes = context.assets.open("Country.mmdb")
.use(InputStream::readBytes)

View File

@@ -3,6 +3,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application>
<service
@@ -31,11 +32,6 @@
android:exported="false"
android:process=":background" />
<receiver
android:name=".ProfileRequestReceiver"
android:exported="false"
android:process=":background" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.profiles"
@@ -66,5 +62,11 @@
android:authorities="${applicationId}.settings"
android:exported="false"
android:process=":background" />
<receiver
android:name=".ProfileReceiver"
android:exported="false"
android:process=":background"
android:permission="${applicationId}.permission.RECEIVE_BROADCASTS" />
</application>
</manifest>

View File

@@ -1,7 +1,6 @@
package com.github.kr328.clash.service;
import com.github.kr328.clash.service.ipc.IStreamCallback;
import com.github.kr328.clash.service.data.ClashProfileEntity;
import com.github.kr328.clash.core.model.Packet;
interface IClashManager {

View File

@@ -1,15 +1,20 @@
package com.github.kr328.clash.service;
import com.github.kr328.clash.service.ipc.IStreamCallback;
import com.github.kr328.clash.service.transact.ProfileRequest;
import com.github.kr328.clash.service.data.ClashProfileEntity;
import com.github.kr328.clash.service.model.ProfileMetadata;
interface IProfileService {
void enqueueRequest(in ProfileRequest request);
String requestProfileEditUri(long id);
void commitProfileEditUri(String uri);
long acquireUnused(String type);
void updateMetadata(long id, in ProfileMetadata metadata);
void commit(long id, in IStreamCallback callback);
void cancel(long id);
void delete(long id);
void clear(long id);
ClashProfileEntity[] queryProfiles();
ClashProfileEntity queryActiveProfile();
ProfileMetadata queryById(long id);
ProfileMetadata[] queryAll();
ProfileMetadata queryActive();
void setActiveProfile(long id);
void setActive(long id);
}

View File

@@ -1,3 +0,0 @@
package com.github.kr328.clash.service.data;
parcelable ClashProfileEntity;

View File

@@ -0,0 +1,3 @@
package com.github.kr328.clash.service.model;
parcelable ProfileMetadata;

View File

@@ -4,8 +4,9 @@ import android.content.Context
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.model.ProxyGroupList
import com.github.kr328.clash.service.data.ClashDatabase
import com.github.kr328.clash.service.data.ClashProfileProxyEntity
import com.github.kr328.clash.service.data.ProfileDao
import com.github.kr328.clash.service.data.SelectedProxyDao
import com.github.kr328.clash.service.data.SelectedProxyEntity
import com.github.kr328.clash.service.ipc.IStreamCallback
import com.github.kr328.clash.service.ipc.ParcelableContainer
import kotlinx.coroutines.CoroutineScope
@@ -14,7 +15,6 @@ import kotlinx.coroutines.launch
class ClashManager(context: Context, parent: CoroutineScope) :
IClashManager.Stub(), CoroutineScope by parent {
private val settings = context.getSharedPreferences("service", Context.MODE_PRIVATE)
private val database = ClashDatabase.getInstance(context)
override fun setProxyMode(mode: String?) {
Clash.setProxyMode(requireNotNull(mode))
@@ -32,10 +32,9 @@ class ClashManager(context: Context, parent: CoroutineScope) :
require(proxy != null && selected != null)
launch {
val current = database.openClashProfileDao()
.queryActiveProfile() ?: return@launch
database.openClashProfileProxyDao()
.setSelectedForProfile(ClashProfileProxyEntity(current.id, proxy, selected))
val current = ProfileDao.queryActive() ?: return@launch
SelectedProxyDao.setSelectedForProfile(SelectedProxyEntity(current.id, proxy, selected))
}
return Clash.setSelectedProxy(proxy, selected)

View File

@@ -9,43 +9,32 @@ 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.common.ids.Intents
import com.github.kr328.clash.service.data.ClashDatabase
import com.github.kr328.clash.service.data.ClashProfileDao
import com.github.kr328.clash.common.ids.NotificationChannels
import com.github.kr328.clash.common.ids.NotificationIds
import com.github.kr328.clash.service.data.ProfileDao
import com.github.kr328.clash.service.ipc.IStreamCallback
import com.github.kr328.clash.service.ipc.ParcelableContainer
import com.github.kr328.clash.service.transact.ProfileRequest
import com.github.kr328.clash.service.util.RandomUtils
import com.github.kr328.clash.service.util.UpdateUtils
import com.github.kr328.clash.service.util.intent
import kotlinx.coroutines.channels.Channel
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 val requestChannel = Channel<ProfileRequest>(2)
private val responseChannel = Channel<ProfileRequest>(2)
private val profiles: ClashProfileDao by lazy {
ClashDatabase.getInstance(this).openClashProfileDao()
}
private val self = this
private val requests = Channel<Long>(Channel.UNLIMITED)
private val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
requestChannel.close()
responseChannel.close()
stopSelf()
}
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
val service = IProfileService.Stub.asInterface(binder) ?: return stopSelf()
startProfileProcessor(service)
processProfiles(service)
}
}
@@ -71,19 +60,16 @@ class ProfileBackgroundService : BaseService() {
super.onStartCommand(intent, flags, startId)
when (intent?.action) {
Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST -> {
val request =
intent.getParcelableExtra<ProfileRequest>(Intents.INTENT_EXTRA_PROFILE_REQUEST)
?: return START_NOT_STICKY
launch {
requestChannel.send(request)
}
Intents.INTENT_ACTION_PROFILE_REQUEST_UPDATE -> {
val id = intent.getLongExtra(Intents.INTENT_EXTRA_PROFILE_ID, -1)
if (id < 0)
return START_NOT_STICKY
requests.offer(id)
}
Intents.INTENT_ACTION_PROFILE_SETUP -> {
launch {
resetProfileUpdateAlarm()
stopSelf()
setup()
}
}
}
@@ -95,20 +81,32 @@ class ProfileBackgroundService : BaseService() {
return Binder()
}
private fun startProfileProcessor(service: IProfileService) = launch {
val queue: MutableMap<Long, ProfileRequest> = mutableMapOf()
private fun processProfiles(service: IProfileService) = launch {
val queue: MutableSet<Long> = mutableSetOf()
val responses = Channel<Pair<Long, Exception?>>(Channel.UNLIMITED)
do {
select<Unit> {
requestChannel.onReceive {
if (!queue.containsKey(it.id)) {
queue[it.id] = it
requests.onReceive {
ProfileReceiver.cancelNextUpdate(self, it)
sendRequest(it, service)
}
service.commit(it, object : IStreamCallback.Stub() {
override fun completeExceptionally(reason: String?) {
responses.offer(it to RemoteException(reason))
}
override fun complete() {
responses.offer(it to null)
}
override fun send(data: ParcelableContainer?) {}
})
}
responseChannel.onReceive {
queue.remove(it.id)
responses.onReceive {
if (it.second == null)
sendUpdateCompleted(it.first)
else
sendUpdateFailed(it.first, it.second!!.message ?: "Unknown")
}
}
@@ -118,39 +116,9 @@ class ProfileBackgroundService : BaseService() {
stopSelf()
}
private fun sendRequest(request: ProfileRequest, service: IProfileService) {
val originalCallback = request.callback
request.withCallback(object : IStreamCallback.Stub() {
override fun complete() {
originalCallback?.complete()
launch {
responseChannel.send(request)
sendUpdateCompleted(request.id)
}
}
override fun completeExceptionally(reason: String?) {
originalCallback?.completeExceptionally(reason)
launch {
responseChannel.send(request)
sendUpdateFailure(request.id, reason ?: "Unknown")
}
}
override fun send(data: ParcelableContainer?) {}
})
service.enqueueRequest(request)
}
private suspend fun resetProfileUpdateAlarm() {
for (entity in profiles.queryProfiles()) {
UpdateUtils.resetProfileUpdateAlarm(this, entity)
private suspend fun setup() {
for (id in ProfileDao.queryAllIds()) {
ProfileReceiver.requestNextUpdate(this, id)
}
}
@@ -161,12 +129,12 @@ class ProfileBackgroundService : BaseService() {
NotificationManagerCompat.from(this).createNotificationChannels(
listOf(
NotificationChannel(
SERVICE_STATUS_CHANNEL,
NotificationChannels.PROFILE_STATUS,
getText(R.string.profile_service_status_channel),
NotificationManager.IMPORTANCE_LOW
),
NotificationChannel(
SERVICE_RESULT_CHANNEL,
NotificationChannels.PROFILE_RESULT,
getText(R.string.profile_status_channel),
NotificationManager.IMPORTANCE_DEFAULT
)
@@ -175,47 +143,47 @@ class ProfileBackgroundService : BaseService() {
}
private fun refreshStatusNotification(queueSize: Int) {
val notification = NotificationCompat.Builder(this, SERVICE_STATUS_CHANNEL)
val notification = NotificationCompat.Builder(this, NotificationChannels.PROFILE_STATUS)
.setContentTitle(getText(R.string.processing_profiles))
.setContentText(getString(R.string.format_in_queue, queueSize))
.setColor(getColor(R.color.colorAccentService))
.setSmallIcon(R.drawable.ic_notification)
.setOnlyAlertOnce(true)
.setGroup(SERVICE_STATUS_CHANNEL)
.setGroup(NotificationChannels.PROFILE_STATUS)
.build()
startForeground(RandomUtils.nextInt(), notification)
startForeground(NotificationIds.CLASH_VPN, notification)
}
private suspend fun sendUpdateCompleted(id: Long) {
val entity = profiles.queryProfileById(id) ?: return
val entity = ProfileDao.queryById(id) ?: return
val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL)
val notification = NotificationCompat.Builder(this, NotificationChannels.PROFILE_RESULT)
.setContentTitle(getText(R.string.process_result))
.setContentText(getString(R.string.format_update_complete, entity.name))
.setColor(getColor(R.color.colorAccentService))
.setSmallIcon(R.drawable.ic_notification)
.setOnlyAlertOnce(true)
.setGroup(SERVICE_RESULT_CHANNEL)
.setGroup(NotificationChannels.PROFILE_RESULT)
.build()
NotificationManagerCompat.from(this)
.notify(RandomUtils.nextInt(), notification)
.notify(NotificationIds.generateProfileResultId(id), notification)
}
private suspend fun sendUpdateFailure(id: Long, reason: String) {
val entity = profiles.queryProfileById(id) ?: return
private suspend fun sendUpdateFailed(id: Long, reason: String) {
val entity = ProfileDao.queryById(id) ?: return
val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL)
val notification = NotificationCompat.Builder(this, NotificationChannels.PROFILE_RESULT)
.setContentTitle(getString(R.string.format_update_failure, entity.name))
.setColor(getColor(R.color.colorAccentService))
.setSmallIcon(R.drawable.ic_notification)
.setStyle(NotificationCompat.BigTextStyle().bigText(reason))
.setOnlyAlertOnce(true)
.setGroup(SERVICE_RESULT_CHANNEL)
.setGroup(NotificationChannels.PROFILE_RESULT)
.build()
NotificationManagerCompat.from(this)
.notify(RandomUtils.nextInt(), notification)
.notify(NotificationIds.generateProfileResultId(id), notification)
}
}

View File

@@ -7,7 +7,6 @@ import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract.Document
import android.provider.DocumentsContract.Root
import android.provider.DocumentsProvider
import com.github.kr328.clash.service.data.ClashDatabase
import com.github.kr328.clash.service.files.ProfilesResolver
import kotlinx.coroutines.runBlocking
@@ -33,7 +32,7 @@ class ProfileDocumentProvider : DocumentsProvider() {
}
private val resolver: ProfilesResolver by lazy {
ProfilesResolver(context!!, ClashDatabase.getInstance(context!!))
ProfilesResolver(context!!)
}
override fun openDocument(

View File

@@ -2,91 +2,71 @@ package com.github.kr328.clash.service
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import android.webkit.URLUtil
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.service.data.ClashDatabase
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.util.resolveBase
import com.github.kr328.clash.service.util.resolveProfile
import com.github.kr328.clash.service.data.ProfileDao
import com.github.kr328.clash.service.model.ProfileMetadata
import com.github.kr328.clash.service.model.ProfileMetadata.Type
import com.github.kr328.clash.service.model.toProfileEntity
import com.github.kr328.clash.service.util.resolveBaseDir
import com.github.kr328.clash.service.util.resolveProfileFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileNotFoundException
import java.util.*
class ProfileProcessor(private val context: Context) {
suspend fun createOrUpdate(entity: ClashProfileEntity, newRecord: Boolean) {
val database = ClashDatabase.getInstance(context).openClashProfileDao()
object ProfileProcessor {
suspend fun createOrUpdate(context: Context, metadata: ProfileMetadata) =
withContext(Dispatchers.IO) {
metadata.enforceFieldValid()
val uri = Uri.parse(entity.uri)
if (uri == null || uri == Uri.EMPTY)
throw IllegalArgumentException("Invalid uri $uri")
downloadProfile(
uri,
resolveProfile(entity.id),
resolveBase(entity.id),
newRecord
)
val newEntity = if (entity.type == ClashProfileEntity.TYPE_FILE)
entity.copy(
lastUpdate = System.currentTimeMillis(),
uri = FileProvider.getUriForFile(
context,
"${context.packageName}${Constants.PROFILE_PROVIDER_SUFFIX}",
resolveProfile(entity.id)
).toString()
downloadProfile(
context, metadata.uri,
context.resolveProfileFile(metadata.id),
context.resolveBaseDir(metadata.id)
)
else
entity.copy(lastUpdate = System.currentTimeMillis())
if (newRecord)
database.addProfile(newEntity)
else
database.updateProfile(newEntity)
}
val entity = metadata.toProfileEntity()
suspend fun remove(id: Long) {
val database = ClashDatabase.getInstance(context).openClashProfileDao()
resolveProfile(id).delete()
resolveBase(id).deleteRecursively()
database.removeProfile(id)
}
fun clear(id: Long) {
resolveBase(id).listFiles()?.forEach {
it.deleteRecursively()
if (ProfileDao.queryById(metadata.id) == null)
ProfileDao.insert(entity)
else
ProfileDao.update(entity)
}
}
private suspend fun downloadProfile(
context: Context,
source: Uri,
target: File,
baseDir: File,
newRecord: Boolean
) {
try {
target.parentFile?.mkdirs()
baseDir.mkdirs()
if (source.scheme.equals("content", ignoreCase = true)
|| source.scheme.equals("file", ignoreCase = true)
) {
val parcelFileDescriptor = context.contentResolver.openFileDescriptor(source, "r")
?: throw FileNotFoundException("Unable to open file $source")
val fd = parcelFileDescriptor.detachFd()
Clash.downloadProfile(fd, target, baseDir).await()
} else {
Clash.downloadProfile(source.toString(), target, baseDir).await()
baseDir: File
) = withContext(Dispatchers.IO) {
when (source.scheme?.toLowerCase(Locale.getDefault())) {
"http", "https" ->
Clash.downloadProfile(source.toString(), target, baseDir)
"content", "file", "resource" -> {
val fd = context.contentResolver.openFileDescriptor(source, "r")
?: throw FileNotFoundException("$source not found")
Clash.downloadProfile(fd.detachFd(), target, baseDir)
}
} catch (e: Exception) {
if (newRecord) {
target.delete()
baseDir.deleteRecursively()
}
throw e
else -> throw IllegalArgumentException("Invalid uri type")
}.await()
}
private fun ProfileMetadata.enforceFieldValid() {
when {
id < 0 ->
throw IllegalArgumentException("Invalid id")
name.isBlank() ->
throw IllegalArgumentException("Empty name")
type != Type.FILE && type != Type.URL && type != Type.EXTERNAL ->
throw IllegalArgumentException("Invalid type")
!URLUtil.isValidUrl(uri.toString()) ->
throw IllegalArgumentException("Invalid uri")
source?.let { URLUtil.isValidUrl(it.toString()) } == false ->
throw IllegalArgumentException("Invalid source")
interval < 0 ->
throw IllegalArgumentException("Invalid interval")
}
}
}

View File

@@ -0,0 +1,76 @@
package com.github.kr328.clash.service
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.content.getSystemService
import com.github.kr328.clash.common.ids.Intents
import com.github.kr328.clash.common.ids.PendingIds
import com.github.kr328.clash.service.data.ProfileDao
import com.github.kr328.clash.service.model.toProfileMetadata
import com.github.kr328.clash.service.util.componentName
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.service.util.startForegroundServiceCompat
class ProfileReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (context == null) return
when (intent?.action) {
Intents.INTENT_ACTION_PROFILE_REQUEST_UPDATE -> {
// Redirect to service
intent.component = ProfileBackgroundService::class.componentName
context.startForegroundServiceCompat(intent)
}
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
if (initialized)
return
initialized = true
context.startForegroundServiceCompat(
ProfileBackgroundService::class.intent
.setAction(Intents.INTENT_ACTION_PROFILE_SETUP)
)
}
}
}
companion object {
private val requested = mutableMapOf<Long, PendingIntent>()
private var initialized = false
suspend fun requestNextUpdate(context: Context, id: Long) {
val metadata = ProfileDao.queryById(id)?.toProfileMetadata(context) ?: return
val service = context.getSystemService<AlarmManager>() ?: return
if (metadata.interval <= 0)
return
cancelNextUpdate(context, id)
val pendingIntent = PendingIntent.getBroadcast(
context,
PendingIds.generateProfileResultId(id),
Intent(Intents.INTENT_ACTION_PROFILE_REQUEST_UPDATE)
.setComponent(ProfileReceiver::class.componentName)
.putExtra(Intents.INTENT_EXTRA_PROFILE_ID, id),
PendingIntent.FLAG_UPDATE_CURRENT
)
service.set(
AlarmManager.RTC,
metadata.lastModified + metadata.interval,
pendingIntent
)
requested[id] = pendingIntent
}
fun cancelNextUpdate(context: Context, id: Long) {
val service = context.getSystemService<AlarmManager>() ?: return
service.cancel(requested.remove(id) ?: return)
}
}
}

View File

@@ -1,30 +0,0 @@
package com.github.kr328.clash.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.github.kr328.clash.common.ids.Intents
import com.github.kr328.clash.service.transact.ProfileRequest
import com.github.kr328.clash.service.util.componentName
import com.github.kr328.clash.service.util.startForegroundServiceCompat
class ProfileRequestReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST || context == null)
return
val id = intent.getLongExtra(Intents.INTENT_EXTRA_PROFILE_ID, -1)
if (id < 0)
return
val request = ProfileRequest()
.action(ProfileRequest.Action.UPDATE_OR_CREATE)
.withId(id)
val service = Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST)
.setComponent(ProfileBackgroundService::class.componentName)
.putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, request)
context.startForegroundServiceCompat(service)
}
}

View File

@@ -3,104 +3,132 @@ package com.github.kr328.clash.service
import android.content.Intent
import android.net.Uri
import android.os.IBinder
import androidx.core.content.FileProvider
import com.github.kr328.clash.common.ids.Intents
import com.github.kr328.clash.core.Global
import com.github.kr328.clash.core.utils.Log
import com.github.kr328.clash.service.data.ClashDatabase
import com.github.kr328.clash.service.data.ClashProfileEntity
import android.os.RemoteException
import com.github.kr328.clash.service.data.ProfileDao
import com.github.kr328.clash.service.ipc.IStreamCallback
import com.github.kr328.clash.service.ipc.ParcelableContainer
import com.github.kr328.clash.service.transact.ProfileRequest
import com.github.kr328.clash.service.util.*
import com.github.kr328.clash.service.model.ProfileMetadata
import com.github.kr328.clash.service.model.toProfileMetadata
import com.github.kr328.clash.service.util.broadcastProfileChanged
import com.github.kr328.clash.service.util.resolveBaseDir
import com.github.kr328.clash.service.util.resolveProfileFile
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.*
class ProfileService : BaseService() {
private val service = this
private val queue: MutableMap<Long, Channel<ProfileRequest>> = Hashtable()
private val pending = mutableListOf<ProfileRequest>()
private val profiles = ClashDatabase.getInstance(Global.application).openClashProfileDao()
private val processor = ProfileProcessor(this)
private val lock = Mutex()
private val pending = Collections.synchronizedMap(mutableMapOf<Long, ProfileMetadata>())
private val tasks = mutableMapOf<Long, IStreamCallback?>()
private val request = Channel<Unit>(Channel.CONFLATED)
override fun onBind(intent: Intent?): IBinder? {
return object : IProfileService.Stub() {
override fun enqueueRequest(request: ProfileRequest?) {
service.enqueueRequest(request ?: return)
}
override fun queryActiveProfile(): ClashProfileEntity? {
return runBlocking {
profiles.queryActiveProfile()
}
}
override fun queryProfiles(): Array<ClashProfileEntity> {
return runBlocking {
profiles.queryProfiles()
}
}
override fun setActiveProfile(id: Long) {
override fun setActive(id: Long) {
launch {
profiles.setActiveProfile(id)
ProfileDao.setActive(id)
broadcastProfileChanged()
service.broadcastProfileChanged()
}
}
override fun requestProfileEditUri(id: Long): String? {
override fun commit(id: Long, callback: IStreamCallback?) {
launch {
lock.withLock {
tasks[id] = callback
request.offer(Unit)
}
}
}
override fun cancel(id: Long) {
launch {
lock.withLock {
if (pending.remove(id) != null)
service.resolveBaseDir(id).deleteRecursively()
}
}
}
override fun acquireUnused(type: String): Long {
return runBlocking {
val entity = profiles.queryProfileById(id) ?: return@runBlocking null
lock.withLock {
val id = (ProfileDao.queryAllIds() + pending.keys).max()?.plus(1) ?: 0
val baseDir = cacheDir.resolve("profiles").apply { mkdirs() }
pending[id] = ProfileMetadata(
id = id,
name = "",
type = ProfileMetadata.Type.valueOf(type),
uri = Uri.EMPTY,
source = null,
active = false,
interval = 0,
lastModified = 0
)
val fileName = RandomUtils.fileName(baseDir, ".yaml")
service.resolveBaseDir(id).apply {
deleteRecursively()
mkdirs()
}
val file = resolveProfile(entity.id).copyTo(baseDir.resolve(fileName))
val url = FileProvider.getUriForFile(
service,
"$packageName${Constants.PROFILE_PROVIDER_SUFFIX}",
file
).toString()
"$url?id=${entity.id}&fileName=$fileName"
id
}
}
}
override fun commitProfileEditUri(uri: String?) {
val u = Uri.parse(uri)
override fun queryActive(): ProfileMetadata? {
return runBlocking {
ProfileDao.queryActive()?.toProfileMetadata(service)
}
}
if (u == null || u == Uri.EMPTY)
return
override fun delete(id: Long) {
launch {
lock.withLock {
pending.remove(id)
ProfileDao.remove(id)
}
val id = u.getQueryParameter("id")?.toLongOrNull() ?: return
val fileName = u.getQueryParameter("fileName") ?: return
service.resolveProfileFile(id).delete()
service.resolveBaseDir(id).deleteRecursively()
val request = ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE)
.withId(id)
.withURL(u)
.withCallback(object : IStreamCallback.Stub() {
override fun complete() {
cacheDir.resolve("profiles/$fileName").delete()
service.broadcastProfileChanged()
}
}
override fun clear(id: Long) {
launch {
withContext(Dispatchers.IO) {
resolveBaseDir(id).listFiles()?.forEach {
it.deleteRecursively()
}
}
}
}
override fun completeExceptionally(reason: String?) {
cacheDir.resolve("profiles/$fileName").delete()
}
override fun queryAll(): Array<ProfileMetadata> {
return runBlocking {
ProfileDao.queryAll().map { it.toProfileMetadata(service) }.toTypedArray()
}
}
override fun send(data: ParcelableContainer?) {
override fun queryById(id: Long): ProfileMetadata? {
return runBlocking {
lock.withLock {
queryMetadataById(id)
}
}
}
}
})
val i = ProfileBackgroundService::class.intent
.setAction(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST)
.putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, request)
startForegroundServiceCompat(i)
override fun updateMetadata(id: Long, metadata: ProfileMetadata?) {
launch {
lock.withLock {
pending[id] = metadata ?: return@launch
}
}
}
}
}
@@ -108,112 +136,41 @@ class ProfileService : BaseService() {
override fun onCreate() {
super.onCreate()
Log.d("ProfileService.onCreate")
}
override fun onDestroy() {
super.onDestroy()
pending.forEach {
it.callback?.completeExceptionally("Canceled")
launch {
process()
}
Log.d("ProfileService.onDestroy")
}
private fun createChannelForRequests(id: Long): Channel<ProfileRequest> {
return Channel<ProfileRequest>(Channel.UNLIMITED).also {
launch {
try {
Log.d("Coroutine for $id launched")
private suspend fun process() {
while (isActive) {
request.receive()
while (isActive) {
val request = withTimeout(1000 * 30) {
it.receive()
}
Log.d("Handling $id")
handleRequest(request)
}
} finally {
Log.d("Coroutine for $id exited")
queue.remove(id)
val ctx = lock.withLock {
tasks.entries.firstOrNull()?.also {
tasks.remove(it.key)
}
}
}
}
} ?: continue
private fun enqueueRequest(request: ProfileRequest) {
Log.d("Request $request enqueue")
try {
val metadata = queryMetadataById(ctx.key)
?: throw RemoteException("No such profile")
pending.add(request)
queue.computeIfAbsent(request.id) {
createChannelForRequests(it)
}.offer(request)
}
private suspend fun handleRequest(request: ProfileRequest) {
try {
request.callback?.send(null)
when (request.action) {
ProfileRequest.Action.UPDATE_OR_CREATE ->
handleUpdateOrCreate(request)
ProfileRequest.Action.REMOVE ->
removeProfile(request)
ProfileRequest.Action.CLEAR ->
clearProfile(request)
}
request.callback?.complete()
broadcastProfileChanged()
} catch (e: Exception) {
Log.w("handleRequest", e)
request.callback?.completeExceptionally(e.message)
} finally {
pending.remove(request)
}
}
private suspend fun handleUpdateOrCreate(request: ProfileRequest) =
withContext(Dispatchers.IO) {
val id = request.id
val entity: ClashProfileEntity =
if (id == -1L) {
ClashProfileEntity(
requireNotNull(request.name),
requireNotNull(request.type),
requireNotNull(request.url).toString(),
request.source?.toString(),
false,
0,
request.interval.takeIf { it >= 0 } ?: 0,
profiles.generateNewId()
)
} else {
val e = profiles.queryProfileById(id) ?: return@withContext
e.copy(
name = request.name ?: e.name,
uri = request.url?.toString() ?: e.uri,
updateInterval = request.interval.takeIf { it >= 0 } ?: e.updateInterval
)
lock.withLock {
pending.remove(metadata.id)
}
processor.createOrUpdate(entity, id == -1L)
ProfileProcessor.createOrUpdate(service, metadata)
UpdateUtils.resetProfileUpdateAlarm(service, entity)
ctx.value?.complete()
} catch (e: Exception) {
ctx.value?.completeExceptionally(e.message)
}
request.offer(Unit)
}
private suspend fun removeProfile(request: ProfileRequest) = withContext(Dispatchers.IO) {
processor.remove(request.id)
}
private suspend fun clearProfile(request: ProfileRequest) = withContext(Dispatchers.IO) {
processor.clear(request.id)
private suspend fun queryMetadataById(id: Long): ProfileMetadata? {
return pending[id] ?: ProfileDao.queryById(id)?.toProfileMetadata(service)
}
}

View File

@@ -2,7 +2,6 @@ package com.github.kr328.clash.service
import android.content.Intent
import android.net.VpnService
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.service.clash.ClashRuntime
import com.github.kr328.clash.service.clash.module.*
import com.github.kr328.clash.service.settings.ServiceSettings
@@ -29,8 +28,6 @@ class TunService : VpnService(), CoroutineScope by MainScope() {
override fun onCreate() {
super.onCreate()
Clash.initialize(this)
if (ServiceStatusProvider.serviceRunning)
return stopSelf()

View File

@@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.github.kr328.clash.common.Permissions
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.service.clash.module.Module
import kotlinx.coroutines.channels.Channel
@@ -65,7 +66,7 @@ class ClashRuntime(private val context: Context) {
modules.flatMap { it.receiveBroadcasts }.distinct().forEach {
addAction(it)
}
})
}, Permissions.PERMISSION_ACCESS_CLASH, null)
while (isActive) {
tickerEnabled = modules.any { it.enableTicker }

View File

@@ -10,9 +10,9 @@ import com.github.kr328.clash.common.ids.NotificationChannels
import com.github.kr328.clash.common.ids.NotificationIds
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.utils.asBytesString
import com.github.kr328.clash.core.utils.asSpeedString
import com.github.kr328.clash.service.R
import com.github.kr328.clash.service.ServiceStatusProvider
import com.github.kr328.clash.service.data.ClashDatabase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -68,8 +68,8 @@ class DynamicNotificationModule(private val service: Service) : Module() {
val traffic = Clash.queryTraffic()
val bandwidth = Clash.queryBandwidth()
val uploading = traffic.upload.asBytesString()
val downloading = traffic.download.asBytesString()
val uploading = traffic.upload.asSpeedString()
val downloading = traffic.download.asSpeedString()
val uploaded = bandwidth.upload.asBytesString()
val downloaded = bandwidth.download.asBytesString()

View File

@@ -2,7 +2,6 @@ package com.github.kr328.clash.service.clash.module
import android.content.Context
import android.net.*
import com.github.kr328.clash.core.utils.Log
import kotlinx.coroutines.sync.Mutex
import java.net.InetAddress

View File

@@ -5,9 +5,10 @@ import android.content.Intent
import com.github.kr328.clash.common.ids.Intents
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.service.ServiceStatusProvider
import com.github.kr328.clash.service.data.ClashDatabase
import com.github.kr328.clash.service.util.resolveBase
import com.github.kr328.clash.service.util.resolveProfile
import com.github.kr328.clash.service.data.ProfileDao
import com.github.kr328.clash.service.data.SelectedProxyDao
import com.github.kr328.clash.service.util.resolveBaseDir
import com.github.kr328.clash.service.util.resolveProfileFile
import kotlinx.coroutines.sync.Mutex
class ReloadModule(private val context: Context) : Module() {
@@ -40,20 +41,19 @@ class ReloadModule(private val context: Context) : Module() {
private suspend fun reload() {
try {
val database = ClashDatabase.getInstance(context)
val profileDao = database.openClashProfileDao()
val proxyDao = database.openClashProfileProxyDao()
val active = profileDao.queryActiveProfile()
val active = ProfileDao.queryActive()
?: throw NullPointerException("No profile selected")
Clash.loadProfile(resolveProfile(active.id), resolveBase(active.id)).await()
Clash.loadProfile(
context.resolveProfileFile(active.id),
context.resolveBaseDir(active.id)
).await()
val remove = proxyDao.querySelectedForProfile(active.id)
val remove = SelectedProxyDao.querySelectedForProfile(active.id)
.filterNot { Clash.setSelectedProxy(it.proxy, it.selected) }
.map { it.selected }
proxyDao.removeSelectedForProfile(active.id, remove)
SelectedProxyDao.removeSelectedForProfile(active.id, remove)
ServiceStatusProvider.currentProfile = active.name

View File

@@ -13,7 +13,6 @@ import com.github.kr328.clash.common.ids.NotificationChannels
import com.github.kr328.clash.common.ids.NotificationIds
import com.github.kr328.clash.service.R
import com.github.kr328.clash.service.ServiceStatusProvider
import com.github.kr328.clash.service.data.ClashDatabase
class StaticNotificationModule(private val service: Service) : Module() {
override val receiveBroadcasts: Set<String>
@@ -51,7 +50,7 @@ class StaticNotificationModule(private val service: Service) : Module() {
service.stopForeground(true)
}
private suspend fun update() {
private fun update() {
val profileName = ServiceStatusProvider.currentProfile ?: "Not selected"
val notification = builder

View File

@@ -1,30 +0,0 @@
package com.github.kr328.clash.service.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(
version = 2,
exportSchema = false,
entities = [ClashProfileEntity::class, ClashProfileProxyEntity::class]
)
abstract class ClashDatabase : RoomDatabase() {
abstract fun openClashProfileDao(): ClashProfileDao
abstract fun openClashProfileProxyDao(): ClashProfileProxyDao
companion object {
private var instance: ClashDatabase? = null
fun getInstance(context: Context): ClashDatabase {
if (instance == null)
instance = Room.databaseBuilder(
context.applicationContext,
ClashDatabase::class.java,
"clash-config"
).addMigrations(ClashDatabaseMigrations.VERSION_1_2).build()
return instance ?: throw NullPointerException()
}
}
}

View File

@@ -1,151 +0,0 @@
package com.github.kr328.clash.service.data
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.github.kr328.clash.core.Global
import com.github.kr328.clash.core.utils.Log
import com.github.kr328.clash.service.Constants
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.resolveBase
import com.github.kr328.clash.service.util.resolveProfile
import java.io.File
object ClashDatabaseMigrations {
val VERSION_1_2 = object : Migration(1, 2) {
private fun process(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE profiles RENAME TO _profiles")
database.execSQL("ALTER TABLE profile_select_proxies RENAME TO _profile_select_proxies")
database.execSQL("CREATE TABLE IF NOT EXISTS `profiles` (`name` TEXT NOT NULL, `type` INTEGER NOT NULL, `uri` TEXT NOT NULL, `source` TEXT, `active` INTEGER NOT NULL, `last_update` INTEGER NOT NULL, `update_interval` INTEGER NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`id`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `profile_select_proxies` (`profile_id` INTEGER NOT NULL, `proxy` TEXT NOT NULL, `selected` TEXT NOT NULL, PRIMARY KEY(`profile_id`, `proxy`), FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
try {
val cursor =
database.query("SELECT name, token, file, active, last_update, id FROM _profiles")
Global.application.filesDir.resolve(Constants.CLASH_DIR).listFiles()?.forEach {
it.deleteRecursively()
}
cursor.moveToFirst()
while (!cursor.isAfterLast) {
// old
// name, token, file, active, last_update, id
val name = cursor.getString(0)
val token = cursor.getString(1)
val file = cursor.getString(2)
val active = cursor.getInt(3)
val lastUpdate = cursor.getLong(4)
val id = cursor.getLong(5)
// new
// name, type, uri, source, active, last_update, update_interval, id
val type = when {
token.startsWith("url") -> ClashProfileEntity.TYPE_URL
token.startsWith("file") -> ClashProfileEntity.TYPE_FILE
else -> ClashProfileEntity.TYPE_UNKNOWN
}
File(file).renameTo(resolveProfile(id))
resolveBase(id).mkdirs()
database.insert("profiles",
SQLiteDatabase.CONFLICT_ABORT,
ContentValues().apply {
put("name", name)
put("type", type)
put("uri", token.removePrefix("url|").removePrefix("file|"))
putNull("source")
put("active", active)
put("last_update", lastUpdate)
put("update_interval", 0)
put("id", id)
})
cursor.moveToNext()
}
cursor.close()
} catch (e: Exception) {
Log.d("Migration profiles failure", e)
}
try {
val cursor =
database.query("SELECT profile_id, proxy, selected FROM _profile_select_proxies ORDER BY id")
cursor.moveToFirst()
while (!cursor.isAfterLast) {
// old
// profile_id, proxy, selected, id
val profileId = cursor.getLong(0)
val proxy: String = cursor.getString(1)
val selected = cursor.getString(2)
// new
// profile_id, proxy, selected
database.insert("profile_select_proxies",
SQLiteDatabase.CONFLICT_REPLACE,
ContentValues().apply {
put("profile_id", profileId)
put("proxy", proxy)
put("selected", selected)
})
cursor.moveToNext()
}
cursor.close()
} catch (e: Exception) {
Log.d("Migration selected failure")
}
database.execSQL("DROP TABLE _profiles")
database.execSQL("DROP TABLE _profile_select_proxies")
// Migration settings
val oldSettings = Global.application
.getSharedPreferences("clash_service", Context.MODE_PRIVATE)
val newSettings = ServiceSettings(
Global.application
.getSharedPreferences(Constants.SERVICE_SETTING_FILE_NAME, Context.MODE_PRIVATE)
)
val accessMode = oldSettings
.getInt("key_access_control_mode", 0)
val accessPackages = oldSettings
.getStringSet("ley_access_control_apps", emptySet())!! // just typo :)
val ipv6Enabled = oldSettings
.getBoolean("key_ipv6_enabled", false)
val dnsHijack = oldSettings
.getBoolean("key_dns_hijacking_enabled", true)
val bypassPrivate = oldSettings
.getBoolean("key_bypass_private_network", true)
newSettings.commit {
val newAccessMode = when (accessMode) {
0 -> ServiceSettings.ACCESS_CONTROL_MODE_ALL
1 -> ServiceSettings.ACCESS_CONTROL_MODE_WHITELIST
2 -> ServiceSettings.ACCESS_CONTROL_MODE_BLACKLIST
else -> ServiceSettings.ACCESS_CONTROL_MODE_ALL
}
put(ServiceSettings.ACCESS_CONTROL_MODE, newAccessMode)
put(ServiceSettings.ACCESS_CONTROL_PACKAGES, accessPackages)
put(ServiceSettings.DNS_HIJACKING, dnsHijack)
put(ServiceSettings.BYPASS_PRIVATE_NETWORK, bypassPrivate)
}
}
override fun migrate(database: SupportSQLiteDatabase) {
try {
process(database)
Log.i("Database Migrated 1 -> 2")
} catch (e: Exception) {
Log.e("Migration failure", e)
}
}
}
}

View File

@@ -1,33 +0,0 @@
package com.github.kr328.clash.service.data
import androidx.room.*
@Dao
interface ClashProfileDao {
@Query("UPDATE profiles SET active = CASE WHEN id = :id THEN 1 ELSE 0 END")
suspend fun setActiveProfile(id: Long)
@Query("SELECT * FROM profiles WHERE active = 1 LIMIT 1")
suspend fun queryActiveProfile(): ClashProfileEntity?
@Query("SELECT * FROM profiles")
suspend fun queryProfiles(): Array<ClashProfileEntity>
@Query("SELECT * FROM profiles WHERE id = :id")
suspend fun queryProfileById(id: Long): ClashProfileEntity?
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun addProfile(profile: ClashProfileEntity): Long
@Update(onConflict = OnConflictStrategy.ABORT)
suspend fun updateProfile(profile: ClashProfileEntity)
@Query("DELETE FROM profiles WHERE id = :id")
suspend fun removeProfile(id: Long)
@Query("SELECT id FROM profiles WHERE rowId = :rowId")
suspend fun getId(rowId: Long): Long
@Query("SELECT IfNull(MAX(id) + 1, 0) AS id FROM profiles")
suspend fun generateNewId(): Long
}

View File

@@ -1,49 +0,0 @@
package com.github.kr328.clash.service.data
import android.os.Parcel
import android.os.Parcelable
import androidx.annotation.Keep
import androidx.room.ColumnInfo
import androidx.room.Entity
import com.github.kr328.clash.core.serialization.Parcels
import kotlinx.serialization.Serializable
@Entity(tableName = "profiles", primaryKeys = ["id"])
@Serializable
@Keep
data class ClashProfileEntity(
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "type") val type: Int,
@ColumnInfo(name = "uri") val uri: String,
@ColumnInfo(name = "source") val source: String?,
@ColumnInfo(name = "active") val active: Boolean,
@ColumnInfo(name = "last_update") val lastUpdate: Long,
@ColumnInfo(name = "update_interval") val updateInterval: Long,
@ColumnInfo(name = "id") val id: Long
) : Parcelable {
override fun writeToParcel(parcel: Parcel, flags: Int) {
Parcels.dump(serializer(), this, parcel)
}
override fun describeContents(): Int {
return 0
}
companion object {
const val TYPE_FILE = 1
const val TYPE_URL = 2
const val TYPE_EXTERNAL = 3
const val TYPE_UNKNOWN = -1
@JvmField
val CREATOR = object : Parcelable.Creator<ClashProfileEntity> {
override fun createFromParcel(parcel: Parcel): ClashProfileEntity {
return Parcels.load(serializer(), parcel)
}
override fun newArray(size: Int): Array<ClashProfileEntity?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -1,18 +0,0 @@
package com.github.kr328.clash.service.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface ClashProfileProxyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun setSelectedForProfile(item: ClashProfileProxyEntity)
@Query("SELECT * FROM profile_select_proxies WHERE profile_id = :id")
suspend fun querySelectedForProfile(id: Long): List<ClashProfileProxyEntity>
@Query("DELETE FROM profile_select_proxies WHERE profile_id = :id AND proxy in (:selected)")
suspend fun removeSelectedForProfile(id: Long, selected: List<String>)
}

View File

@@ -0,0 +1,31 @@
package com.github.kr328.clash.service.data
import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase
import com.github.kr328.clash.common.Global
import androidx.room.Database as DatabaseMetadata
@DatabaseMetadata(
version = 3,
exportSchema = false,
entities = [ProfileEntity::class, SelectedProxyEntity::class]
)
abstract class Database : RoomDatabase() {
abstract fun openProfileDao(): ProfileDao
abstract fun openSelectedProxyDao(): SelectedProxyDao
companion object {
val database = open(Global.application)
fun open(context: Context): Database {
return Room.databaseBuilder(
context.applicationContext,
Database::class.java,
"clash-config"
)
.addMigrations(DatabaseMigrations.VERSION_1_2, DatabaseMigrations.VERSION_2_3)
.build()
}
}
}

View File

@@ -0,0 +1,197 @@
package com.github.kr328.clash.service.data
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import androidx.core.content.edit
import androidx.core.database.getStringOrNull
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.core.utils.Log
import com.github.kr328.clash.service.Constants
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.resolveBaseDir
import com.github.kr328.clash.service.util.resolveProfileFile
import java.io.File
object DatabaseMigrations {
val VERSION_1_2 = object : Migration(1, 2) {
private fun process(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE profiles RENAME TO _profiles")
database.execSQL("ALTER TABLE profile_select_proxies RENAME TO _profile_select_proxies")
database.execSQL("CREATE TABLE IF NOT EXISTS `profiles` (`name` TEXT NOT NULL, `type` INTEGER NOT NULL, `uri` TEXT NOT NULL, `source` TEXT, `active` INTEGER NOT NULL, `last_update` INTEGER NOT NULL, `update_interval` INTEGER NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`id`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `profile_select_proxies` (`profile_id` INTEGER NOT NULL, `proxy` TEXT NOT NULL, `selected` TEXT NOT NULL, PRIMARY KEY(`profile_id`, `proxy`), FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.query("SELECT name, token, file, active, last_update, id FROM _profiles")
.use { cursor ->
Global.application.filesDir.resolve(Constants.CLASH_DIR).listFiles()?.forEach {
it.deleteRecursively()
}
cursor.moveToFirst()
while (!cursor.isAfterLast) {
// old
// name, token, file, active, last_update, id
val name = cursor.getString(0)
val token = cursor.getString(1)
val file = cursor.getString(2)
val active = cursor.getInt(3)
val lastUpdate = cursor.getLong(4)
val id = cursor.getLong(5)
// new
// name, type, uri, source, active, last_update, update_interval, id
val type = when {
token.startsWith("url") -> ProfileEntity.TYPE_URL
token.startsWith("file") -> ProfileEntity.TYPE_FILE
else -> ProfileEntity.TYPE_UNKNOWN
}
File(file).renameTo(Global.application.resolveProfileFile(id))
Global.application.resolveBaseDir(id).mkdirs()
database.insert("profiles",
SQLiteDatabase.CONFLICT_ABORT,
ContentValues().apply {
put("name", name)
put("type", type)
put("uri", token.removePrefix("url|").removePrefix("file|"))
putNull("source")
put("active", active)
put("last_update", lastUpdate)
put("update_interval", 0)
put("id", id)
})
cursor.moveToNext()
}
}
database.query("SELECT profile_id, proxy, selected FROM _profile_select_proxies ORDER BY id")
.use { cursor ->
cursor.moveToFirst()
while (!cursor.isAfterLast) {
// old
// profile_id, proxy, selected, id
val profileId = cursor.getLong(0)
val proxy: String = cursor.getString(1)
val selected = cursor.getString(2)
// new
// profile_id, proxy, selected
database.insert("profile_select_proxies",
SQLiteDatabase.CONFLICT_REPLACE,
ContentValues().apply {
put("profile_id", profileId)
put("proxy", proxy)
put("selected", selected)
})
cursor.moveToNext()
}
}
database.execSQL("DROP TABLE _profiles")
database.execSQL("DROP TABLE _profile_select_proxies")
// Migration settings
val oldSettings = Global.application
.getSharedPreferences("clash_service", Context.MODE_PRIVATE)
val newSettings = ServiceSettings(
Global.application
.getSharedPreferences(Constants.SERVICE_SETTING_FILE_NAME, Context.MODE_PRIVATE)
)
val accessMode = oldSettings
.getInt("key_access_control_mode", 0)
val accessPackages = oldSettings
.getStringSet("ley_access_control_apps", emptySet())!! // just typo :)
val dnsHijack = oldSettings
.getBoolean("key_dns_hijacking_enabled", true)
val bypassPrivate = oldSettings
.getBoolean("key_bypass_private_network", true)
oldSettings.edit {
clear()
}
newSettings.commit {
val newAccessMode = when (accessMode) {
0 -> ServiceSettings.ACCESS_CONTROL_MODE_ALL
1 -> ServiceSettings.ACCESS_CONTROL_MODE_WHITELIST
2 -> ServiceSettings.ACCESS_CONTROL_MODE_BLACKLIST
else -> ServiceSettings.ACCESS_CONTROL_MODE_ALL
}
put(ServiceSettings.ACCESS_CONTROL_MODE, newAccessMode)
put(ServiceSettings.ACCESS_CONTROL_PACKAGES, accessPackages)
put(ServiceSettings.DNS_HIJACKING, dnsHijack)
put(ServiceSettings.BYPASS_PRIVATE_NETWORK, bypassPrivate)
}
}
override fun migrate(database: SupportSQLiteDatabase) {
try {
process(database)
Log.i("Database Migrated 1 -> 2")
} catch (e: Exception) {
Log.e("Migration failure", e)
}
}
}
val VERSION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
try {
database.execSQL("ALTER TABLE profile_select_proxies RENAME TO selected_proxies")
database.execSQL("ALTER TABLE profiles RENAME TO _profiles")
database.execSQL("CREATE TABLE IF NOT EXISTS `profiles` (`name` TEXT NOT NULL, `type` INTEGER NOT NULL, `uri` TEXT NOT NULL, `source` TEXT, `active` INTEGER NOT NULL, `interval` INTEGER NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`id`))")
database.query("SELECT name, type, uri, source, active, update_interval, id FROM _profiles")
.use { cursor ->
Global.application.filesDir.resolve(Constants.CLASH_DIR).listFiles()
?.forEach {
it.deleteRecursively()
}
cursor.moveToFirst()
while (!cursor.isAfterLast) {
// old
// name, type, uri, source, active, last_update, update_interval(seconds), id
// new
// name, type, uri, source, active, interval(millis seconds), id
val name = cursor.getString(0)
val type = cursor.getInt(1)
val uri = cursor.getString(2)
val source = cursor.getStringOrNull(3)
val active = cursor.getInt(4)
val interval = cursor.getInt(5)
val id = cursor.getLong(6)
database.insert("profiles",
SQLiteDatabase.CONFLICT_ABORT,
ContentValues().apply {
put("name", name)
put("type", type)
put("uri", uri)
put("source", source)
put("active", active)
put("interval", interval * 1000)
put("id", id)
})
cursor.moveToNext()
}
}
database.execSQL("DROP TABLE _profiles")
} catch (e: Exception) {
Log.e("Migration failure", e)
}
}
}
}

View File

@@ -0,0 +1,32 @@
package com.github.kr328.clash.service.data
import androidx.room.*
@Dao
interface ProfileDao {
@Query("UPDATE profiles SET active = CASE WHEN id = :id THEN 1 ELSE 0 END")
suspend fun setActive(id: Long)
@Query("SELECT * FROM profiles WHERE active = 1 LIMIT 1")
suspend fun queryActive(): ProfileEntity?
@Query("SELECT * FROM profiles")
suspend fun queryAll(): List<ProfileEntity>
@Query("SELECT * FROM profiles WHERE id = :id")
suspend fun queryById(id: Long): ProfileEntity?
@Query("SELECT id FROM profiles")
suspend fun queryAllIds(): List<Long>
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insert(profile: ProfileEntity): Long
@Update(onConflict = OnConflictStrategy.ABORT)
suspend fun update(profile: ProfileEntity)
@Query("DELETE FROM profiles WHERE id = :id")
suspend fun remove(id: Long)
companion object : ProfileDao by Database.database.openProfileDao()
}

View File

@@ -0,0 +1,24 @@
package com.github.kr328.clash.service.data
import androidx.annotation.Keep
import androidx.room.ColumnInfo
import androidx.room.Entity
@Entity(tableName = "profiles", primaryKeys = ["id"])
@Keep
data class ProfileEntity(
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "type") val type: Int,
@ColumnInfo(name = "uri") val uri: String,
@ColumnInfo(name = "source") val source: String?,
@ColumnInfo(name = "active") val active: Boolean,
@ColumnInfo(name = "interval") val interval: Long,
@ColumnInfo(name = "id") val id: Long
) {
companion object {
const val TYPE_FILE = 1
const val TYPE_URL = 2
const val TYPE_EXTERNAL = 3
const val TYPE_UNKNOWN = -1
}
}

View File

@@ -0,0 +1,20 @@
package com.github.kr328.clash.service.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface SelectedProxyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun setSelectedForProfile(item: SelectedProxyEntity)
@Query("SELECT * FROM selected_proxies WHERE profile_id = :id")
suspend fun querySelectedForProfile(id: Long): List<SelectedProxyEntity>
@Query("DELETE FROM selected_proxies WHERE profile_id = :id AND proxy in (:selected)")
suspend fun removeSelectedForProfile(id: Long, selected: List<String>)
companion object : SelectedProxyDao by Database.database.openSelectedProxyDao()
}

View File

@@ -5,9 +5,9 @@ import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(
tableName = "profile_select_proxies",
tableName = "selected_proxies",
foreignKeys = [ForeignKey(
entity = ClashProfileEntity::class,
entity = ProfileEntity::class,
childColumns = ["profile_id"],
parentColumns = ["id"],
onDelete = ForeignKey.CASCADE,
@@ -15,7 +15,7 @@ import androidx.room.ForeignKey
)],
primaryKeys = ["profile_id", "proxy"]
)
data class ClashProfileProxyEntity(
data class SelectedProxyEntity(
@ColumnInfo(name = "profile_id") val profileId: Long,
@ColumnInfo(name = "proxy") val proxy: String,
@ColumnInfo(name = "selected") val selected: String

View File

@@ -4,8 +4,8 @@ import android.content.Context
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import com.github.kr328.clash.service.R
import com.github.kr328.clash.service.util.resolveBase
import com.github.kr328.clash.service.util.resolveProfile
import com.github.kr328.clash.service.util.resolveBaseDir
import com.github.kr328.clash.service.util.resolveProfileFile
import java.io.FileNotFoundException
class ProfileDirectoryResolver(private val context: Context) {
@@ -14,7 +14,7 @@ class ProfileDirectoryResolver(private val context: Context) {
const val FILE_NAME_PROVIDER = "providers"
}
private val nextResolver = ProviderResolver()
private val nextResolver = ProviderResolver(context)
fun resolve(id: Long, paths: List<String>): VirtualFile {
if (paths.size == 1) {
@@ -35,7 +35,7 @@ class ProfileDirectoryResolver(private val context: Context) {
}
override fun size(): Long {
return resolveProfile(id).length()
return context.resolveProfileFile(id).length()
}
override fun mimeType(): String {
@@ -51,7 +51,7 @@ class ProfileDirectoryResolver(private val context: Context) {
override fun listFiles(): List<String> {
if (paths[0] == FILE_NAME_PROVIDER) {
return resolveBase(id).list()?.toList() ?: emptyList()
return context.resolveBaseDir(id).list()?.toList() ?: emptyList()
}
return emptyList()
@@ -59,7 +59,7 @@ class ProfileDirectoryResolver(private val context: Context) {
override fun openFile(mode: Int): ParcelFileDescriptor {
return if (paths[0] == FILE_NAME_CONFIG)
ParcelFileDescriptor.open(resolveProfile(id), mode)
ParcelFileDescriptor.open(context.resolveProfileFile(id), mode)
else
throw UnsupportedOperationException()
}

View File

@@ -4,20 +4,17 @@ import android.content.Context
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import com.github.kr328.clash.service.R
import com.github.kr328.clash.service.data.ClashDatabase
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.data.ProfileDao
import com.github.kr328.clash.service.util.resolveProfileFile
import java.io.FileNotFoundException
class ProfilesResolver(private val context: Context, private val database: ClashDatabase) {
class ProfilesResolver(private val context: Context) {
private val nextResolver = ProfileDirectoryResolver(context)
suspend fun resolve(paths: List<String>): VirtualFile {
return when (paths.size) {
0 -> {
val files = database.openClashProfileDao()
.queryProfiles()
.map(ClashProfileEntity::id)
.map(Long::toString)
val files = ProfileDao.queryAllIds().map(Long::toString)
object : VirtualFile {
override fun name(): String {
@@ -47,7 +44,7 @@ class ProfilesResolver(private val context: Context, private val database: Clash
}
1 -> {
val profile = paths[0].toLongOrNull()?.let {
database.openClashProfileDao().queryProfileById(it)
ProfileDao.queryById(it)
} ?: throw FileNotFoundException()
object : VirtualFile {
@@ -56,7 +53,7 @@ class ProfilesResolver(private val context: Context, private val database: Clash
}
override fun lastModified(): Long {
return profile.lastUpdate
return context.resolveProfileFile(profile.id).lastModified()
}
override fun size(): Long {

View File

@@ -1,13 +1,14 @@
package com.github.kr328.clash.service.files
import android.content.Context
import android.os.ParcelFileDescriptor
import com.github.kr328.clash.service.util.resolveBase
import com.github.kr328.clash.service.util.resolveBaseDir
import java.io.FileNotFoundException
import java.net.URLDecoder
class ProviderResolver {
class ProviderResolver(private val context: Context) {
fun resolve(id: Long, fileName: String): VirtualFile {
val file = resolveBase(id).resolve(fileName)
val file = context.resolveBaseDir(id).resolve(fileName)
if (!file.exists())
throw FileNotFoundException()

View File

@@ -0,0 +1,46 @@
package com.github.kr328.clash.service.model
import android.content.Context
import android.net.Uri
import com.github.kr328.clash.service.data.ProfileEntity
import com.github.kr328.clash.service.util.resolveProfileFile
fun ProfileEntity.toProfileMetadata(context: Context): ProfileMetadata {
val type = when (this.type) {
ProfileEntity.TYPE_FILE -> ProfileMetadata.Type.FILE
ProfileEntity.TYPE_URL -> ProfileMetadata.Type.URL
ProfileEntity.TYPE_EXTERNAL -> ProfileMetadata.Type.EXTERNAL
else -> ProfileMetadata.Type.EXTERNAL
}
val lastModified = context.resolveProfileFile(id).lastModified()
return ProfileMetadata(
id = id,
name = name,
type = type,
uri = Uri.parse(uri),
source = Uri.parse(source),
active = active,
interval = interval,
lastModified = lastModified
)
}
fun ProfileMetadata.toProfileEntity(): ProfileEntity {
val type = when (this.type) {
ProfileMetadata.Type.FILE -> ProfileEntity.TYPE_FILE
ProfileMetadata.Type.URL -> ProfileEntity.TYPE_URL
ProfileMetadata.Type.EXTERNAL -> ProfileEntity.TYPE_EXTERNAL
ProfileMetadata.Type.UNKNOWN -> ProfileEntity.TYPE_UNKNOWN
}
return ProfileEntity(
name = name,
type = type,
uri = uri.toString(),
source = source.toString(),
active = active,
interval = interval,
id = id
)
}

View File

@@ -0,0 +1,23 @@
@file:UseSerializers(UriSerializer::class)
package com.github.kr328.clash.service.model
import android.net.Uri
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
@Serializable
data class ProfileMetadata(
val id: Long,
val name: String,
val type: Type,
val uri: Uri,
val source: Uri?,
val active: Boolean,
val interval: Long,
val lastModified: Long
) {
enum class Type {
FILE, URL, EXTERNAL, UNKNOWN
}
}

View File

@@ -0,0 +1,17 @@
package com.github.kr328.clash.service.model
import android.net.Uri
import kotlinx.serialization.*
class UriSerializer : KSerializer<Uri> {
override val descriptor: SerialDescriptor
get() = PrimitiveDescriptor("Uri", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Uri {
return Uri.parse(decoder.decodeString())
}
override fun serialize(encoder: Encoder, value: Uri) {
encoder.encodeString(value.toString())
}
}

View File

@@ -3,7 +3,6 @@ package com.github.kr328.clash.service.util
import android.content.Context
import android.content.Intent
import com.github.kr328.clash.common.ids.Intents
import com.github.kr328.clash.core.Global
fun Context.sendBroadcastSelf(intent: Intent) {
this.sendBroadcast(intent.setPackage(this.packageName))
@@ -37,10 +36,3 @@ fun Context.broadcastClashStopped(reason: String?) {
)
)
}
fun Intent.enforceSelfPackage(block: () -> Unit) {
if (`package` != Global.application.packageName)
return
block()
}

View File

@@ -2,7 +2,7 @@ package com.github.kr328.clash.service.util
import android.content.ComponentName
import android.content.Intent
import com.github.kr328.clash.core.Global
import com.github.kr328.clash.common.Global
import kotlin.reflect.KClass
val KClass<*>.componentName: ComponentName

View File

@@ -1,13 +1,13 @@
package com.github.kr328.clash.service.util
import com.github.kr328.clash.core.Global
import android.content.Context
import com.github.kr328.clash.service.Constants
import java.io.File
fun resolveProfile(id: Long): File {
return Global.application.filesDir.resolve(Constants.PROFILES_DIR).resolve("$id.yaml")
fun Context.resolveProfileFile(id: Long): File {
return filesDir.resolve(Constants.PROFILES_DIR).resolve("$id.yaml")
}
fun resolveBase(id: Long): File {
return Global.application.filesDir.resolve(Constants.CLASH_DIR).resolve(id.toString())
fun Context.resolveBaseDir(id: Long): File {
return filesDir.resolve(Constants.CLASH_DIR).resolve(id.toString())
}

View File

@@ -1,25 +0,0 @@
package com.github.kr328.clash.service.util
import java.io.File
import java.security.SecureRandom
import kotlin.math.absoluteValue
object RandomUtils {
private val random = SecureRandom()
fun fileName(dir: File, suffix: String = ""): String {
dir.mkdirs()
var fileName: String
do {
fileName = random.nextLong().absoluteValue.toString() + suffix
} while (dir.resolve(fileName).exists())
return fileName
}
fun nextInt(): Int {
return random.nextInt()
}
}

View File

@@ -1,28 +0,0 @@
package com.github.kr328.clash.service.util
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import com.github.kr328.clash.common.ids.Intents
import com.github.kr328.clash.service.ProfileRequestReceiver
import com.github.kr328.clash.service.data.ClashProfileEntity
object UpdateUtils {
fun resetProfileUpdateAlarm(context: Context, profile: ClashProfileEntity) {
if (profile.updateInterval > 0) {
requireNotNull(context.getSystemService(AlarmManager::class.java)).set(
AlarmManager.RTC,
profile.lastUpdate + (profile.updateInterval * 1000),
PendingIntent.getBroadcast(
context,
RandomUtils.nextInt(),
Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST)
.setComponent(ProfileRequestReceiver::class.componentName)
.putExtra(Intents.INTENT_EXTRA_PROFILE_ID, profile.id),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
}
}
}