mirror of
https://github.com/MetaCubeX/ClashMetaForAndroid.git
synced 2026-05-09 18:11:26 +08:00
service refactored
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.github.kr328.clash.common
|
||||
|
||||
object Permissions {
|
||||
val PERMISSION_ACCESS_CLASH: String
|
||||
get() = Global.application.packageName + ".permission.RECEIVE_BROADCASTS"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.github.kr328.clash.common.ids
|
||||
|
||||
object PendingIds {
|
||||
fun generateProfileResultId(profileId: Long): Int {
|
||||
return NotificationIds.generateProfileResultId(profileId)
|
||||
}
|
||||
}
|
||||
5
common/src/main/res/values/strings.xml
Normal file
5
common/src/main/res/values/strings.xml
Normal 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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package com.github.kr328.clash.service.data;
|
||||
|
||||
parcelable ClashProfileEntity;
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.github.kr328.clash.service.model;
|
||||
|
||||
parcelable ProfileMetadata;
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user