Compare commits

..

36 Commits
1.1.3 ... 1.1.5

Author SHA1 Message Date
Kr328
d8b1c0b7b8 remove debug symbol of clash core 2020-02-26 14:58:29 +08:00
Kr328
8bf908b731 remove default disallow package 2020-02-26 14:53:30 +08:00
Kr328
568f1f12e7 fix crash on ipv6 enable & reduce provider call 2020-02-26 14:06:46 +08:00
Kr328
ca739a5e2f add abi split 2020-02-26 13:53:42 +08:00
Kr328
7833f747d5 format & cleanup 2020-02-26 13:33:44 +08:00
Kr328
977519b383 change crash reporter
fix crash on log polling
fix crash on udp write back
fix goroutine leak
fix default rule inject
2020-02-26 13:32:29 +08:00
Kr328
b39b3812d0 add crash reporter 2020-02-25 20:53:39 +08:00
Kr328
f7dddb41fa add crash reporter 2020-02-25 20:53:26 +08:00
Kr328
053300919c add crash reporter 2020-02-25 20:38:55 +08:00
Kr328
93dff86383 clear dumper 2020-02-25 14:42:06 +08:00
Kr328
1b7e9c6b12 add golang crash dumper 2020-02-25 14:41:53 +08:00
Kr328
f072cd0839 update dependencies 2020-02-25 12:43:58 +08:00
Kr328
f626a0fc5c fix ipv6 support
cc840c9fdd (diff-e089fe63dcb3674c0a1e459a95508e3e)
2020-02-24 20:33:29 +08:00
Kr328
63da12f046 1.1.4 2020-02-23 23:50:41 +08:00
Kr328
ce1beafb08 fix proxies active cache 2020-02-23 23:22:32 +08:00
Kr328
f6ecc5c953 remove reduce recreate call 2020-02-23 22:55:10 +08:00
Kr328
de87e3fd74 add crash log 2020-02-23 22:54:13 +08:00
Kr328
e34a7a3081 update version 2020-02-22 23:42:28 +08:00
Kr328
ecb1678e48 fix not start vpn after request 2020-02-22 23:41:34 +08:00
Kr328
ca395033a5 update clash core 2020-02-22 23:27:42 +08:00
Kr328
8095451e14 remove testing code 2020-02-22 23:27:00 +08:00
Kr328
c0435ead01 fix tun leak 2020-02-22 23:23:50 +08:00
Kr328
d346344e17 simple ui adjust 2020-02-22 23:23:22 +08:00
Kr328
68d6af4255 fix notification cancel delay 2020-02-22 22:24:45 +08:00
Kr328
253d465d32 Merge pull request #135 from tsln1998/fix_windows_build
fix source path on windows
2020-02-22 21:57:16 +08:00
Kr328
7ab7c8abee fix receiver leak 2020-02-22 21:43:28 +08:00
Kr328
ea3aee0883 fix proxy only not load profile default 2020-02-22 21:19:33 +08:00
Kr328
b4a5d7b1be Use single service to handle clash 2020-02-22 21:17:29 +08:00
tsln1998
028346713e fix source path on windows 2020-02-22 18:15:40 +08:00
Kr328
d519bc1a23 update README.md 2020-02-22 01:48:38 +08:00
Kr328
a6d5fbc2ca update PRIVACY_POLICY.md 2020-02-22 01:46:45 +08:00
Kr328
e9a6bc2fbc add delay for proxies 2020-02-22 01:13:15 +08:00
Kr328
ef42612c4b change launchMode & fix tile icon size 2020-02-22 00:51:04 +08:00
Kr328
f4b421c953 1.1.1 2020-02-22 00:35:25 +08:00
Kr328
14aa981ca4 update base version 2020-02-20 23:33:50 +08:00
Kr328
515942274d bug fix 2020-02-20 23:33:30 +08:00
102 changed files with 1174 additions and 806 deletions

View File

@@ -17,7 +17,7 @@ The app does use third party services that may collect information used to ident
Link to privacy policy of third party service providers used by the app
* [Google Play Services](https://www.google.com/policies/privacy/)
* [Firebase Analytics](https://firebase.google.com/policies/analytics)
* [AppCenter](https://docs.microsoft.com/en-us/appcenter/gdpr/)
**Log Data**

View File

@@ -39,7 +39,7 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
git submodule update --init --recursive
```
2. Install `Android SDK` ,`Android NDK` and `Golang`
2. Install `Android SDK (include JDK)` ,`Android NDK` and `Golang`
3. Configure `local.properties`

View File

@@ -17,6 +17,7 @@ android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
@@ -27,6 +28,12 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}
splits {
abi {
enable true
universalApk true
}
}
}
dependencies {

View File

@@ -20,7 +20,7 @@
android:name=".MainActivity"
android:label="@string/launch_name"
android:configChanges="uiMode"
android:launchMode="singleTask">
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -65,7 +65,7 @@
android:label="@string/log_viewer"
android:exported="false"
android:configChanges="uiMode"
android:launchMode="singleTask"/>
android:launchMode="singleTop"/>
<activity
android:name=".SettingsActivity"
android:label="@string/settings"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -13,8 +13,10 @@ class ApkBrokenActivity : BaseActivity() {
setContentView(R.layout.activity_application_broken)
setSupportActionBar(toolbar)
text.text = Html.fromHtml(getString(R.string.application_broken_description),
Html.FROM_HTML_MODE_COMPACT)
text.text = Html.fromHtml(
getString(R.string.application_broken_description),
Html.FROM_HTML_MODE_COMPACT
)
commonUi.build {
option(
@@ -22,8 +24,10 @@ class ApkBrokenActivity : BaseActivity() {
title = getString(R.string.learn_more_about_split_apks)
) {
onClick {
startActivity(Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.about_split_apks_url))))
startActivity(
Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.about_split_apks_url)))
)
}
}
option(
@@ -31,8 +35,10 @@ class ApkBrokenActivity : BaseActivity() {
title = getString(R.string.reinstall_from_google_play)
) {
onClick {
startActivity(Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.google_play_url))))
startActivity(
Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.google_play_url)))
)
}
}
option(
@@ -40,8 +46,10 @@ class ApkBrokenActivity : BaseActivity() {
title = getString(R.string.download_from_github_releases)
) {
onClick {
startActivity(Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.github_releases_url))))
startActivity(
Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.github_releases_url)))
)
}
}
}

View File

@@ -15,13 +15,12 @@ import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.util.createLanguageConfigurationContext
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.util.*
abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() {
class EmptyBroadcastReceiver : BroadcastReceiver() {
@@ -47,9 +46,9 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope()
}
}
override fun onProfileLoaded(profileEntity: ClashProfileEntity) {
override fun onProfileLoaded() {
launch {
onClashProfileLoaded(profileEntity)
onClashProfileLoaded()
}
}
}
@@ -69,7 +68,7 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope()
open suspend fun onClashStarted() {}
open suspend fun onClashStopped(reason: String?) {}
open suspend fun onClashProfileChanged() {}
open suspend fun onClashProfileLoaded(profile: ClashProfileEntity) {}
open suspend fun onClashProfileLoaded() {}
override fun setContentView(layoutResID: Int) {
val base = CoordinatorLayout(this).apply {
@@ -99,19 +98,7 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope()
language = uiSettings.get(UiSettings.LANGUAGE)
val languageOverride = language.split("-")
if (language.isEmpty())
return super.attachBaseContext(base)
val configuration = Configuration()
val localeOverride = if (languageOverride.size == 2)
Locale(languageOverride[0], languageOverride[1])
else
Locale(languageOverride[0])
configuration.setLocale(localeOverride)
super.attachBaseContext(base.createConfigurationContext(configuration))
super.attachBaseContext(base.createLanguageConfigurationContext(language))
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -127,12 +114,8 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope()
override fun onStart() {
super.onStart()
if (language != uiSettings.get(UiSettings.LANGUAGE))
if (language != uiSettings.get(UiSettings.LANGUAGE) || darkMode != uiSettings.get(UiSettings.DARK_MODE))
recreate()
if (darkMode != uiSettings.get(UiSettings.DARK_MODE)) {
resetDarkMode()
recreate()
}
Broadcasts.register(receiver)
}
@@ -204,7 +187,7 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope()
}
private fun resetDarkMode() {
when ( uiSettings.get(UiSettings.DARK_MODE).also { darkMode = it } ) {
when (uiSettings.get(UiSettings.DARK_MODE).also { darkMode = it }) {
UiSettings.DARK_MODE_AUTO ->
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
UiSettings.DARK_MODE_DARK ->

View File

@@ -70,7 +70,7 @@ class LogViewerActivity : BaseActivity() {
super.onStart()
launch {
if ( pauseMutex.isLocked )
if (pauseMutex.isLocked)
pauseMutex.unlock()
}
}
@@ -99,13 +99,12 @@ class LogViewerActivity : BaseActivity() {
.split("\n")
.parallelStream()
.map { it.trim() }
.filter { it.isNotEmpty() && !it.startsWith("#")}
.filter { it.isNotEmpty() && !it.startsWith("#") }
.map { it.split(" ", limit = 3) }
.filter { it.size == 3 }
.map { LogEvent(LogEvent.Level.valueOf(it[1]), it[2], it[0].toLong()) }
.toList()
}
catch (e: Exception) {
} catch (e: Exception) {
makeSnackbarException(getString(R.string.open_log_failure), e.message)
throw CancellationException()
@@ -130,7 +129,7 @@ class LogViewerActivity : BaseActivity() {
(mainList.adapter as LiveLogAdapter).insertItems(response.logs)
mainList.apply {
if ( computeVerticalScrollOffset() < 30 )
if (computeVerticalScrollOffset() < 30)
scrollToPosition(0)
}

View File

@@ -18,10 +18,12 @@ import androidx.core.app.NotificationManagerCompat
import com.github.kr328.clash.core.event.LogEvent
import com.github.kr328.clash.core.utils.Log
import com.github.kr328.clash.model.LogFile
import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.service.ClashManagerService
import com.github.kr328.clash.service.IClashManager
import com.github.kr328.clash.service.ipc.IStreamCallback
import com.github.kr328.clash.service.ipc.ParcelableContainer
import com.github.kr328.clash.service.util.createLanguageConfigurationContext
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.utils.format
import com.github.kr328.clash.utils.logsDir
@@ -38,6 +40,7 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
private const val NOTIFICATION_CHANNEL_ID = "clash_logcat_channel"
private const val NOTIFICATION_ID = 256
private const val MAX_CACHE_COUNT = 200
private const val LOG_LISTENER_KEY = "logcat_service"
private const val LOG_CONTENT_FORMAT = "%d %s %s"
@@ -54,27 +57,23 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
private val entity = LogFile.generate()
private val connection = object : ServiceConnection {
private var manager: IClashManager? = null
override fun onServiceDisconnected(name: ComponentName?) {
stopSelf()
manager?.unregisterLogListener(LOG_LISTENER_KEY)
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val manager = IClashManager.Stub.asInterface(service) ?: return stopSelf()
manager.openLogEvent(object : IStreamCallback.Stub() {
override fun complete() {
stopSelf()
}
override fun completeExceptionally(reason: String?) {
stopSelf()
}
manager = IClashManager.Stub.asInterface(service) ?: return stopSelf()
manager?.registerLogListener(LOG_LISTENER_KEY, object : IStreamCallback.Stub() {
override fun complete() {}
override fun completeExceptionally(reason: String?) {}
override fun send(data: ParcelableContainer?) {
val logEvent = (data?.data as LogEvent?) ?: return
data ?: return
data.data ?: return
if (!logChannel.offer(logEvent))
Log.w("Drop log $logEvent")
logChannel.offer(data.data as LogEvent)
}
})
}
@@ -100,6 +99,8 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
cancel()
connection.onServiceDisconnected(null)
stopForeground(true)
super.onDestroy()
@@ -119,6 +120,14 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
}
}
override fun attachBaseContext(base: Context?) {
val b = base ?: return super.attachBaseContext(base)
val language = UiSettings(b).get(UiSettings.LANGUAGE)
super.attachBaseContext(b.createLanguageConfigurationContext(language))
}
// Export to UI
suspend fun pollLogEvent(offset: Long): CompletableDeferred<Response> {
val request = Request(offset, CompletableDeferred())
@@ -171,8 +180,7 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
}
}
}
}
catch (e: Exception) {
} catch (e: Exception) {
return@launch
}
}
@@ -239,6 +247,7 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
.setColor(getColor(R.color.colorAccentService))
.setContentTitle(getString(R.string.clash_logcat))
.setContentText(getString(R.string.running))
.setGroup(NOTIFICATION_CHANNEL_ID)
.setContentIntent(
PendingIntent.getActivity(
this,

View File

@@ -40,7 +40,7 @@ class LogsActivity : BaseActivity() {
setSupportActionBar(toolbar)
if ( LogcatService.isServiceRunning ) {
if (LogcatService.isServiceRunning) {
startActivity(LogViewerActivity::class.intent)
finish()
return
@@ -67,12 +67,16 @@ class LogsActivity : BaseActivity() {
showClearAllDialog()
}
val adapter = LogFileAdapter(this@LogsActivity,
val adapter = LogFileAdapter(
this@LogsActivity,
onItemClicked = {
startActivity(LogViewerActivity::class.intent
.setData(Uri.fromFile(logsDir.resolve(it.fileName))))
startActivity(
LogViewerActivity::class.intent
.setData(Uri.fromFile(logsDir.resolve(it.fileName)))
)
},
onMenuClicked = this::showMenu)
onMenuClicked = this::showMenu
)
val layoutManager = LinearLayoutManager(this@LogsActivity)
mainList.layoutManager = layoutManager
@@ -82,7 +86,7 @@ class LogsActivity : BaseActivity() {
override fun onStart() {
super.onStart()
if ( LogcatService.isServiceRunning )
if (LogcatService.isServiceRunning)
return
refreshList()
@@ -92,8 +96,8 @@ class LogsActivity : BaseActivity() {
get() = getText(R.string.logs)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if ( requestCode == REQUEST_CODE ) {
if ( resultCode == Activity.RESULT_OK ) {
if (requestCode == REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK) {
val url = data?.data ?: return
val file = lastWriteFile ?: return
@@ -135,8 +139,11 @@ class LogsActivity : BaseActivity() {
val old = adapter.fileList
val result = withContext(Dispatchers.Default) {
DiffUtil.calculateDiff(object: DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun areItemsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
return old[oldItemPosition].fileName == files[newItemPosition].fileName
}
@@ -188,7 +195,8 @@ class LogsActivity : BaseActivity() {
menu.build {
option(
icon = getDrawable(R.drawable.ic_save),
title = getString(R.string.export)) {
title = getString(R.string.export)
) {
onClick {
export(logFile)
@@ -197,7 +205,8 @@ class LogsActivity : BaseActivity() {
}
option(
icon = getDrawable(R.drawable.ic_delete_colorful),
title = getString(R.string.delete)) {
title = getString(R.string.delete)
) {
textColor = errorColor
onClick {
@@ -224,7 +233,7 @@ class LogsActivity : BaseActivity() {
}
private fun export(file: LogFile) {
if ( lastWriteFile != null )
if (lastWriteFile != null)
return
val d = Date(file.date)

View File

@@ -12,11 +12,9 @@ import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.utils.asBytesString
import com.github.kr328.clash.remote.withClash
import com.github.kr328.clash.remote.withProfile
import com.github.kr328.clash.service.ClashService
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.service.util.startForegroundServiceCompat
import com.github.kr328.clash.utils.startClashService
import com.github.kr328.clash.utils.stopClashService
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
@@ -34,7 +32,7 @@ class MainActivity : BaseActivity() {
status.setOnClickListener {
if (clashRunning) {
stopService(ClashService::class.intent)
stopClashService()
} else {
val vpnRequest = startClashService()
if (vpnRequest != null)
@@ -84,7 +82,7 @@ class MainActivity : BaseActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK)
startForegroundServiceCompat(ClashService::class.intent)
startClashService()
return
}
@@ -102,7 +100,7 @@ class MainActivity : BaseActivity() {
makeSnackbarException(getString(R.string.clash_start_failure), reason)
}
override suspend fun onClashProfileLoaded(profile: ClashProfileEntity) {
override suspend fun onClashProfileLoaded() {
updateClashStatus()
}

View File

@@ -3,11 +3,15 @@ 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.dump.LogcatDumper
import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.remote.Remote
import com.microsoft.appcenter.AppCenter
import com.microsoft.appcenter.analytics.Analytics
import com.microsoft.appcenter.crashes.AbstractCrashesListener
import com.microsoft.appcenter.crashes.Crashes
import com.microsoft.appcenter.crashes.ingestion.models.ErrorAttachmentLog
import com.microsoft.appcenter.crashes.model.ErrorReport
@Suppress("unused")
class MainApplication : Application() {
@@ -27,6 +31,21 @@ class MainApplication : Application() {
BuildConfig.APP_CENTER_KEY,
Analytics::class.java, Crashes::class.java
)
Crashes.setListener(object : AbstractCrashesListener() {
override fun getErrorAttachments(report: ErrorReport?): MutableIterable<ErrorAttachmentLog> {
report ?: return mutableListOf()
if (!report.stackTrace.contains("DeadObjectException"))
return mutableListOf()
val logcat = LogcatDumper.dump().joinToString(separator = "\n")
return mutableListOf(
ErrorAttachmentLog.attachmentWithText(logcat, "logcat.txt")
)
}
})
}
Remote.init(this)

View File

@@ -9,13 +9,15 @@ import com.github.kr328.clash.service.util.componentName
import com.github.kr328.clash.service.util.startForegroundServiceCompat
import com.github.kr328.clash.utils.startClashService
class OnBootReceiver: BroadcastReceiver() {
class OnBootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if ( intent?.action != Intent.ACTION_BOOT_COMPLETED || context == null )
if (intent?.action != Intent.ACTION_BOOT_COMPLETED || context == null)
return
context.startClashService()
context.startForegroundServiceCompat(Intent(Intents.INTENT_ACTION_PROFILE_SETUP)
.setComponent(ProfileBackgroundService::class.componentName))
context.startForegroundServiceCompat(
Intent(Intents.INTENT_ACTION_PROFILE_SETUP)
.setComponent(ProfileBackgroundService::class.componentName)
)
}
}

View File

@@ -141,7 +141,7 @@ class ProfileEditActivity : BaseActivity() {
val name = requireElement<TextInput>(KEY_NAME).content.toString()
val url = Uri.parse(requireElement<TextInput>(KEY_URL).content.toString())
val interval = requireElement<TextInput>(KEY_AUTO_UPDATE).content.toString()
.toLongOrNull()?: 0
.toLongOrNull() ?: 0
if (name.isBlank()) {
Snackbar.make(rootView, R.string.empty_name, Snackbar.LENGTH_LONG).show()

View File

@@ -9,27 +9,22 @@ import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.adapter.ProxyAdapter
import com.github.kr328.clash.adapter.ProxyChipAdapter
import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.model.Proxy
import com.github.kr328.clash.pipeline.Pipeline
import com.github.kr328.clash.pipeline.mergePrefix
import com.github.kr328.clash.pipeline.sort
import com.github.kr328.clash.pipeline.toAdapterElement
import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.remote.withClash
import com.github.kr328.clash.utils.PrefixMerger
import com.github.kr328.clash.utils.ProxySorter
import com.github.kr328.clash.utils.ScrollBinding
import kotlinx.android.synthetic.main.activity_proxies.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.sync.Mutex
class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
private val refreshMutex = Mutex()
private val scrollBinding = ScrollBinding(this, this)
private val doScrollToLastProxy by lazy {
val selected = uiSettings.get(UiSettings.PROXY_LAST_SELECT_GROUP)
launch {
scrollBinding.scrollMaster(selected)
}
}
private var scrollToLast = true
private val mainListAdapter: ProxyAdapter
get() = mainList.adapter as ProxyAdapter
@@ -42,35 +37,10 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
setContentView(R.layout.activity_proxies)
setSupportActionBar(toolbar)
mainList.adapter = ProxyAdapter(this, { group, proxy ->
launch {
withClash {
setSelectProxy(group, proxy)
}
}
}, {
launch {
urlTesting.add(it)
withClash {
urlTesting.add(it)
startHealthCheck(it)
urlTesting.remove(it)
refreshList()
}
}
})
mainList.adapter = ProxyAdapter(this, this::setGroupSelected, this::startUrlTesting)
mainList.layoutManager = mainListAdapter.layoutManager
chipList.adapter = ProxyChipAdapter(this) {
launch {
scrollBinding.scrollMaster(it)
}
}
chipList.adapter = ProxyChipAdapter(this, this::chipClicked)
chipList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
chipList.itemAnimator?.changeDuration = 0
@@ -89,8 +59,10 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
override fun onStop() {
uiSettings.commit {
put(UiSettings.PROXY_LAST_SELECT_GROUP,
(chipList.adapter!! as ProxyChipAdapter).selected)
put(
UiSettings.PROXY_LAST_SELECT_GROUP,
(chipList.adapter!! as ProxyChipAdapter).selected
)
}
super.onStop()
@@ -117,6 +89,8 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
}
launch {
var scrollTop = false
when (item.itemId) {
R.id.modeDirect -> {
withClash {
@@ -126,6 +100,8 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
R.id.modeGlobal -> {
withClash {
setProxyMode(General.Mode.GLOBAL)
scrollTop = true
}
}
R.id.modeRule -> {
@@ -179,7 +155,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
item.isChecked = true
refreshList()
refreshList(scrollTop)
}
return true
@@ -188,7 +164,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
override val activityLabel: CharSequence?
get() = getText(R.string.proxy)
override suspend fun onClashStarted() {
override suspend fun onClashStopped(reason: String?) {
finish()
}
@@ -230,8 +206,39 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
}
}
private fun refreshList() {
private fun setGroupSelected(group: String, select: String) {
launch {
withClash {
setSelectProxy(group, select)
}
}
}
private fun startUrlTesting(group: String) {
launch {
urlTesting.add(group)
withClash {
startHealthCheck(group)
}
urlTesting.remove(group)
refreshList()
}
}
private fun chipClicked(name: String) {
launch {
scrollBinding.scrollMaster(name)
}
}
private fun refreshList(scrollTop: Boolean = false) {
launch {
if (!refreshMutex.tryLock())
return@launch
val general = withClash {
queryGeneral()
}
@@ -239,90 +246,31 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
queryAllProxyGroups()
}
val prefixDeferred = async {
if (uiSettings.get(UiSettings.PROXY_MERGE_PREFIX)) {
proxies.map {
async { PrefixMerger.merge(it.proxies.map { p -> it.name to p.name }) { it.second } }
}.flatMap {
it.await()
}.map {
it.value to it
}.toMap()
} else emptyMap()
}
val merged = Pipeline(proxies, uiSettings).mergePrefix()
val sorted = Pipeline(proxies, uiSettings).sort()
val sortDeferred = async {
val groupSort = when (uiSettings.get(UiSettings.PROXY_GROUP_SORT)) {
UiSettings.PROXY_SORT_DEFAULT ->
ProxySorter.Order.DEFAULT
UiSettings.PROXY_SORT_NAME ->
ProxySorter.Order.NAME_INCREASE
UiSettings.PROXY_SORT_DELAY ->
ProxySorter.Order.DELAY_INCREASE
else -> throw IllegalArgumentException()
}
val proxySort = when (uiSettings.get(UiSettings.PROXY_PROXY_SORT)) {
UiSettings.PROXY_SORT_DEFAULT ->
ProxySorter.Order.DEFAULT
UiSettings.PROXY_SORT_NAME ->
ProxySorter.Order.NAME_INCREASE
UiSettings.PROXY_SORT_DELAY ->
ProxySorter.Order.DELAY_INCREASE
else -> throw IllegalArgumentException()
}
val sorter = ProxySorter(groupSort, proxySort)
sorter.sort(proxies).run {
when (general.mode) {
General.Mode.GLOBAL -> this
General.Mode.DIRECT -> emptyList()
General.Mode.RULE -> this.filter { it.name != "GLOBAL" }
}
}
}
val prefix = prefixDeferred.await()
val sorted = sortDeferred.await()
val newList = withContext(Dispatchers.Default) {
sorted.map {
ProxyAdapter.ProxyGroupInfo(it.name,
it.proxies.map { p ->
val r = prefix.getOrElse(it.name to p.name) {
PrefixMerger.Result(p.name, "", p)
}
val data = if ( r.content.isEmpty() ) {
r.prefix to p.type.toString()
}
else {
r.content to r.prefix
}
ProxyAdapter.ProxyInfo(
p.name,
it.name,
data.first,
data.second,
p.delay.toShort(),
it.type == Proxy.Type.SELECT,
p.name == it.current
)
}
)
}
}
val newList = sorted.toAdapterElement(merged.input, general)
mainListAdapter.applyChange(newList, urlTesting)
(chipList.adapter!! as ProxyChipAdapter).apply {
chips = sorted.map { it.name }
chips = newList.map { it.name }
notifyDataSetChanged()
}
doScrollToLastProxy
if (scrollTop)
mainList.smoothScrollToPosition(0)
else if (scrollToLast) {
scrollToLast = false
val selected = uiSettings.get(UiSettings.PROXY_LAST_SELECT_GROUP)
scrollBinding.scrollMaster(selected)
}
delay(200)
refreshMutex.unlock()
}
}
@@ -344,7 +292,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
return mainListAdapter.getGroupPosition(token)
}
override fun doMasterScroll(scroller: LinearSmoothScroller) {
override fun doMasterScroll(scroller: LinearSmoothScroller, target: Int) {
mainListAdapter.layoutManager.startSmoothScroll(scroller)
}
}

View File

@@ -4,7 +4,7 @@ import android.os.Bundle
import com.github.kr328.clash.service.util.intent
import kotlinx.android.synthetic.main.activity_settings.*
class SettingsActivity: BaseActivity() {
class SettingsActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
@@ -13,7 +13,8 @@ class SettingsActivity: BaseActivity() {
commonUi.build {
option(
icon = getDrawable(R.drawable.ic_settings_applications),
title = getString(R.string.behavior)) {
title = getString(R.string.behavior)
) {
paddingHeight = true
onClick {
@@ -22,7 +23,8 @@ class SettingsActivity: BaseActivity() {
}
option(
icon = getDrawable(R.drawable.ic_network),
title = getString(R.string.network)) {
title = getString(R.string.network)
) {
paddingHeight = true
onClick {
@@ -31,7 +33,8 @@ class SettingsActivity: BaseActivity() {
}
option(
icon = getDrawable(R.drawable.ic_interface),
title = getString(R.string.interface_)) {
title = getString(R.string.interface_)
) {
paddingHeight = true
onClick {

View File

@@ -3,7 +3,7 @@ package com.github.kr328.clash
import android.os.Bundle
import com.github.kr328.clash.settings.BehaviorFragment
class SettingsBehaviorActivity: BaseActivity() {
class SettingsBehaviorActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@@ -3,7 +3,7 @@ package com.github.kr328.clash
import android.os.Bundle
import com.github.kr328.clash.settings.InterfaceFragment
class SettingsInterfaceActivity: BaseActivity() {
class SettingsInterfaceActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@@ -15,7 +15,7 @@ class SettingsNetworkActivity : BaseActivity() {
.replace(R.id.fragment, NetworkFragment())
.commit()
if ( clashRunning )
if (clashRunning)
Snackbar.make(rootView, R.string.options_unavailable, Snackbar.LENGTH_INDEFINITE).show()
}

View File

@@ -9,7 +9,7 @@ import androidx.core.content.getSystemService
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_support.*
class SupportActivity: BaseActivity() {
class SupportActivity : BaseActivity() {
override val activityLabel: CharSequence?
get() = getText(R.string.support)
@@ -24,10 +24,13 @@ class SupportActivity: BaseActivity() {
option(
title = getString(R.string.clash),
summary = getString(R.string.clash_url)) {
summary = getString(R.string.clash_url)
) {
onClick {
startActivity(Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.clash_url))))
startActivity(
Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.clash_url)))
)
}
}
option(
@@ -35,8 +38,10 @@ class SupportActivity: BaseActivity() {
summary = getString(R.string.clash_for_android_url)
) {
onClick {
startActivity(Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.clash_for_android_url))))
startActivity(
Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.clash_for_android_url)))
)
}
}
@@ -59,20 +64,25 @@ class SupportActivity: BaseActivity() {
summary = getString(R.string.github_issues_url)
) {
onClick {
startActivity(Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.github_issues_url))))
startActivity(
Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.github_issues_url)))
)
}
}
if ( resources.configuration.locales.get(0)
.language.equals("zh", true) ) {
if (resources.configuration.locales.get(0)
.language.equals("zh", true)
) {
option(
title = getString(R.string.telegram_channel),
summary = getString(R.string.telegram_channel_url)
) {
onClick {
startActivity(Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.telegram_channel_url))))
startActivity(
Intent(Intent.ACTION_VIEW)
.setData(Uri.parse(getString(R.string.telegram_channel_url)))
)
}
}
}

View File

@@ -4,14 +4,13 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.github.kr328.clash.remote.RemoteUtils
import com.github.kr328.clash.service.ClashService
import com.github.kr328.clash.service.Intents
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.utils.startClashService
import com.github.kr328.clash.utils.stopClashService
class TileService : TileService() {
private var currentProfile = ""
@@ -25,7 +24,7 @@ class TileService : TileService() {
startClashService()
}
Tile.STATE_ACTIVE -> {
stopService(ClashService::class.intent)
stopClashService()
}
}
}
@@ -38,22 +37,24 @@ class TileService : TileService() {
if (qsTile == null)
return
qsTile.state = if ( clashRunning )
qsTile.state = if (clashRunning)
Tile.STATE_ACTIVE
else
Tile.STATE_INACTIVE
qsTile.label = if ( currentProfile.isEmpty() )
qsTile.label = if (currentProfile.isEmpty())
getText(R.string.launch_name)
else
currentProfile
qsTile.icon = Icon.createWithResource(this, R.drawable.ic_notification)
qsTile.updateTile()
}
private val clashStatusReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when ( intent?.action ) {
when (intent?.action) {
Intents.INTENT_ACTION_CLASH_STARTED -> {
clashRunning = true
@@ -65,10 +66,8 @@ class TileService : TileService() {
currentProfile = ""
}
Intents.INTENT_ACTION_PROFILE_LOADED -> {
val entity = intent.
getParcelableExtra<ClashProfileEntity>(Intents.INTENT_EXTRA_PROFILE)
currentProfile = entity?.name ?: ""
currentProfile = RemoteUtils
.getCurrentClashProfileName(this@TileService) ?: ""
}
}
@@ -88,7 +87,10 @@ class TileService : TileService() {
}
)
clashRunning = RemoteUtils.detectClashRunning(this)
val name = RemoteUtils.getCurrentClashProfileName(this)
clashRunning = name != null
currentProfile = name ?: ""
}
override fun onDestroy() {

View File

@@ -34,14 +34,13 @@ class LiveLogAdapter(private val context: Context) : RecyclerView.Adapter<LogAda
}
fun insertItems(i: List<LogEvent>) {
val items = if ( i.size > MAX_LOG_ITEMS ) {
val items = if (i.size > MAX_LOG_ITEMS) {
i.subList(i.size - MAX_LOG_ITEMS, i.size)
}
else i
} else i
val predictSize = items.size + circularArray.size()
if ( predictSize > MAX_LOG_ITEMS ) {
if (predictSize > MAX_LOG_ITEMS) {
val removeSize = predictSize - MAX_LOG_ITEMS
notifyItemRangeRemoved(MAX_LOG_ITEMS - removeSize, removeSize)
circularArray.removeFromEnd(removeSize)

View File

@@ -15,11 +15,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.streams.toList
class PackagesAdapter(private val context: Context,
private val apps: List<AppInfo>) :
class PackagesAdapter(
private val context: Context,
private val apps: List<AppInfo>
) :
RecyclerView.Adapter<PackagesAdapter.Holder>() {
data class AppInfo(val packageName: String, val label: String, val icon: Drawable,
val installTime: Long, val updateTime: Long, val isSystem: Boolean)
data class AppInfo(
val packageName: String, val label: String, val icon: Drawable,
val installTime: Long, val updateTime: Long, val isSystem: Boolean
)
enum class Sort {
NAME, PACKAGE, INSTALL_TIME, UPDATE_TIME
@@ -49,7 +53,7 @@ class PackagesAdapter(private val context: Context,
val sA = selectedPackages.contains(a.packageName)
val sB = selectedPackages.contains(b.packageName)
if ( sA != sB ) {
if (sA != sB) {
when {
sA -> return@sorted -1
sB -> return@sorted 1
@@ -115,10 +119,9 @@ class PackagesAdapter(private val context: Context,
holder.packageName.text = current.packageName
holder.checkbox.isChecked = selectedPackages.contains(current.packageName)
holder.root.setOnClickListener {
if ( selectedPackages.contains(current.packageName) ) {
if (selectedPackages.contains(current.packageName)) {
selectedPackages.remove(current.packageName)
}
else {
} else {
selectedPackages.add(current.packageName)
}

View File

@@ -132,6 +132,6 @@ class ProfileAdapter(private val context: Context, private val callback: Callbac
}
private fun offsetDate(date: Long): CharSequence {
return IntervalUtils.intervalString(context,System.currentTimeMillis() - date)
return IntervalUtils.intervalString(context, System.currentTimeMillis() - date)
}
}

View File

@@ -14,7 +14,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.google.android.material.card.MaterialCardView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
@@ -29,14 +28,15 @@ class ProxyAdapter(
data class ProxyGroupInfo(
val name: String,
val current: String,
val proxies: List<ProxyInfo>
)
data class ProxyInfo(
val name: String,
val group: String,
val prefix: String,
val content: String,
val title: String,
val summary: String,
val delay: Short,
val selectable: Boolean,
val active: Boolean
@@ -62,8 +62,6 @@ class ProxyAdapter(
get() = info.group
}
private var rootMutex = Mutex()
private var urlTesting: Set<String> = emptySet()
private var renderList = mutableListOf<RenderInfo>()
private var activeList: MutableMap<String, Int> = mutableMapOf()
@@ -86,7 +84,10 @@ class ProxyAdapter(
val layoutManager = GridLayoutManager(context, DEFAULT_SPAN_COUNT).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (renderList[position]) {
val current = renderList.getOrNull(position)
?: renderList.getOrNull(position) ?: return spanCount
return when (current) {
is ProxyGroupRenderInfo -> spanCount
is ProxyRenderInfo -> 1
else -> throw IllegalArgumentException()
@@ -98,7 +99,7 @@ class ProxyAdapter(
private var root = listOf<ProxyGroupInfo>()
private class ProxyGroupHeader(view: View) : RecyclerView.ViewHolder(view) {
val name: TextView = view.findViewById(R.id.name)
val title: TextView = view.findViewById(R.id.name)
val urlTest: View = view.findViewById(R.id.urlTest)
val urlTestProgress: View = view.findViewById(R.id.urlTestProgress)
}
@@ -112,8 +113,6 @@ class ProxyAdapter(
suspend fun applyChange(newList: List<ProxyGroupInfo>, testing: Set<String>) =
withContext(Dispatchers.Default) {
rootMutex.lock()
val newRenderList = newList
.flatMap {
listOf(ProxyGroupRenderInfo(it)) + it.proxies.map { p -> ProxyRenderInfo(p) }
@@ -142,7 +141,7 @@ class ProxyAdapter(
groupCache[it.name] = index
is ProxyRenderInfo -> {
if (it.info.active)
activeCache[it.name] = index
activeCache[it.group] = index
}
}
}
@@ -154,9 +153,11 @@ class ProxyAdapter(
groupPosition = groupCache
activeList = activeCache
result.dispatchUpdatesTo(this@ProxyAdapter)
}
rootMutex.unlock()
groupCache.forEach { (_, u) ->
notifyItemChanged(u)
}
}
}
fun getGroupPosition(name: String): Int {
@@ -193,9 +194,10 @@ class ProxyAdapter(
is ProxyGroupHeader -> {
val current = renderList[position] as ProxyGroupRenderInfo
groupPosition[current.name] = position
holder.name.text = current.info.name
holder.title.text = context.getString(
R.string.format_proxy_group_title,
current.info.name, current.info.current
)
holder.urlTest.setOnClickListener {
holder.urlTest.visibility = View.GONE
holder.urlTestProgress.visibility = View.VISIBLE
@@ -214,8 +216,8 @@ class ProxyAdapter(
is ProxyItem -> {
val current = renderList[position] as ProxyRenderInfo
holder.prefix.text = current.info.prefix
holder.content.text = current.info.content
holder.prefix.text = current.info.title
holder.content.text = current.info.summary
if (current.info.delay > 0)
holder.delay.text = current.info.delay.toString()
@@ -223,8 +225,6 @@ class ProxyAdapter(
holder.delay.text = if (current.info.selectable) "" else "N/A"
if (current.info.active) {
activeList[current.group] = position
holder.prefix.setTextColor(Color.WHITE)
holder.content.setTextColor(Color.WHITE)
holder.delay.setTextColor(Color.WHITE)
@@ -239,14 +239,22 @@ class ProxyAdapter(
if (current.info.selectable) {
holder.root.setOnClickListener {
val oldPosition = activeList[current.group] ?: return@setOnClickListener
val groupPosition =
groupPosition[current.group] ?: return@setOnClickListener
val old = renderList[oldPosition] as ProxyRenderInfo
val new = renderList[position] as ProxyRenderInfo
val group = renderList[groupPosition] as ProxyGroupRenderInfo
renderList[oldPosition] = old.copy(info = old.info.copy(active = false))
renderList[position] = new.copy(info = new.info.copy(active = true))
renderList[groupPosition] =
group.copy(info = group.info.copy(current = current.name))
activeList[current.group] = position
notifyItemChanged(oldPosition)
notifyItemChanged(position)
notifyItemChanged(groupPosition)
onSelect(current.group, current.name)
}

View File

@@ -0,0 +1,34 @@
package com.github.kr328.clash.dump
object LogcatDumper {
fun dump(): List<String> {
return try {
val process =
Runtime.getRuntime().exec(arrayOf("logcat", "-d", "-s", "-v", "raw", "Go"))
val result = process.inputStream.bufferedReader().useLines {
var list = mutableListOf<String>()
var capture = false
it.forEach { line ->
if (line.startsWith("panic")) {
capture = true
list = mutableListOf()
}
if (capture)
list.add(line)
}
list
}
process.waitFor()
result
} catch (e: Exception) {
emptyList()
}
}
}

View File

@@ -0,0 +1,5 @@
package com.github.kr328.clash.pipeline
import com.github.kr328.clash.service.settings.BaseSettings
data class Pipeline<T>(val input: T, val settings: BaseSettings)

View File

@@ -0,0 +1,101 @@
package com.github.kr328.clash.pipeline
import com.github.kr328.clash.adapter.ProxyAdapter
import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.model.Proxy
import com.github.kr328.clash.core.model.ProxyGroup
import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.utils.PrefixMerger
import com.github.kr328.clash.utils.ProxySorter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
data class ProxyEntry(val group: String, val name: String)
data class ProxyMerged(val prefix: String, val content: String)
suspend fun Pipeline<List<ProxyGroup>>.mergePrefix(): Pipeline<Map<ProxyEntry, ProxyMerged>> {
if (!settings.get(UiSettings.PROXY_MERGE_PREFIX))
return Pipeline(emptyMap(), settings)
val result = coroutineScope {
input
.map {
async {
it.name to PrefixMerger.merge(it.proxies, Proxy::name)
}
}
.map {
it.await()
}
.flatMap {
it.second.map { merged ->
ProxyEntry(it.first, merged.value.name) to ProxyMerged(
merged.prefix,
merged.content
)
}
}
.toMap()
}
return Pipeline(result, settings)
}
suspend fun Pipeline<List<ProxyGroup>>.sort(): Pipeline<List<ProxyGroup>> {
val groupSort = when (settings.get(UiSettings.PROXY_GROUP_SORT)) {
UiSettings.PROXY_SORT_DEFAULT ->
ProxySorter.Order.DEFAULT
UiSettings.PROXY_SORT_NAME ->
ProxySorter.Order.NAME_INCREASE
UiSettings.PROXY_SORT_DELAY ->
ProxySorter.Order.DELAY_INCREASE
else -> throw IllegalArgumentException()
}
val proxySort = when (settings.get(UiSettings.PROXY_PROXY_SORT)) {
UiSettings.PROXY_SORT_DEFAULT ->
ProxySorter.Order.DEFAULT
UiSettings.PROXY_SORT_NAME ->
ProxySorter.Order.NAME_INCREASE
UiSettings.PROXY_SORT_DELAY ->
ProxySorter.Order.DELAY_INCREASE
else -> throw IllegalArgumentException()
}
val sorter = ProxySorter(groupSort, proxySort)
return copy(input = sorter.sort(input))
}
suspend fun Pipeline<List<ProxyGroup>>.toAdapterElement(
prefixMerged: Map<ProxyEntry, ProxyMerged>,
general: General
):
List<ProxyAdapter.ProxyGroupInfo> {
return input.map { group ->
val proxies = group.proxies.map { proxy ->
val merged = prefixMerged[ProxyEntry(group.name, proxy.name)]?.takeIf {
it.prefix.isNotBlank() && it.content.isNotBlank()
} ?: ProxyMerged(proxy.type.toString(), proxy.name)
ProxyAdapter.ProxyInfo(
proxy.name, group.name, merged.content, merged.prefix,
proxy.delay.toShort(), group.type == Proxy.Type.SELECT,
group.current == proxy.name
)
}
ProxyAdapter.ProxyGroupInfo(group.name, group.current, proxies)
}.let {
withContext(Dispatchers.Default) {
when (general.mode) {
General.Mode.DIRECT -> emptyList()
General.Mode.GLOBAL -> it
General.Mode.RULE -> it.filter { it.name != "GLOBAL" }
}
}
}
}

View File

@@ -3,7 +3,7 @@ package com.github.kr328.clash.preference
import android.content.Context
import com.github.kr328.clash.service.settings.BaseSettings
class UiSettings(context: Context):
class UiSettings(context: Context) :
BaseSettings(context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)) {
companion object {
private const val FILE_NAME = "ui"

View File

@@ -9,14 +9,13 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.github.kr328.clash.service.Intents
import com.github.kr328.clash.service.data.ClashProfileEntity
object Broadcasts {
interface Receiver {
fun onStarted()
fun onStopped(cause: String?)
fun onProfileChanged()
fun onProfileLoaded(profileEntity: ClashProfileEntity)
fun onProfileLoaded()
}
var clashRunning: Boolean = false
@@ -47,11 +46,8 @@ object Broadcasts {
it.onProfileChanged()
}
Intents.INTENT_ACTION_PROFILE_LOADED -> {
val profile = intent
.getParcelableExtra<ClashProfileEntity>(Intents.INTENT_EXTRA_PROFILE) ?: return
receivers.forEach {
it.onProfileLoaded(profile)
it.onProfileLoaded()
}
}
}

View File

@@ -12,7 +12,7 @@ import kotlinx.coroutines.withContext
class ClashClient(val service: IClashManager) {
suspend fun setSelectProxy(name: String, proxy: String): Boolean = withContext(Dispatchers.IO) {
return@withContext service.setSelectProxy(name, proxy)
service.setSelectProxy(name, proxy)
}
suspend fun startHealthCheck(group: String) = withContext(Dispatchers.IO) {
@@ -39,10 +39,9 @@ class ClashClient(val service: IClashManager) {
service.queryGeneral()
}
suspend fun queryBandwidth(): Long =
withContext(Dispatchers.IO) {
service.queryBandwidth()
}
suspend fun queryBandwidth(): Long = withContext(Dispatchers.IO) {
service.queryBandwidth()
}
suspend fun setProxyMode(mode: General.Mode) = withContext(Dispatchers.IO) {
service.setProxyMode(mode.toString())

View File

@@ -7,17 +7,21 @@ import android.content.Intent
import android.content.ServiceConnection
import android.os.Handler
import android.os.IBinder
import android.os.RemoteException
import androidx.core.content.edit
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.github.kr328.clash.ApkBrokenActivity
import com.github.kr328.clash.Constants
import com.github.kr328.clash.dump.LogcatDumper
import com.github.kr328.clash.service.ClashManagerService
import com.github.kr328.clash.service.IClashManager
import com.github.kr328.clash.service.IProfileService
import com.github.kr328.clash.service.ProfileService
import com.github.kr328.clash.service.util.intent
import com.microsoft.appcenter.crashes.Crashes
import com.microsoft.appcenter.crashes.ingestion.models.ErrorAttachmentLog
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.util.zip.ZipFile
@@ -44,6 +48,16 @@ object Remote {
instance = ClashClient(IClashManager.Stub.asInterface(service))
service?.linkToDeath({
val log = LogcatDumper.dump().joinToString(separator = "\n")
val attachmentLog = ErrorAttachmentLog
.attachmentWithText(log, "logcat.txt")
Crashes.trackError(
RemoteException("Clash Service Crashed"),
null, listOf(attachmentLog)
)
onServiceDisconnected(null)
}, 0)
@@ -92,8 +106,10 @@ object Remote {
GlobalScope.launch {
if (!verifyApk(application)) {
application.startActivity(ApkBrokenActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
application.startActivity(
ApkBrokenActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
return@launch
}

View File

@@ -1,24 +1,55 @@
package com.github.kr328.clash.remote
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.github.kr328.clash.ApkBrokenActivity
import com.github.kr328.clash.service.Constants
import com.github.kr328.clash.service.ServiceStatusProvider
import com.github.kr328.clash.service.util.intent
object RemoteUtils {
fun detectClashRunning(context: Context): Boolean {
val authority = Uri.Builder()
.scheme("content")
.authority("${context.packageName}${Constants.STATUS_PROVIDER_SUFFIX}")
.build()
try {
val authority = Uri.Builder()
.scheme("content")
.authority("${context.packageName}${Constants.STATUS_PROVIDER_SUFFIX}")
.build()
val pong = context.contentResolver.call(
authority,
ServiceStatusProvider.METHOD_PING_CLASH_SERVICE,
null,
null
)
val pong = context.contentResolver.call(
authority,
ServiceStatusProvider.METHOD_PING_CLASH_SERVICE,
null,
null
)
return pong != null
return pong != null
} catch (e: IllegalArgumentException) {
context.startActivity(ApkBrokenActivity::class.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
return false
}
}
fun getCurrentClashProfileName(context: Context): String? {
try {
val authority = Uri.Builder()
.scheme("content")
.authority("${context.packageName}${Constants.STATUS_PROVIDER_SUFFIX}")
.build()
val pong = context.contentResolver.call(
authority,
ServiceStatusProvider.METHOD_PING_CLASH_SERVICE,
null,
null
)
return pong?.getString("name")
} catch (e: IllegalArgumentException) {
context.startActivity(ApkBrokenActivity::class.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
return null
}
}
}

View File

@@ -8,7 +8,7 @@ import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.service.settings.ServiceSettings
import moe.shizuku.preference.PreferenceFragment
abstract class BaseSettingFragment: PreferenceFragment() {
abstract class BaseSettingFragment : PreferenceFragment() {
abstract fun onCreateDataStore(): SettingsDataStore
abstract val xmlResourceId: Int

View File

@@ -8,7 +8,7 @@ import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.componentName
class BehaviorFragment: BaseSettingFragment() {
class BehaviorFragment : BaseSettingFragment() {
companion object {
private const val KEY_START_ON_BOOT = "start_on_boot"
private const val KEY_SHOW_TRAFFIC = "show_traffic"
@@ -30,11 +30,11 @@ class BehaviorFragment: BaseSettingFragment() {
override val xmlResourceId: Int
get() = R.xml.settings_behavior
private inner class StartOnBootSource: SettingsDataStore.Source {
private inner class StartOnBootSource : SettingsDataStore.Source {
override fun set(value: Any?) {
val v = value as Boolean? ?: return
val status = if ( v )
val status = if (v)
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
@@ -42,7 +42,8 @@ class BehaviorFragment: BaseSettingFragment() {
requireActivity().packageManager.setComponentEnabledSetting(
OnBootReceiver::class.componentName,
status,
PackageManager.DONT_KILL_APP)
PackageManager.DONT_KILL_APP
)
}
override fun get(): Any? {

View File

@@ -4,7 +4,7 @@ import com.github.kr328.clash.R
import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.service.settings.ServiceSettings
class InterfaceFragment: BaseSettingFragment() {
class InterfaceFragment : BaseSettingFragment() {
companion object {
private const val KEY_DARK_MODE = "dark_mode"
private const val KEY_LANGUAGE = "language"

View File

@@ -8,7 +8,7 @@ import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.intent
class NetworkFragment: BaseSettingFragment() {
class NetworkFragment : BaseSettingFragment() {
companion object {
private const val KEY_ENABLE_VPN_SERVICE = "enable_vpn_service"
private const val KEY_IPV6 = "ipv6"

View File

@@ -3,7 +3,7 @@ package com.github.kr328.clash.settings
import com.github.kr328.clash.service.settings.BaseSettings
import moe.shizuku.preference.PreferenceDataStore
class SettingsDataStore: PreferenceDataStore() {
class SettingsDataStore : PreferenceDataStore() {
interface Source {
fun set(value: Any?)
fun get(): Any?
@@ -20,8 +20,8 @@ class SettingsDataStore: PreferenceDataStore() {
this.applyListener = block
}
inline fun <reified T>BaseSettings.Entry<T>.asSource(settings: BaseSettings): Source {
return object: Source {
inline fun <reified T> BaseSettings.Entry<T>.asSource(settings: BaseSettings): Source {
return object : Source {
override fun set(value: Any?) {
val v = value ?: throw NullPointerException()

View File

@@ -8,7 +8,12 @@ const val DATE_DATE_ONLY = "yyyy-MM-dd"
const val DATE_TIME_ONLY = "HH:mm:ss"
const val DATE_ALL = "$DATE_DATE_ONLY $DATE_TIME_ONLY"
fun Date.format(context: Context, includeDate: Boolean = true, includeTime: Boolean = true, custom: String = ""): String {
fun Date.format(
context: Context,
includeDate: Boolean = true,
includeTime: Boolean = true,
custom: String = ""
): String {
val locale = context.resources.configuration.locales[0]
return when {

View File

@@ -11,16 +11,19 @@ object PrefixMerger {
suspend fun <T> merge(values: List<T>, transform: (T) -> String): List<Result<T>> =
withContext(Dispatchers.Default) {
val pairs = values.map {
transform(it).trim() to it
transform(it).trim().toCodePointList() to it
}
val groups = mutableListOf<List<Pair<String, T>>>()
var mergingGroup = mutableListOf<Pair<String, T>>()
var currentChar: Char = 0.toChar()
val groups = mutableListOf<List<Pair<List<Int>, T>>>()
var mergingGroup = mutableListOf<Pair<List<Int>, T>>()
var currentCodePoint = 0
val result = mutableListOf<Result<T>>()
for (pair in pairs) {
if (pair.first[0] == currentChar) {
if (pair.first.isEmpty())
continue
if (pair.first[0] == currentCodePoint) {
mergingGroup.add(pair)
} else {
if (mergingGroup.isNotEmpty()) {
@@ -28,7 +31,7 @@ object PrefixMerger {
mergingGroup = mutableListOf()
}
currentChar = pair.first[0]
currentCodePoint = pair.first[0]
mergingGroup.add(pair)
}
}
@@ -38,7 +41,7 @@ object PrefixMerger {
for (group in groups) {
var diffIndex = 0
val size = group.map { it.first.length }.min() ?: 0
val size = group.map { it.first.size }.min() ?: 0
diff@ for (charIndex in 0 until size) {
for (stringIndex in 0 until (group.size - 1)) {
@@ -50,12 +53,13 @@ object PrefixMerger {
}
group.forEach {
val prefix = it.first.subList(0, diffIndex)
val content = it.first.subList(diffIndex, it.first.size)
result.add(
Result(
it.first.substring(0, diffIndex)
.replace(REGEX_PREFIX_TRIM, ""),
it.first.substring(diffIndex),
it.second
prefix.asCodePointString().replace(REGEX_PREFIX_TRIM, ""),
content.asCodePointString(), it.second
)
)
}

View File

@@ -12,19 +12,30 @@ class ProxySorter(private val groupOrder: Order, private val proxyOrder: Order)
suspend fun sort(proxyGroup: List<ProxyGroup>): List<ProxyGroup> =
withContext(Dispatchers.Default) {
val global = proxyGroup.singleOrNull {
it.name == "GLOBAL"
val groups = proxyGroup.groupBy {
if (it.name == "GLOBAL")
"GLOBAL"
else
"OTHER"
}
val global = groups["GLOBAL"]?.singleOrNull()
val other = groups["OTHER"] ?: emptyList()
val sortedGroup = when (groupOrder) {
Order.DEFAULT -> groupSortWithDefault(global, proxyGroup)
Order.DELAY_INCREASE -> groupSortWithDelay(true, proxyGroup)
Order.DELAY_DECREASE -> groupSortWithDelay(false, proxyGroup)
Order.NAME_INCREASE -> groupSortWithName(true, proxyGroup)
Order.NAME_DECREASE -> groupSortWithName(false, proxyGroup)
Order.DEFAULT -> groupSortWithDefault(global, other)
Order.DELAY_INCREASE -> groupSortWithDelay(true, other)
Order.DELAY_DECREASE -> groupSortWithDelay(false, other)
Order.NAME_INCREASE -> groupSortWithName(true, other)
Order.NAME_DECREASE -> groupSortWithName(false, other)
}
sortedGroup.map {
val sorted = if (global == null)
sortedGroup
else
listOf(global) + sortedGroup
sorted.map {
val sortedProxy = when (proxyOrder) {
Order.DEFAULT -> it.proxies
Order.DELAY_INCREASE -> proxySortWithDelay(true, it.proxies)

View File

@@ -0,0 +1,60 @@
package com.github.kr328.clash.utils
import android.content.Context
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
class QuickSmoothScroller(context: Context, target: Int) :
LinearSmoothScroller(context) {
companion object {
const val MAX_OFFSET = 2
}
var started = {}
var stopped = {}
init {
targetPosition = target
}
override fun getVerticalSnapPreference(): Int {
return SNAP_TO_START
}
override fun onStop() {
super.onStop()
stopped()
}
override fun onStart() {
super.onStart()
started()
}
override fun onSeekTargetStep(dx: Int, dy: Int, state: RecyclerView.State, action: Action) {
when (val lm = layoutManager) {
is LinearLayoutManager -> {
val current = lm.findFirstCompletelyVisibleItemPosition()
if (targetPosition > current && targetPosition - current > MAX_OFFSET)
action.jumpTo(targetPosition - MAX_OFFSET)
else if (current > targetPosition && current - targetPosition > MAX_OFFSET)
action.jumpTo(targetPosition + MAX_OFFSET)
}
is GridLayoutManager -> {
val current = lm.findFirstCompletelyVisibleItemPosition()
if (targetPosition > current && targetPosition - current > MAX_OFFSET)
action.jumpTo(targetPosition - MAX_OFFSET)
else if (current > targetPosition && current - targetPosition > MAX_OFFSET)
action.jumpTo(targetPosition + MAX_OFFSET)
}
}
super.onSeekTargetStep(dx, dy, state, action)
}
}

View File

@@ -1,7 +1,6 @@
package com.github.kr328.clash.utils
import android.content.Context
import android.util.DisplayMetrics
import androidx.recyclerview.widget.LinearSmoothScroller
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
@@ -14,7 +13,7 @@ class ScrollBinding(
fun getCurrentMasterToken(): String
fun onMasterTokenChanged(token: String)
fun getMasterTokenPosition(token: String): Int
fun doMasterScroll(scroller: LinearSmoothScroller)
fun doMasterScroll(scroller: LinearSmoothScroller, target: Int)
}
private val updateChannel = Channel<Unit>(Channel.CONFLATED)
@@ -30,33 +29,9 @@ class ScrollBinding(
if (position < 0)
return
val scroller = (object : LinearSmoothScroller(context) {
override fun getVerticalSnapPreference(): Int {
return SNAP_TO_START
}
val scroller = QuickSmoothScroller(context, position)
override fun onStop() {
super.onStop()
preventSlaveScroll = false
}
override fun onStart() {
super.onStart()
preventSlaveScroll = true
}
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float {
return super.calculateSpeedPerPixel(displayMetrics) * 0.8f
}
init {
targetPosition = position
}
})
callback.doMasterScroll(scroller)
callback.doMasterScroll(scroller, position)
}
suspend fun exec() {

View File

@@ -6,20 +6,27 @@ import android.net.VpnService
import com.github.kr328.clash.preference.UiSettings
import com.github.kr328.clash.service.ClashService
import com.github.kr328.clash.service.Intents
import com.github.kr328.clash.service.TunService
import com.github.kr328.clash.service.util.intent
import com.github.kr328.clash.service.util.sendBroadcastSelf
import com.github.kr328.clash.service.util.startForegroundServiceCompat
fun Context.startClashService(): Intent? {
val startTun = UiSettings(this).get(UiSettings.ENABLE_VPN)
if ( startTun ) {
if (startTun) {
val vpnRequest = VpnService.prepare(this)
if ( vpnRequest != null )
if (vpnRequest != null)
return vpnRequest
startForegroundServiceCompat(TunService::class.intent)
} else {
startForegroundServiceCompat(ClashService::class.intent)
}
startForegroundServiceCompat(ClashService::class.intent
.putExtra(Intents.INTENT_EXTRA_START_TUN, startTun))
return null
}
fun Context.stopClashService() {
sendBroadcastSelf(Intent(Intents.INTENT_ACTION_REQUEST_STOP))
}

View File

@@ -0,0 +1,25 @@
package com.github.kr328.clash.utils
fun String.toCodePointList(): List<Int> {
var offset = 0
val result = mutableListOf<Int>()
while (offset < length) {
val codePoint = codePointAt(offset)
result.add(codePoint)
offset += Character.charCount(codePoint)
}
return result.toList()
}
fun List<Int>.asCodePointString(): String {
val sb = StringBuilder()
forEach {
sb.appendCodePoint(it)
}
return sb.toString()
}

View File

@@ -1,12 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="108dp"
android:height="108dp"
android:viewportWidth="796.13654"
android:viewportHeight="796.13654">
<group android:translateX="196.83206"
android:translateY="207.54466">
<path
android:fillColor="#1E4376"
android:pathData="M95.758,327.156l4,-41l12,-97l13,-94l12,-61l2,-7c2,-5 4,-6 9,-3l10,9l26,41c3,4 6,5 11,4c28,-6 57,-5 85,1c5,1 7,-1 9,-5a1453,1453 0,0 1,30 -49c4,-4 7,-4 9,2l8,36l28,177c5,34 8,67 11,100c2,12 1,13 -11,14c-54,5 -108,5 -162,4c-30,0 -59,-2 -88,-3c-6,0 -12,-3 -18,-4l-17,-1c-18,-2 -34,-9 -44,-26c-17,-28 -6,-65 29,-75c4,-1 8,0 12,1c3,2 3,5 0,7l-6,5c-4,3 -9,6 -12,10a31,31 0,0 0,7 49c10,5 20,6 31,6zM176.758,139.156c-8,0 -15,7 -15,15s6,15 15,15c8,0 15,-6 15,-15c0,-8 -7,-15 -15,-15zM299.758,169.156c8,0 15,-6 15,-15c0,-8 -6,-15 -15,-15a15,15 0,1 0,0 30zM216.758,189.156c4,7 11,7 20,1c7,6 16,6 18,-1c-8,3 -14,2 -19,-7c-4,9 -10,10 -19,7z"/>
android:viewportWidth="406.92642"
android:viewportHeight="406.92642">
<group android:translateX="103.4632"
android:translateY="103.4632">
<path
android:fillColor="#1E4376"
android:pathData="M47.211,168.128C70.531,-34.962 67.471,13.788 94.071,43.818c13.45,-1.52 27.24,-3.47 40.82,-0.67c2.64,0.13 5.42,1.86 7.71,0.18c4.12,-6.27 7.35,-13.54 11.35,-20c12.19,-24.44 12.85,19.54 15.48,26.52c5.23,32.99 10.89,64.46 14.67,97.59c0.31,10.72 5.74,32.92 1.08,33.56c-49.36,5.23 -147.71,3.91 -160.84,-6.3c-15.85,-10.5 -15.18,-35.33 2.03,-43.72c3.63,-2.03 10.68,-3.72 11.94,0.7c-2.41,4.99 -8.79,5.77 -12.12,11.17C16.621,158.948 33.111,168.888 47.211,168.128zM87.841,74.008c-10.42,0.52 -9.59,14.89 -0.07,15.18C98.191,88.668 97.361,74.298 87.841,74.008zM149.121,89.188c10.46,-0.34 9.85,-14.71 0.38,-15.18C139.031,74.348 139.651,88.718 149.121,89.188zM107.871,99.228c2.16,3.48 5.28,3.29 9.79,0.16c3.81,3.17 8.06,3.28 9.18,-0.19c-3.78,1.17 -7.04,0.79 -9.4,-3.49C115.371,100.108 112.071,100.428 107.871,99.228z"
tools:ignore="VectorPath" />
</group>
</vector>

View File

@@ -1,9 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="402.47244dp"
android:height="381.04724dp"
android:viewportWidth="402.47244"
android:viewportHeight="381.04724">
<path
android:fillColor="?attr/colorPrimary"
android:pathData="M95.758,327.156l4,-41l12,-97l13,-94l12,-61l2,-7c2,-5 4,-6 9,-3l10,9l26,41c3,4 6,5 11,4c28,-6 57,-5 85,1c5,1 7,-1 9,-5a1453,1453 0,0 1,30 -49c4,-4 7,-4 9,2l8,36l28,177c5,34 8,67 11,100c2,12 1,13 -11,14c-54,5 -108,5 -162,4c-30,0 -59,-2 -88,-3c-6,0 -12,-3 -18,-4l-17,-1c-18,-2 -34,-9 -44,-26c-17,-28 -6,-65 29,-75c4,-1 8,0 12,1c3,2 3,5 0,7l-6,5c-4,3 -9,6 -12,10a31,31 0,0 0,7 49c10,5 20,6 31,6zM176.758,139.156c-8,0 -15,7 -15,15s6,15 15,15c8,0 15,-6 15,-15c0,-8 -7,-15 -15,-15zM299.758,169.156c8,0 15,-6 15,-15c0,-8 -6,-15 -15,-15a15,15 0,1 0,0 30zM216.758,189.156c4,7 11,7 20,1c7,6 16,6 18,-1c-8,3 -14,2 -19,-7c-4,9 -10,10 -19,7z"/>
xmlns:tools="http://schemas.android.com/tools"
android:width="200dp"
android:height="200dp"
android:viewportWidth="200"
android:viewportHeight="200">
<path
android:fillColor="?attr/colorPrimary"
android:pathData="M47.211,168.128C70.531,-34.962 67.471,13.788 94.071,43.818c13.45,-1.52 27.24,-3.47 40.82,-0.67c2.64,0.13 5.42,1.86 7.71,0.18c4.12,-6.27 7.35,-13.54 11.35,-20c12.19,-24.44 12.85,19.54 15.48,26.52c5.23,32.99 10.89,64.46 14.67,97.59c0.31,10.72 5.74,32.92 1.08,33.56c-49.36,5.23 -147.71,3.91 -160.84,-6.3c-15.85,-10.5 -15.18,-35.33 2.03,-43.72c3.63,-2.03 10.68,-3.72 11.94,0.7c-2.41,4.99 -8.79,5.77 -12.12,11.17C16.621,158.948 33.111,168.888 47.211,168.128zM87.841,74.008c-10.42,0.52 -9.59,14.89 -0.07,15.18C98.191,88.668 97.361,74.298 87.841,74.008zM149.121,89.188c10.46,-0.34 9.85,-14.71 0.38,-15.18C139.031,74.348 139.651,88.718 149.121,89.188zM107.871,99.228c2.16,3.48 5.28,3.29 9.79,0.16c3.81,3.17 8.06,3.28 9.18,-0.19c-3.78,1.17 -7.04,0.79 -9.4,-3.49C115.371,100.108 112.071,100.428 107.871,99.228z"
tools:ignore="VectorPath" />
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -170,4 +170,6 @@
<string name="about_split_apks_url" translatable="false">https://developer.android.com/platform/technology/app-bundle</string>
<string name="google_play_url" translatable="false">https://play.google.com/store/apps/details?id=com.github.kr328.clash</string>
<string name="github_releases_url" translatable="false">https://github.com/Kr328/ClashForAndroid/releases</string>
<string name="format_proxy_group_title" translatable="false">%s - %s</string>
</resources>

View File

@@ -8,8 +8,8 @@ buildscript {
gMinSdkVersion = 24
gTargetSdkVersion = 29
gVersionCode = 10100
gVersionName = "1.1.0"
gVersionCode = 10105
gVersionName = "1.1.5"
gKotlinVersion = '1.3.61'
gKotlinCoroutineVersion = '1.3.3'
@@ -20,7 +20,7 @@ buildscript {
gLifecycleVersion = "2.2.0"
gRecyclerviewVersion = "1.1.0"
gAppCompatVersion = "1.1.0"
gMaterialDesignVersion = "1.2.0-alpha04"
gMaterialDesignVersion = '1.2.0-alpha05'
gShizukuPreferenceVersion = "4.2.0"
gMultiprocessPreferenceVersion = "1.0.0"
}
@@ -29,7 +29,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.android.tools.build:gradle:3.6.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$gKotlinVersion"
classpath "org.jetbrains.kotlin:kotlin-serialization:$gKotlinVersion"
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -103,7 +103,7 @@ open class GolangBindTask : DefaultTask() {
.copyRecursively(goPath.resolve("src"), overwrite = true)
"gomobile init".exec(goBuildPath)
"gomobile bind -target=android \"-gcflags=all=-trimpath=$goPath\" github.com/kr328/cfa/bridge".exec(goBuildPath)
"gomobile bind -target=android \"-gcflags=all=-trimpath=$goPath\" \"-ldflags=-w -s\" github.com/kr328/cfa/bridge".exec(goBuildPath)
nativeOutput.deleteRecursively()
javaOutput.deleteRecursively()
@@ -161,7 +161,7 @@ open class GolangBindTask : DefaultTask() {
.asSequence()
.filter { line -> line.startsWith("replace") }
.map { replace ->
replace.replace(REGEX_REPLACE_TARGET_LOCAL, "=> " + file.parentFile.absolutePath + "/")
replace.replace(REGEX_REPLACE_TARGET_LOCAL, "=> " + file.parentFile.absolutePath.replace('\\','/') + "/")
}
.map { replace ->
replace.replace(REGEX_REPLACE_SOURCE_VERSION, " =>")

View File

@@ -1,12 +1,24 @@
package bridge
import (
"sync"
"github.com/Dreamacro/clash/component/mmdb"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
"github.com/kr328/cfa/profile"
)
var (
logCallback LogCallback
logSubscribe sync.Once
)
type LogCallback interface {
OnLogEvent(level, payload string)
}
func LoadMMDB(data []byte) {
dataClone := make([]byte, len(data))
copy(dataClone, data)
@@ -26,3 +38,27 @@ func Reset() {
func SetApplicationVersion(version string) {
profile.ApplicationVersion = version
}
func SetLogCallback(callback LogCallback) {
logSubscribe.Do(func() {
go func() {
sub := log.Subscribe()
defer log.UnSubscribe(sub)
for {
elm := <-sub
l := elm.(*log.Event)
if l.LogLevel < log.Level() {
continue
}
if cb := logCallback; cb != nil {
cb.OnLogEvent(l.LogLevel.String(), l.Payload)
}
}
}()
})
logCallback = callback
}

View File

@@ -1,16 +1,21 @@
package bridge
import (
"github.com/Dreamacro/clash/log"
"sync"
"github.com/Dreamacro/clash/tunnel"
)
type EventPoll struct {
stop sync.Once
onStop func()
}
func (e *EventPoll) Stop() {
e.onStop()
e.stop.Do(func() {
e.onStop()
})
}
type Traffic struct {
@@ -40,35 +45,3 @@ func QueryTraffic() *Traffic {
Download: down,
}
}
func PollLogs(logs Logs) *EventPoll {
stopChannel := make(chan int, 1)
sub := log.Subscribe()
go func() {
defer log.UnSubscribe(sub)
defer close(stopChannel)
defer log.Infoln("Logs Poll Stopped")
for {
select {
case <-stopChannel:
return
case elm := <-sub:
l := elm.(*log.Event)
if l.LogLevel < log.Level() {
break
}
logs.OnEvent(l.Type(), l.Payload)
}
}
}()
return &EventPoll{
onStop: func() {
stopChannel <- 0
},
}
}

View File

@@ -1,33 +1,3 @@
package main
import (
"io/ioutil"
"net"
"os"
"github.com/Dreamacro/clash/component/mmdb"
)
func main() {
f, err := os.Open("./Country.mmdb")
if err != nil {
println(err)
return
}
buf, err := ioutil.ReadAll(f)
if err != nil {
println(err)
return
}
mmdb.LoadFromBytes(buf)
c, err := mmdb.Instance().Country(net.ParseIP("114.114.114.114"))
if err != nil {
println(err)
return
}
println(c.Country.IsoCode)
}
func main() {}

View File

@@ -83,10 +83,14 @@ func ReadAndCheck(fd int, output, baseDir string) error {
}
func SaveAndCheck(data []byte, output, baseDir string) error {
_, err := parseConfig(data, baseDir)
cfg, err := parseConfig(data, baseDir)
if err != nil {
return err
}
for _, v := range cfg.Providers {
v.Destroy()
}
return ioutil.WriteFile(output, data, defaultFileMode)
}

View File

@@ -107,7 +107,7 @@ func parseConfig(data []byte, baseDir string) (*config.Config, error) {
raw.Experimental.Interface = ""
raw.ExternalUI = ""
raw.ExternalController = ""
raw.Rule = append([]string{fmt.Sprintf("IP-CIDR,%s,REJECT", tunAddress)}, raw.Rule...)
raw.Rule = append([]string{fmt.Sprintf("IP-CIDR,%s,REJECT,no-resolve", tunAddress)}, raw.Rule...)
patchRawConfig(raw)

View File

@@ -5,6 +5,7 @@ import (
"sync"
"github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/proxy/tun"
)
@@ -56,5 +57,5 @@ func ResetDnsRedirect() {
return
}
(*tunInstance).ReCreateDNSServer(resolver.DefaultResolver, dnsAddress)
(*tunInstance).ReCreateDNSServer(resolver.DefaultResolver.(*dns.Resolver), dnsAddress)
}

View File

@@ -3,7 +3,6 @@ package com.github.kr328.clash.core
import android.content.Context
import bridge.Bridge
import bridge.TunCallback
import com.github.kr328.clash.core.event.EventStream
import com.github.kr328.clash.core.event.LogEvent
import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.model.Proxy
@@ -12,12 +11,13 @@ import com.github.kr328.clash.core.model.Traffic
import com.github.kr328.clash.core.transact.DoneCallbackImpl
import com.github.kr328.clash.core.transact.ProxyCollectionImpl
import com.github.kr328.clash.core.transact.ProxyGroupCollectionImpl
import com.github.kr328.clash.core.utils.Log
import kotlinx.coroutines.CompletableDeferred
import java.io.File
import java.io.InputStream
object Clash {
private val logReceivers = mutableMapOf<String, (LogEvent) -> Unit>()
private var initialized = false
@Synchronized
@@ -33,8 +33,6 @@ object Clash {
Bridge.setHome(context.cacheDir.absolutePath)
Bridge.setApplicationVersion(BuildConfig.VERSION_NAME)
Bridge.reset()
Log.d("MMDB loaded ${bytes.size}")
}
fun start() {
@@ -52,10 +50,11 @@ object Clash {
onNewSocket: (Int) -> Boolean,
onTunStop: () -> Unit
) {
Bridge.startTunDevice(fd.toLong(), mtu.toLong(), dns, object: TunCallback {
Bridge.startTunDevice(fd.toLong(), mtu.toLong(), dns, object : TunCallback {
override fun onCreateSocket(fd: Long) {
onNewSocket(fd.toInt())
}
override fun onStop() {
onTunStop()
}
@@ -143,14 +142,27 @@ object Clash {
return Traffic(data.upload, data.download)
}
fun openLogEvent(): EventStream<LogEvent> {
return object : EventStream<LogEvent>() {
val log = Bridge.pollLogs { level, payload ->
send(LogEvent(LogEvent.Level.fromString(level), payload))
}
fun registerLogReceiver(key: String, receiver: (LogEvent) -> Unit) {
synchronized(logReceivers) {
logReceivers[key] = receiver
override fun onClose() {
log.stop()
Bridge.setLogCallback(this::onLogEvent)
}
}
fun unregisterLogReceiver(key: String) {
synchronized(logReceivers) {
logReceivers.remove(key)
if (logReceivers.isEmpty())
Bridge.setLogCallback(null)
}
}
private fun onLogEvent(level: String, payload: String) {
synchronized(logReceivers) {
logReceivers.forEach {
it.value(LogEvent(LogEvent.Level.fromString(level), payload))
}
}
}

View File

@@ -10,7 +10,7 @@ data class LogEvent(
val level: Level,
val message: String,
val time: Long = System.currentTimeMillis()
): Event {
) : Event {
companion object {
const val DEBUG_VALUE = "debug"
const val INFO_VALUE = "info"

View File

@@ -34,6 +34,6 @@ abstract class Base(val screen: CommonUiScreen) {
abstract fun restoreState(bundle: Bundle)
protected open fun applyAttribute(enabled: Boolean, hidden: Boolean) {
view.isEnabled = enabled
view.visibility = if ( hidden ) View.GONE else View.VISIBLE
view.visibility = if (hidden) View.GONE else View.VISIBLE
}
}

View File

@@ -6,8 +6,9 @@ import android.view.View
import android.widget.TextView
import com.github.kr328.clash.design.R
class Category(screen: CommonUiScreen): Base(screen) {
override val view: View = LayoutInflater.from(context).inflate(R.layout.view_category, screen.layout, false)
class Category(screen: CommonUiScreen) : Base(screen) {
override val view: View =
LayoutInflater.from(context).inflate(R.layout.view_category, screen.layout, false)
private val vText: TextView = view.findViewById(R.id.text)
private val vTopSeparator: View = view.findViewById(R.id.topSeparator)
@@ -15,19 +16,21 @@ class Category(screen: CommonUiScreen): Base(screen) {
var text: CharSequence
get() = vText.text
set(value) { vText.text = value }
set(value) {
vText.text = value
}
var showTopSeparator: Boolean
get() = vTopSeparator.visibility == View.VISIBLE
set(value) {
vTopSeparator.visibility =
if ( value ) View.VISIBLE else View.GONE
if (value) View.VISIBLE else View.GONE
}
var showBottomSeparator: Boolean
get() = vBottomSeparator.visibility == View.VISIBLE
set(value) {
vBottomSeparator.visibility =
if ( value ) View.VISIBLE else View.GONE
if (value) View.VISIBLE else View.GONE
}
override fun saveState(bundle: Bundle) {}

View File

@@ -73,7 +73,8 @@ class CommonUiBuilder(val screen: CommonUiScreen) {
fun tips(
title: String = "",
icon: Drawable? = null,
setup: Tips.() -> Unit) {
setup: Tips.() -> Unit
) {
val tips = Tips(screen)
tips.title = title

View File

@@ -41,7 +41,7 @@ class CommonUiScreen(val layout: CommonUiLayout) {
}
fun restoreState(bundle: Bundle?) {
if ( bundle == null )
if (bundle == null)
return
elements.forEach {

View File

@@ -7,8 +7,9 @@ import android.view.View
import android.widget.TextView
import com.github.kr328.clash.design.R
class Option(screen: CommonUiScreen): Base(screen) {
override val view: View = LayoutInflater.from(context).inflate(R.layout.view_setting_option, screen.layout, false)
class Option(screen: CommonUiScreen) : Base(screen) {
override val view: View =
LayoutInflater.from(context).inflate(R.layout.view_setting_option, screen.layout, false)
private val vIcon: View = view.findViewById(android.R.id.icon)
private val vTitle: TextView = view.findViewById(android.R.id.title)
@@ -19,15 +20,19 @@ class Option(screen: CommonUiScreen): Base(screen) {
var icon: Drawable?
get() = vIcon.background
set(value) { vIcon.background = value }
set(value) {
vIcon.background = value
}
var title: CharSequence
get() = vTitle.text
set(value) { vTitle.text = value }
set(value) {
vTitle.text = value
}
var summary: CharSequence
get() = vSummary.text
set(value) {
vSummary.text = value
if ( value.isEmpty() )
if (value.isEmpty())
vSummary.visibility = View.GONE
else
vSummary.visibility = View.VISIBLE
@@ -41,7 +46,7 @@ class Option(screen: CommonUiScreen): Base(screen) {
var paddingHeight: Boolean
get() = vPadding.visibility != View.GONE
set(value) {
if ( value )
if (value)
vPadding.visibility = View.INVISIBLE
else
vPadding.visibility = View.GONE

View File

@@ -74,14 +74,14 @@ class TextInput(screen: CommonUiScreen) : Base(screen) {
}
override fun saveState(bundle: Bundle) {
if ( id == null )
if (id == null)
return
bundle.putCharSequence(id, content)
}
override fun restoreState(bundle: Bundle) {
if ( id == null )
if (id == null)
return
bundle.getCharSequence(id)?.apply {

View File

@@ -7,7 +7,7 @@ import android.view.View
import android.widget.TextView
import com.github.kr328.clash.design.R
class Tips(screen: CommonUiScreen): Base(screen) {
class Tips(screen: CommonUiScreen) : Base(screen) {
override val view: View = LayoutInflater.from(context)
.inflate(R.layout.view_setting_tip, screen.layout, false)
@@ -16,11 +16,15 @@ class Tips(screen: CommonUiScreen): Base(screen) {
var icon: Drawable?
get() = vIcon.background
set(value) { vIcon.background = value }
set(value) {
vIcon.background = value
}
var title: CharSequence
get() = vTitle.text
set(value) { vTitle.text = value }
set(value) {
vTitle.text = value
}
override fun saveState(bundle: Bundle) {}
override fun restoreState(bundle: Bundle) {}

View File

@@ -20,6 +20,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="25dp"
android:layout_alignParentEnd="true"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
<TextView
@@ -29,5 +30,6 @@
android:layout_alignStart="@android:id/title"
android:layout_below="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"/>
</RelativeLayout>

View File

@@ -1,6 +1,6 @@
#Thu Nov 14 20:04:34 CST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
#Tue Feb 25 12:40:41 CST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

View File

@@ -16,9 +16,6 @@ interface IClashManager {
long queryBandwidth();
// Events
void openLogEvent(IStreamCallback callback);
// Settings
boolean putSetting(String key, String value);
String getSetting(String key);
void registerLogListener(String key, IStreamCallback callback);
void unregisterLogListener(String key);
}

View File

@@ -2,13 +2,12 @@ package com.github.kr328.clash.service
import android.app.Service
import android.content.Context
import android.content.res.Configuration
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.createLanguageConfigurationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import java.util.*
abstract class BaseService : Service(), CoroutineScope by MainScope() {
lateinit var settings: ServiceSettings
@@ -17,19 +16,8 @@ abstract class BaseService : Service(), CoroutineScope by MainScope() {
settings = ServiceSettings(base ?: return super.attachBaseContext(base))
val language = settings.get(ServiceSettings.LANGUAGE)
if ( language.isEmpty() )
return super.attachBaseContext(base)
val languageOverride = language.split("-")
val configuration = Configuration()
val localeOverride = if (languageOverride.size == 2)
Locale(languageOverride[0], languageOverride[1])
else
Locale(languageOverride[0])
configuration.setLocale(localeOverride)
super.attachBaseContext(base.createConfigurationContext(configuration))
super.attachBaseContext(base.createLanguageConfigurationContext(language))
}
override fun onCreate() {

View File

@@ -0,0 +1,151 @@
package com.github.kr328.clash.service
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.PowerManager
import androidx.core.content.getSystemService
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.utils.Log
import com.github.kr328.clash.service.data.ClashDatabase
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.selects.select
class ClashCore(private val service: Service) : CoroutineScope by MainScope() {
private var stopReason: String? = null
private val settings = ServiceSettings(service)
private val database = ClashDatabase.getInstance(service)
private val notification = ClashNotification(service)
private val reloadChannel = Channel<Unit>(Channel.CONFLATED)
private val screenChannel = Channel<Boolean>(Channel.CONFLATED)
private val receivers = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intent.ACTION_SCREEN_ON ->
screenChannel.offer(true)
Intent.ACTION_SCREEN_OFF ->
screenChannel.offer(false)
Intents.INTENT_ACTION_PROFILE_CHANGED ->
intent.enforceSelfPackage {
reloadChannel.offer(Unit)
}
Intents.INTENT_ACTION_NETWORK_CHANGED ->
intent.enforceSelfPackage {
reloadChannel.offer(Unit)
}
Intents.INTENT_ACTION_REQUEST_STOP ->
intent.enforceSelfPackage {
stopSelf(null)
}
}
}
}
fun start() {
service.registerReceiver(receivers, IntentFilter().apply {
addAction(Intents.INTENT_ACTION_PROFILE_CHANGED)
addAction(Intents.INTENT_ACTION_REQUEST_STOP)
addAction(Intents.INTENT_ACTION_NETWORK_CHANGED)
if (settings.get(ServiceSettings.NOTIFICATION_REFRESH)) {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
}
})
broadcastClashStarted(service)
Clash.start()
ServiceStatusProvider.serviceRunning = true
reloadChannel.offer(Unit)
launch {
val ticker = Channel<Unit>()
var enableRefresh = settings.get(ServiceSettings.NOTIFICATION_REFRESH)
&& service.getSystemService<PowerManager>()!!.isInteractive
launch {
while (isActive) {
ticker.send(Unit)
delay(1000)
}
}
while (isActive) {
select<Unit> {
reloadChannel.onReceive {
reload()
}
screenChannel.onReceive {
enableRefresh = it
Log.i("Clash Notification Status $it")
}
if (enableRefresh) {
ticker.onReceive {
notification.update()
}
}
}
}
}
}
fun destroy() {
cancel()
service.unregisterReceiver(receivers)
reloadChannel.close()
notification.destroy()
broadcastClashStopped(service, stopReason)
Clash.stopTunDevice()
Clash.stop()
ServiceStatusProvider.serviceRunning = false
}
private fun stopSelf(reason: String?) {
stopReason = reason
service.stopSelf()
Clash.stopTunDevice()
}
private suspend fun reload() {
try {
val active = database.openClashProfileDao()
.queryActiveProfile() ?: return stopSelf("Active Profile not Found")
Clash.loadProfile(
resolveProfile(active.id),
resolveBase(active.id)
).await()
ClashDatabase.getInstance(service).openClashProfileProxyDao()
.querySelectedForProfile(active.id).forEach {
Clash.setSelectedProxy(it.proxy, it.selected)
}
ServiceStatusProvider.currentProfile = active.name
notification.setProfile(active.name)
broadcastProfileLoaded(service)
} catch (e: Exception) {
stopSelf(e.message ?: "Unknown")
}
}
}

View File

@@ -1,7 +1,6 @@
package com.github.kr328.clash.service
import android.content.Context
import androidx.core.content.edit
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.model.General
import com.github.kr328.clash.core.model.ProxyGroupList
@@ -42,37 +41,12 @@ class ClashManager(context: Context, parent: CoroutineScope) :
return Clash.setSelectedProxy(proxy, selected)
}
override fun putSetting(key: String?, value: String?): Boolean {
settings.edit(commit = false) {
putString(key, value)
}
return true
}
override fun queryBandwidth(): Long {
val data = Clash.queryBandwidth()
return data.download + data.upload
}
override fun openLogEvent(callback: IStreamCallback?) {
require(callback != null)
Clash.openLogEvent().apply {
callback.asBinder()?.linkToDeath({
close()
}, 0)
onEvent {
try {
callback.send(ParcelableContainer(it))
} catch (e: Exception) {
close()
}
}
}
}
override fun startHealthCheck(group: String?, callback: IStreamCallback?) {
require(group != null && callback != null)
@@ -84,7 +58,26 @@ class ClashManager(context: Context, parent: CoroutineScope) :
}
}
override fun getSetting(key: String?): String? {
return settings.getString(key, null)
override fun registerLogListener(key: String?, callback: IStreamCallback?) {
requireNotNull(key)
requireNotNull(callback)
callback.asBinder().linkToDeath({
Clash.unregisterLogReceiver(key)
}, 0)
Clash.registerLogReceiver(key) {
try {
callback.send(ParcelableContainer(it))
} catch (e: Exception) {
Clash.unregisterLogReceiver(key)
}
}
}
override fun unregisterLogListener(key: String?) {
requireNotNull(key)
Clash.unregisterLogReceiver(key)
}
}

View File

@@ -1,26 +1,19 @@
package com.github.kr328.clash.service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.app.*
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.utils.Log
import com.github.kr328.clash.core.utils.asBytesString
import com.github.kr328.clash.core.utils.asSpeedString
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class ClashNotification(private val context: ClashService, private val enableRefresh: Boolean) :
CoroutineScope by context {
class ClashNotification(
private val context: Service
) {
companion object {
private const val CLASH_STATUS_NOTIFICATION_CHANNEL = "clash_status_channel"
private const val CLASH_STATUS_NOTIFICATION_ID = 413
@@ -37,6 +30,7 @@ class ClashNotification(private val context: ClashService, private val enableRef
.setColor(context.getColor(R.color.colorAccentService))
.setOnlyAlertOnce(true)
.setShowWhen(false)
.setGroup(CLASH_STATUS_NOTIFICATION_CHANNEL)
.setContentIntent(
PendingIntent.getActivity(
context,
@@ -45,107 +39,26 @@ class ClashNotification(private val context: ClashService, private val enableRef
PendingIntent.FLAG_UPDATE_CURRENT
)
)
private val screenChannel: Channel<Boolean> = Channel(Channel.CONFLATED)
private val tickerChannel: Channel<Unit> = Channel()
private var profile = "None"
private val observer = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intent.ACTION_SCREEN_ON ->
screenChannel.offer(true)
Intent.ACTION_SCREEN_OFF ->
screenChannel.offer(false)
}
}
}
private var currentProfile = "None"
init {
createNotificationChannel()
updateBase()
if (enableRefresh) {
launch {
val powerManager =
requireNotNull(context.getSystemService(PowerManager::class.java))
screenChannel.send(powerManager.isInteractive)
var tickerJob: Job? = null
launch {
while (isActive) {
tickerJob = if (screenChannel.receive()) {
tickerJob?.cancel()
startTicker()
} else {
tickerJob?.cancel()
null
}
}
}
launch {
while (isActive) {
tickerChannel.receive()
update()
}
}
context.registerReceiver(observer, IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
})
}
}
}
private fun startTicker(): Job {
return launch {
Log.d("Clash Notification Started")
try {
while (isActive) {
tickerChannel.send(Unit)
delay(1000)
}
} finally {
Log.d("Clash Notification Stopped")
}
}
}
fun destroy() {
if ( enableRefresh )
context.unregisterReceiver(observer)
context.stopForeground(true)
updateDestroy()
}
fun setProfile(profile: String) {
launch {
this@ClashNotification.profile = profile
if ( enableRefresh )
update()
else
updateBase()
}
currentProfile = profile
}
private fun updateBase() {
val notification = baseBuilder
.setContentTitle(profile)
.setContentText(context.getText(R.string.running))
.build()
context.startForeground(CLASH_STATUS_NOTIFICATION_ID, notification)
}
private suspend fun update() {
suspend fun update() {
val notification = withContext(Dispatchers.Default) {
createNotification()
}
@@ -153,12 +66,37 @@ class ClashNotification(private val context: ClashService, private val enableRef
context.startForeground(CLASH_STATUS_NOTIFICATION_ID, notification)
}
private fun updateBase() {
val notification = baseBuilder
.setContentTitle(currentProfile)
.setContentText(context.getText(R.string.running))
.build()
context.startForeground(CLASH_STATUS_NOTIFICATION_ID, notification)
}
private fun updateDestroy() {
// just waiting system cancel our notification :)
// fxxking google
val notification = baseBuilder
.setContentTitle(context.getText(R.string.destroying))
.setContentText(context.getText(R.string.recycling_resources))
.setSubText(null)
.build()
NotificationManagerCompat.from(context).apply {
notify(CLASH_STATUS_NOTIFICATION_ID, notification)
cancel(CLASH_STATUS_NOTIFICATION_ID)
}
}
private fun createNotification(): Notification {
val traffic = Clash.queryTraffic()
val bandwidth = Clash.queryBandwidth()
return baseBuilder
.setContentTitle(profile)
.setContentTitle(currentProfile)
.setContentText(
context.getString(
R.string.clash_notification_content,

View File

@@ -1,79 +1,25 @@
package com.github.kr328.clash.service
import android.content.BroadcastReceiver
import android.content.Context
import android.app.Service
import android.content.Intent
import android.content.IntentFilter
import android.os.Binder
import android.os.IBinder
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.service.data.ClashDatabase
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.*
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
class ClashService : BaseService() {
companion object {
var isServiceRunning = false
}
private val loadLock = Mutex()
private val service = this
private lateinit var notification: ClashNotification
private var stopReason: String? = null
private val reloadChannel = Channel<Unit>(Channel.CONFLATED)
private val reloadReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.`package` != packageName)
return
reloadChannel.offer(Unit)
}
}
class ClashService : Service() {
private var clashCore: ClashCore? = null
override fun onCreate() {
super.onCreate()
notification = ClashNotification(service, settings.get(ServiceSettings.NOTIFICATION_REFRESH))
if (ServiceStatusProvider.serviceRunning)
return stopSelf()
launch {
while (isActive) {
reloadChannel.receive()
Clash.initialize(this)
reloadProfile()
}
}
}
clashCore = ClashCore(this)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
if (isServiceRunning)
return START_NOT_STICKY
isServiceRunning = true
Clash.start()
broadcastClashStarted(this)
val startVpn = intent?.getBooleanExtra(
Intents.INTENT_EXTRA_START_TUN, true) ?: true
if (startVpn)
startService(TunService::class.intent)
registerReceiver(reloadReceiver, IntentFilter().apply {
addAction(Intents.INTENT_ACTION_PROFILE_CHANGED)
addAction(Intents.INTENT_ACTION_NETWORK_CHANGED)
})
reloadChannel.offer(Unit)
return START_NOT_STICKY
clashCore?.start()
}
override fun onBind(intent: Intent?): IBinder? {
@@ -81,52 +27,6 @@ class ClashService : BaseService() {
}
override fun onDestroy() {
cancel()
Clash.stopTunDevice()
Clash.stop()
notification.destroy()
broadcastClashStopped(this, stopReason)
unregisterReceiver(reloadReceiver)
isServiceRunning = false
super.onDestroy()
}
private suspend fun reloadProfile() {
if (!loadLock.tryLock())
return
try {
val active = ClashDatabase.getInstance(service).openClashProfileDao()
.queryActiveProfile() ?: return stopSelf("Empty active profile")
Clash.loadProfile(
resolveProfile(active.id),
resolveBase(active.id)
).await()
ClashDatabase.getInstance(service).openClashProfileProxyDao()
.querySelectedForProfile(active.id).forEach {
Clash.setSelectedProxy(it.proxy, it.selected)
}
notification.setProfile(active.name)
broadcastProfileLoaded(this, active)
} catch (e: Exception) {
stopSelf("Load profile failure")
} finally {
loadLock.unlock()
}
}
private fun stopSelf(reason: String) {
stopReason = reason
stopSelf()
clashCore?.destroy()
}
}

View File

@@ -15,11 +15,11 @@ object Intents {
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.loaded"
const val INTENT_ACTION_NETWORK_CHANGED =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.network.changed"
const val INTENT_ACTION_REQUEST_STOP =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.request.stop"
const val INTENT_EXTRA_CLASH_STOP_REASON =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.stop.reason"
const val INTENT_EXTRA_PROFILE =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.profile"
const val INTENT_EXTRA_PROFILE_REQUEST =
"${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.profile.request"
const val INTENT_EXTRA_START_TUN =

View File

@@ -100,7 +100,7 @@ class ProfileBackgroundService : BaseService() {
do {
select<Unit> {
requestChannel.onReceive {
if ( !queue.containsKey(it.id) ) {
if (!queue.containsKey(it.id)) {
queue[it.id] = it
sendRequest(it, service)
@@ -112,7 +112,7 @@ class ProfileBackgroundService : BaseService() {
}
refreshStatusNotification(queue.size)
} while ( queue.isNotEmpty() )
} while (queue.isNotEmpty())
stopSelf()
}
@@ -120,7 +120,7 @@ class ProfileBackgroundService : BaseService() {
private fun sendRequest(request: ProfileRequest, service: IProfileService) {
val originalCallback = request.callback
request.withCallback(object: IStreamCallback.Stub() {
request.withCallback(object : IStreamCallback.Stub() {
override fun complete() {
originalCallback?.complete()
@@ -137,7 +137,7 @@ class ProfileBackgroundService : BaseService() {
launch {
responseChannel.send(request)
sendUpdateFailure(request.id, reason?: "Unknown")
sendUpdateFailure(request.id, reason ?: "Unknown")
}
}
@@ -180,6 +180,7 @@ class ProfileBackgroundService : BaseService() {
.setColor(getColor(R.color.colorAccentService))
.setSmallIcon(R.drawable.ic_notification)
.setOnlyAlertOnce(true)
.setGroup(SERVICE_STATUS_CHANNEL)
.build()
startForeground(RandomUtils.nextInt(), notification)
@@ -194,6 +195,7 @@ class ProfileBackgroundService : BaseService() {
.setColor(getColor(R.color.colorAccentService))
.setSmallIcon(R.drawable.ic_notification)
.setOnlyAlertOnce(true)
.setGroup(SERVICE_RESULT_CHANNEL)
.build()
NotificationManagerCompat.from(this)
@@ -209,6 +211,7 @@ class ProfileBackgroundService : BaseService() {
.setColor(getColor(R.color.colorAccentService))
.setSmallIcon(R.drawable.ic_notification)
.setOnlyAlertOnce(true)
.setGroup(SERVICE_RESULT_CHANNEL)
.build()
NotificationManagerCompat.from(this)

View File

@@ -13,7 +13,7 @@ class ProfileRequestReceiver : BroadcastReceiver() {
return
val id = intent.getLongExtra(Intents.INTENT_EXTRA_PROFILE_ID, -1)
if ( id < 0 )
if (id < 0)
return
val request = ProfileRequest()

View File

@@ -2,10 +2,25 @@ package com.github.kr328.clash.service
import android.content.Context
import android.content.SharedPreferences
import rikka.preference.MultiProcessPreference
import rikka.preference.PreferenceProvider
class ServiceSettingsProvider: PreferenceProvider() {
class ServiceSettingsProvider : PreferenceProvider() {
override fun onCreatePreference(context: Context?): SharedPreferences {
return context!!.getSharedPreferences(Constants.SERVICE_SETTING_FILE_NAME, Context.MODE_PRIVATE)
return context!!.getSharedPreferences(
Constants.SERVICE_SETTING_FILE_NAME,
Context.MODE_PRIVATE
)
}
companion object {
fun createSharedPreferencesFromContext(context: Context): SharedPreferences {
return when ( context ) {
is BaseService, is TunService ->
context.getSharedPreferences(Constants.SERVICE_SETTING_FILE_NAME, Context.MODE_PRIVATE)
else ->
MultiProcessPreference(context, context.packageName + Constants.SETTING_PROVIDER_SUFFIX)
}
}
}
}

View File

@@ -9,13 +9,18 @@ import android.os.Bundle
class ServiceStatusProvider : ContentProvider() {
companion object {
const val METHOD_PING_CLASH_SERVICE = "pingClashService"
var serviceRunning: Boolean = false
var currentProfile: String? = null
}
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
return when (method) {
METHOD_PING_CLASH_SERVICE -> {
return if (ClashService.isServiceRunning)
Bundle()
return if (serviceRunning)
Bundle().apply {
putString("name", currentProfile)
}
else
null
}

View File

@@ -6,9 +6,9 @@ import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.utils.Log
import com.github.kr328.clash.service.net.DefaultNetworkChannel
import com.github.kr328.clash.service.settings.ServiceSettings
import com.github.kr328.clash.service.util.asSocketAddressText
import com.github.kr328.clash.service.util.broadcastNetworkChanged
import kotlinx.coroutines.*
import java.net.Inet6Address
class TunService : VpnService(), CoroutineScope by MainScope() {
companion object {
@@ -16,10 +16,13 @@ class TunService : VpnService(), CoroutineScope by MainScope() {
private const val VPN_MTU = 1500
private const val PRIVATE_VLAN4_SUBNET = 30
private const val PRIVATE_VLAN4_CLIENT = "172.31.255.253"
private const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1"
private const val PRIVATE_VLAN_DNS = "172.31.255.254"
private const val VLAN4_ANY = "0.0.0.0"
}
private var clashCore: ClashCore? = null
private lateinit var defaultNetworkChannel: DefaultNetworkChannel
private lateinit var settings: ServiceSettings
@@ -55,6 +58,13 @@ class TunService : VpnService(), CoroutineScope by MainScope() {
Clash.initialize(this)
if (ServiceStatusProvider.serviceRunning)
return stopSelf()
clashCore = ClashCore(this)
clashCore?.start()
settings = ServiceSettings(this)
defaultNetworkChannel = DefaultNetworkChannel(this, this)
@@ -83,12 +93,7 @@ class TunService : VpnService(), CoroutineScope by MainScope() {
val dnsServers = d.second?.dnsServers ?: emptyList()
val dnsStrings = dnsServers.map {
when ( it ) {
is Inet6Address ->
"[${it.hostName}]:53"
else ->
"${it.hostName}:53"
}
it.asSocketAddressText(53)
}
Clash.appendDns(dnsStrings)
@@ -103,7 +108,11 @@ class TunService : VpnService(), CoroutineScope by MainScope() {
override fun onDestroy() {
cancel()
defaultNetworkChannel.unregister()
clashCore?.apply {
destroy()
defaultNetworkChannel.unregister()
}
Log.i("TunService.onDestroy")
@@ -117,8 +126,11 @@ class TunService : VpnService(), CoroutineScope by MainScope() {
}
private fun Builder.addBypassPrivateRoute(): Builder {
val ipv6Support = settings.get(ServiceSettings.IPV6_SUPPORT)
val bypassPrivate = settings.get(ServiceSettings.BYPASS_PRIVATE_NETWORK)
// IPv4
if (settings.get(ServiceSettings.BYPASS_PRIVATE_NETWORK)) {
if (bypassPrivate) {
resources.getStringArray(R.array.bypass_private_route).forEach {
val address = it.split("/")
addRoute(address[0], address[1].toInt())
@@ -128,8 +140,12 @@ class TunService : VpnService(), CoroutineScope by MainScope() {
}
// IPv6
if (settings.get(ServiceSettings.IPV6_SUPPORT)) {
addRoute("::", 0)
if (ipv6Support) {
if (bypassPrivate)
// from https://github.com/shadowsocks/shadowsocks-android/commit/cc840c9fddb3f4f6677005de18f1fcb387b84064#diff-e089fe63dcb3674c0a1e459a95508e3e
addRoute("2000::", 3)
else
addRoute("::", 0)
}
return this
@@ -137,16 +153,9 @@ class TunService : VpnService(), CoroutineScope by MainScope() {
private fun Builder.addBypassApplications(): Builder {
when (settings.get(ServiceSettings.ACCESS_CONTROL_MODE)) {
ServiceSettings.ACCESS_CONTROL_MODE_ALL -> {
for (app in resources.getStringArray(R.array.default_disallow_application)) {
runCatching {
addDisallowedApplication(app)
}
}
}
ServiceSettings.ACCESS_CONTROL_MODE_ALL -> {}
ServiceSettings.ACCESS_CONTROL_MODE_WHITELIST -> {
for (app in settings.get(ServiceSettings.ACCESS_CONTROL_PACKAGES).toSet() -
resources.getStringArray(R.array.default_disallow_application)) {
for (app in settings.get(ServiceSettings.ACCESS_CONTROL_PACKAGES).toSet()) {
runCatching {
addAllowedApplication(app)
}.onFailure {
@@ -155,8 +164,7 @@ class TunService : VpnService(), CoroutineScope by MainScope() {
}
}
ServiceSettings.ACCESS_CONTROL_MODE_BLACKLIST -> {
for (app in settings.get(ServiceSettings.ACCESS_CONTROL_PACKAGES).toSet() +
resources.getStringArray(R.array.default_disallow_application)) {
for (app in settings.get(ServiceSettings.ACCESS_CONTROL_PACKAGES).toSet()) {
runCatching {
addDisallowedApplication(app)
}.onFailure {
@@ -173,6 +181,9 @@ class TunService : VpnService(), CoroutineScope by MainScope() {
private fun Builder.addAddress(): Builder {
addAddress(PRIVATE_VLAN4_CLIENT, PRIVATE_VLAN4_SUBNET)
if (settings.get(ServiceSettings.IPV6_SUPPORT))
addAddress(PRIVATE_VLAN6_CLIENT, 126)
return this
}
}

View File

@@ -68,13 +68,13 @@ object ClashDatabaseMigrations {
cursor.moveToNext()
}
cursor.close()
}
catch (e: Exception) {
} 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")
val cursor =
database.query("SELECT profile_id, proxy, selected FROM _profile_select_proxies ORDER BY id")
cursor.moveToFirst()
while (!cursor.isAfterLast) {
@@ -98,8 +98,7 @@ object ClashDatabaseMigrations {
cursor.moveToNext()
}
cursor.close()
}
catch (e: Exception) {
} catch (e: Exception) {
Log.d("Migration selected failure")
}
@@ -109,8 +108,10 @@ object ClashDatabaseMigrations {
// 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 newSettings = ServiceSettings(
Global.application
.getSharedPreferences(Constants.SERVICE_SETTING_FILE_NAME, Context.MODE_PRIVATE)
)
val accessMode = oldSettings
.getInt("key_access_control_mode", 0)
@@ -124,7 +125,7 @@ object ClashDatabaseMigrations {
.getBoolean("key_bypass_private_network", true)
newSettings.commit {
val newAccessMode = when ( accessMode ) {
val newAccessMode = when (accessMode) {
0 -> ServiceSettings.ACCESS_CONTROL_MODE_ALL
1 -> ServiceSettings.ACCESS_CONTROL_MODE_WHITELIST
2 -> ServiceSettings.ACCESS_CONTROL_MODE_BLACKLIST
@@ -143,8 +144,7 @@ object ClashDatabaseMigrations {
try {
process(database)
Log.i("Database Migrated 1 -> 2")
}
catch (e: Exception) {
} catch (e: Exception) {
Log.e("Migration failure", e)
}
}

View File

@@ -2,6 +2,7 @@ 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
@@ -9,6 +10,7 @@ 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,

View File

@@ -40,7 +40,8 @@ class DefaultNetworkChannel(val context: Context, scope: CoroutineScope) :
val cap = capabilitiesCache[network]
if (cap?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
!= networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
!= networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
) {
sendDefaultNetwork(true)
}
@@ -50,7 +51,7 @@ class DefaultNetworkChannel(val context: Context, scope: CoroutineScope) :
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
val cache = dnsServerCache[network]
if ( cache != linkProperties.dnsServers ) {
if (cache != linkProperties.dnsServers) {
sendDefaultNetwork(false)
}
@@ -74,7 +75,7 @@ class DefaultNetworkChannel(val context: Context, scope: CoroutineScope) :
val network = detectDefaultNetwork()
val link = network?.let(connectivity::getLinkProperties)
if ( ignoreSame && network == currentNetwork )
if (ignoreSame && network == currentNetwork)
return@launch sendLock.unlock()
currentNetwork = network

View File

@@ -8,7 +8,7 @@ abstract class BaseSettings(private val preferences: SharedPreferences) {
fun put(editor: SharedPreferences.Editor, value: T)
}
class StringEntry(private val key: String, private val defaultValue: String): Entry<String> {
class StringEntry(private val key: String, private val defaultValue: String) : Entry<String> {
override fun get(preferences: SharedPreferences): String {
return preferences.getString(key, defaultValue)!!
}
@@ -18,7 +18,8 @@ abstract class BaseSettings(private val preferences: SharedPreferences) {
}
}
class BooleanEntry(private val key: String, private val defaultValue: Boolean): Entry<Boolean> {
class BooleanEntry(private val key: String, private val defaultValue: Boolean) :
Entry<Boolean> {
override fun get(preferences: SharedPreferences): Boolean {
return preferences.getBoolean(key, defaultValue)
}
@@ -28,7 +29,8 @@ abstract class BaseSettings(private val preferences: SharedPreferences) {
}
}
class StringSetEntry(private val key: String, private val defaultValue: Set<String>): Entry<Set<String>> {
class StringSetEntry(private val key: String, private val defaultValue: Set<String>) :
Entry<Set<String>> {
override fun get(preferences: SharedPreferences): Set<String> {
return preferences.getStringSet(key, defaultValue)!!
}
@@ -39,12 +41,12 @@ abstract class BaseSettings(private val preferences: SharedPreferences) {
}
class Editor(private val editor: SharedPreferences.Editor) {
fun <T>put(entry: Entry<T>, value: T) {
fun <T> put(entry: Entry<T>, value: T) {
entry.put(editor, value)
}
}
fun <T>get(entry: Entry<T>): T {
fun <T> get(entry: Entry<T>): T {
return entry.get(preferences)
}
@@ -53,7 +55,7 @@ abstract class BaseSettings(private val preferences: SharedPreferences) {
Editor(editor).apply(block)
if ( async )
if (async)
editor.apply()
else
editor.commit()

View File

@@ -3,12 +3,15 @@ package com.github.kr328.clash.service.settings
import android.content.Context
import android.content.SharedPreferences
import com.github.kr328.clash.service.Constants
import com.github.kr328.clash.service.ServiceSettingsProvider
import rikka.preference.MultiProcessPreference
class ServiceSettings(preference: SharedPreferences):
class ServiceSettings(preference: SharedPreferences) :
BaseSettings(preference) {
constructor(context: Context): this(MultiProcessPreference(context,
context.packageName + Constants.SETTING_PROVIDER_SUFFIX))
constructor(context: Context) : this(
ServiceSettingsProvider.createSharedPreferencesFromContext(context)
)
companion object {
const val ACCESS_CONTROL_MODE_ALL = "access_control_mode_all"
const val ACCESS_CONTROL_MODE_BLACKLIST = "access_control_mode_blacklist"

View File

@@ -2,8 +2,8 @@ package com.github.kr328.clash.service.util
import android.content.Context
import android.content.Intent
import com.github.kr328.clash.core.Global
import com.github.kr328.clash.service.Intents
import com.github.kr328.clash.service.data.ClashProfileEntity
fun Context.sendBroadcastSelf(intent: Intent) {
this.sendBroadcast(intent.setPackage(this.packageName))
@@ -15,9 +15,8 @@ fun broadcastProfileChanged(context: Context) {
context.sendBroadcastSelf(intent)
}
fun broadcastProfileLoaded(context: Context, profileEntity: ClashProfileEntity) {
fun broadcastProfileLoaded(context: Context) {
val intent = Intent(Intents.INTENT_ACTION_PROFILE_LOADED)
.putExtra(Intents.INTENT_EXTRA_PROFILE, profileEntity)
context.sendBroadcastSelf(intent)
}
@@ -37,4 +36,11 @@ fun broadcastClashStopped(context: Context, reason: String?) {
reason
)
)
}
fun Intent.enforceSelfPackage(block: () -> Unit) {
if (`package` != Global.application.packageName)
return
block()
}

View File

@@ -0,0 +1,34 @@
package com.github.kr328.clash.service.util
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
fun InetAddress.asSocketAddressText(port: Int): String {
return when (this) {
is Inet6Address ->
"[${numericToTextFormat(this.address)}]:$port"
is Inet4Address ->
"${this.hostAddress}:$port"
else -> throw IllegalArgumentException("Unsupported Inet type ${this.javaClass}")
}
}
private const val INT16SZ = 2
private const val INADDRSZ = 16
private fun numericToTextFormat(src: ByteArray): String {
val sb = StringBuilder(39)
for (i in 0 until INADDRSZ / INT16SZ) {
sb.append(
Integer.toHexString(
src[i shl 1].toInt() shl 8 and 0xff00
or (src[(i shl 1) + 1].toInt() and 0xff)
)
)
if (i < INADDRSZ / INT16SZ - 1) {
sb.append(":")
}
}
return sb.toString()
}

View File

@@ -0,0 +1,23 @@
package com.github.kr328.clash.service.util
import android.content.Context
import android.content.res.Configuration
import java.util.*
fun Context.createLanguageConfigurationContext(language: String): Context {
if (language.isBlank()) {
return this
}
val split = language.split("-")
val locale = if (split.size == 1)
Locale(split[0])
else
Locale(split[0], split[1])
val configuration = Configuration()
configuration.setLocale(locale)
return createConfigurationContext(configuration)
}

View File

@@ -1,9 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="402.47244dp"
android:height="381.04724dp"
android:viewportWidth="402.47244"
android:viewportHeight="381.04724">
xmlns:tools="http://schemas.android.com/tools"
android:width="200dp"
android:height="200dp"
android:viewportWidth="200"
android:viewportHeight="200">
<path
android:fillColor="#FF000000"
android:pathData="M95.758,327.156l4,-41l12,-97l13,-94l12,-61l2,-7c2,-5 4,-6 9,-3l10,9l26,41c3,4 6,5 11,4c28,-6 57,-5 85,1c5,1 7,-1 9,-5a1453,1453 0,0 1,30 -49c4,-4 7,-4 9,2l8,36l28,177c5,34 8,67 11,100c2,12 1,13 -11,14c-54,5 -108,5 -162,4c-30,0 -59,-2 -88,-3c-6,0 -12,-3 -18,-4l-17,-1c-18,-2 -34,-9 -44,-26c-17,-28 -6,-65 29,-75c4,-1 8,0 12,1c3,2 3,5 0,7l-6,5c-4,3 -9,6 -12,10a31,31 0,0 0,7 49c10,5 20,6 31,6zM176.758,139.156c-8,0 -15,7 -15,15s6,15 15,15c8,0 15,-6 15,-15c0,-8 -7,-15 -15,-15zM299.758,169.156c8,0 15,-6 15,-15c0,-8 -6,-15 -15,-15a15,15 0,1 0,0 30zM216.758,189.156c4,7 11,7 20,1c7,6 16,6 18,-1c-8,3 -14,2 -19,-7c-4,9 -10,10 -19,7z"/>
android:fillColor="#000000"
android:pathData="M47.211,168.128C70.531,-34.962 67.471,13.788 94.071,43.818c13.45,-1.52 27.24,-3.47 40.82,-0.67c2.64,0.13 5.42,1.86 7.71,0.18c4.12,-6.27 7.35,-13.54 11.35,-20c12.19,-24.44 12.85,19.54 15.48,26.52c5.23,32.99 10.89,64.46 14.67,97.59c0.31,10.72 5.74,32.92 1.08,33.56c-49.36,5.23 -147.71,3.91 -160.84,-6.3c-15.85,-10.5 -15.18,-35.33 2.03,-43.72c3.63,-2.03 10.68,-3.72 11.94,0.7c-2.41,4.99 -8.79,5.77 -12.12,11.17C16.621,158.948 33.111,168.888 47.211,168.128zM87.841,74.008c-10.42,0.52 -9.59,14.89 -0.07,15.18C98.191,88.668 97.361,74.298 87.841,74.008zM149.121,89.188c10.46,-0.34 9.85,-14.71 0.38,-15.18C139.031,74.348 139.651,88.718 149.121,89.188zM107.871,99.228c2.16,3.48 5.28,3.29 9.79,0.16c3.81,3.17 8.06,3.28 9.18,-0.19c-3.78,1.17 -7.04,0.79 -9.4,-3.49C115.371,100.108 112.071,100.428 107.871,99.228z"
tools:ignore="VectorPath" />
</vector>

View File

@@ -9,4 +9,6 @@
<string name="format_update_failure">更新 %s 失败\\n%s</string>
<string name="process_result">处理结果</string>
<string name="processing_profiles">正在处理配置文件</string>
<string name="recycling_resources">正在回收资源</string>
<string name="destroying">销毁中</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More