Compare commits

..

38 Commits

Author SHA1 Message Date
Kr328
e958e7f675 Update 03-bug-report-zh-cn.md 2020-03-22 20:45:20 +08:00
Kr328
68113e694e update issue template 2020-03-22 13:04:50 +08:00
Kr328
f500596621 update README.md 2020-03-21 16:11:03 +08:00
Kr328
df5bafd0bb update README.md 2020-03-21 16:01:35 +08:00
Kr328
01fe9deb20 update issue template 2020-03-20 13:16:43 +08:00
Kr328
4e44298e98 sorted github issue 2020-03-19 19:46:12 +08:00
Kr328
a7c3a05c23 add application info request 2020-03-19 19:09:20 +08:00
Kr328
05fa36497b rename issues template file 2020-03-19 18:56:41 +08:00
Kr328
e6f8ae265e Add issue templates 2020-03-19 18:53:05 +08:00
Kr328
594949d3f0 format code & update core 2020-02-28 23:52:58 +08:00
Kr328
8fd34b5258 improve apk broken detect & fix udp crash 2020-02-28 14:00:38 +08:00
Kr328
1161482a5b fix udp crash 2020-02-28 11:19:06 +08:00
Kr328
8a123e7a0d improve network settings 2020-02-28 01:13:11 +08:00
Kr328
4ef3a20929 add vpn component detect 2020-02-28 00:32:49 +08:00
Kr328
16c2e9b694 increase refresh interval 2020-02-28 00:24:24 +08:00
Kr328
c9a9d310ef update version 2020-02-28 00:22:21 +08:00
Kr328
84996a5652 update apk broken detect 2020-02-27 16:30:57 +08:00
Kr328
115afc5735 update apk broken detect 2020-02-27 16:30:34 +08:00
Kr328
2ae75e876d fix uri case 2020-02-27 16:03:28 +08:00
Kr328
126291d544 improve duplicate profile 2020-02-27 14:26:47 +08:00
Kr328
37db81f6b1 improve profile update & update clash core 2020-02-27 14:05:00 +08:00
Kr328
a3cfd10d39 fix udp crash 2020-02-26 16:31:59 +08:00
Kr328
0e61f02046 fix typo 2020-02-26 15:20:22 +08:00
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
84 changed files with 714 additions and 381 deletions

View File

@@ -0,0 +1,43 @@
---
name: "[English] Bug report"
about: Create a report to help us improve
title: "[BUG] "
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
if applicable, add logs to help detect problem
**Device Info (please complete the following information):**
- Device: [e.g. Pixel 4]
- ROM: [e.g: AOSP]
- ROM Version:
- Android Version [e.g. Oreo]
**Application Info (please complete the following information):**
- Version: [e.g. 1.1.10]
- Apk File Name: [e.g. app-release-arm64-v8a.apk]
- Distribution Channel: [e.g. Google Play]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: "[English] Feature request"
about: Suggest an idea for this app
title: "[Feature Request] "
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,41 @@
---
name: "[简体中文] 创建错误报告"
about: 创建错误报告以帮助我们改进应用
title: "[BUG] "
labels: ''
assignees: ''
---
**描述出现的错误**
请简洁的描述你遇到的错误
**如何复现该错误**
复现步骤:
1. ...
2. ...
3. ...
4. ...
**预期行为**
清晰简单的描述你预期的应用应该表现的行为
**屏幕截图**
如果适用, 上传屏幕截图以帮助描述错误
**日志**
如果适用, 上传日志以帮助侦测错误
**设备信息 (请完成一下信息):**
- 机型: [例如: Pixel 4]
- 系统/ROM: [例如: MIUI 11]
- Android 版本 [例如: Oreo]
- ROM版本 [例如: 20.3.19]
**应用信息**
- 版本: [例如: 1.1.10]
- 安装包文件名: [例如: app-release-arm64-v8a.apk]
- 应用来源: [例如: Google Play]
**附加信息**
其他的可能与改错误相关的信息

View File

@@ -0,0 +1,17 @@
---
name: "[简体中文] 功能请求"
about: 你希望的能够在应用中增加的功能
title: "[Feature Request] "
labels: ''
assignees: ''
---
**功能描述**
请清晰的描述你想要的功能
**描述你希望的实现方式**
清晰的描述应用应该如何实现该功能
**附加信息**
其他的与改功能相关的附加信息

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -1,10 +1,8 @@
## Clash for Android
A GUI for [clash](https://github.com/Dreamacro/clash) on Android
> NOTICE: Early testing currently
A Graphical user interface of [clash](https://github.com/Dreamacro/clash) for Android
<a href="https://play.google.com/store/apps/details?id=com.github.kr328.clash&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1"><img width="200px" alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"/></a> or [Releases](https://github.com/Kr328/ClashForAndroid/releases)
### Feature
@@ -15,9 +13,7 @@ Fully feature of [clash](https://github.com/Dreamacro/clash) ~~(Exclude `externa
### Requirement
* Android 7.0+
* `arm64` or `x86_64` architecture
* `armeabi-v7a` , `arm64-v8a`, `x86` or `x86_64` Architecture
### License
@@ -39,7 +35,7 @@ See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
git submodule update --init --recursive
```
2. Install `Android SDK (include JDK)` ,`Android NDK` and `Golang`
2. Install `JDK 1.8`, `Android SDK` ,`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

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

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

@@ -8,7 +8,10 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.*
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.IInterface
import androidx.collection.CircularArray
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@@ -24,8 +27,6 @@ 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
import com.microsoft.appcenter.analytics.Analytics
import com.microsoft.appcenter.crashes.Crashes
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.selects.select
@@ -39,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"
@@ -55,29 +57,23 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
private val entity = LogFile.generate()
private val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
logChannel.offer(LogEvent(LogEvent.Level.ERROR, "Clash Service Crashed"))
private var manager: IClashManager? = null
Crashes.trackError(RemoteException("Clash Service Crashed"))
override fun onServiceDisconnected(name: ComponentName?) {
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)
}
})
}
@@ -103,11 +99,15 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
cancel()
connection.onServiceDisconnected(null)
unbindService(connection)
stopForeground(true)
super.onDestroy()
isServiceRunning = false
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? {
@@ -182,8 +182,7 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface {
}
}
}
}
catch (e: Exception) {
} catch (e: Exception) {
return@launch
}
}

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,9 +12,7 @@ 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.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.*
@@ -37,8 +35,15 @@ class MainActivity : BaseActivity() {
stopClashService()
} else {
val vpnRequest = startClashService()
if (vpnRequest != null)
startActivityForResult(vpnRequest, REQUEST_CODE)
if (vpnRequest != null) {
val resolved = packageManager.resolveActivity(vpnRequest, 0)
if ( resolved != null ) {
startActivityForResult(vpnRequest, REQUEST_CODE)
}
else {
makeSnackbarException(getString(R.string.missing_vpn_component), null)
}
}
}
}

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()
@@ -149,7 +149,10 @@ class ProfileEditActivity : BaseActivity() {
}
if (url == null || url == Uri.EMPTY ||
(url.scheme != "http" && url.scheme != "https" && url.scheme != "content")
(!url.scheme.equals("http", ignoreCase = true)
&& !url.scheme.equals("https", ignoreCase = true)
&& !url.scheme.equals("content", ignoreCase = true)
&& !url.scheme.equals("file", ignoreCase = true))
) {
Snackbar.make(rootView, R.string.invalid_url, Snackbar.LENGTH_LONG).show()
return@setOnClickListener

View File

@@ -148,11 +148,11 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C
val editor = ProfileEditActivity::class.intent
.putExtra("id", if (duplicate) -1L else entity.id)
.putExtra("type", type)
.putExtra("type", if (duplicate) Constants.URL_PROVIDER_TYPE_FILE else type)
.putExtra("intent", intent)
.putExtra("name", name)
.putExtra("url", uri)
.putExtra("interval", interval)
.putExtra("interval", if (duplicate) "0" else interval)
startActivity(editor)
}

View File

@@ -59,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()
@@ -234,7 +236,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
private fun refreshList(scrollTop: Boolean = false) {
launch {
if ( !refreshMutex.tryLock() )
if (!refreshMutex.tryLock())
return@launch
val general = withClash {
@@ -256,9 +258,9 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
notifyDataSetChanged()
}
if ( scrollTop )
if (scrollTop)
mainList.smoothScrollToPosition(0)
else if ( scrollToLast ) {
else if (scrollToLast) {
scrollToLast = false
val selected = uiSettings.get(UiSettings.PROXY_LAST_SELECT_GROUP)
@@ -266,7 +268,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback {
scrollBinding.scrollMaster(selected)
}
delay(200)
delay(500)
refreshMutex.unlock()
}

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,10 +15,18 @@ class SettingsNetworkActivity : BaseActivity() {
.replace(R.id.fragment, NetworkFragment())
.commit()
if ( clashRunning )
if (clashRunning)
Snackbar.make(rootView, R.string.options_unavailable, Snackbar.LENGTH_INDEFINITE).show()
}
override suspend fun onClashStopped(reason: String?) {
recreate()
}
override suspend fun onClashStarted() {
recreate()
}
override val activityLabel: CharSequence?
get() = getText(R.string.network)
}
}

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

@@ -37,12 +37,12 @@ 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
@@ -54,7 +54,7 @@ class TileService : TileService() {
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

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

@@ -141,7 +141,7 @@ class ProxyAdapter(
groupCache[it.name] = index
is ProxyRenderInfo -> {
if (it.info.active)
activeCache[it.name] = index
activeCache[it.group] = index
}
}
}
@@ -194,8 +194,10 @@ class ProxyAdapter(
is ProxyGroupHeader -> {
val current = renderList[position] as ProxyGroupRenderInfo
holder.title.text = context.getString(R.string.format_proxy_group_title,
current.info.name, current.info.current)
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
@@ -237,14 +239,16 @@ class ProxyAdapter(
if (current.info.selectable) {
holder.root.setOnClickListener {
val oldPosition = activeList[current.group] ?: return@setOnClickListener
val groupPosition = groupPosition[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))
renderList[groupPosition] =
group.copy(info = group.info.copy(current = current.name))
activeList[current.group] = position

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

@@ -16,7 +16,7 @@ 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) )
if (!settings.get(UiSettings.PROXY_MERGE_PREFIX))
return Pipeline(emptyMap(), settings)
val result = coroutineScope {
@@ -31,7 +31,10 @@ suspend fun Pipeline<List<ProxyGroup>>.mergePrefix(): Pipeline<Map<ProxyEntry, P
}
.flatMap {
it.second.map { merged ->
ProxyEntry(it.first, merged.value.name) to ProxyMerged(merged.prefix, merged.content)
ProxyEntry(it.first, merged.value.name) to ProxyMerged(
merged.prefix,
merged.content
)
}
}
.toMap()
@@ -66,27 +69,31 @@ suspend fun Pipeline<List<ProxyGroup>>.sort(): Pipeline<List<ProxyGroup>> {
return copy(input = sorter.sort(input))
}
suspend fun Pipeline<List<ProxyGroup>>.toAdapterElement(prefixMerged: Map<ProxyEntry, ProxyMerged>, general: General):
List<ProxyAdapter.ProxyGroupInfo> {
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,
ProxyAdapter.ProxyInfo(
proxy.name, group.name, merged.content, merged.prefix,
proxy.delay.toShort(), group.type == Proxy.Type.SELECT,
group.current == proxy.name)
group.current == proxy.name
)
}
ProxyAdapter.ProxyGroupInfo(group.name, group.current, proxies)
}.let {
withContext(Dispatchers.Default) {
when ( general.mode ) {
when (general.mode) {
General.Mode.DIRECT -> emptyList()
General.Mode.GLOBAL -> it
General.Mode.RULE -> it.filter { it.name != "GLOBAL"}
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

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

@@ -5,6 +5,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Build
import android.os.Handler
import android.os.IBinder
import androidx.core.content.edit
@@ -18,8 +19,10 @@ 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 kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.io.File
import java.util.zip.ZipFile
object Remote {
@@ -43,9 +46,7 @@ object Remote {
if (service != null)
instance = ClashClient(IClashManager.Stub.asInterface(service))
service?.linkToDeath({
onServiceDisconnected(null)
}, 0)
service?.linkToDeath({ onServiceDisconnected(null) }, 0)
sender = GlobalScope.launch {
while (isActive) {
@@ -70,9 +71,7 @@ object Remote {
if (service != null)
instance = ProfileClient(IProfileService.Stub.asInterface(service))
service?.linkToDeath({
onServiceDisconnected(null)
}, 0)
service?.linkToDeath({ onServiceDisconnected(null) }, 0)
sender = GlobalScope.launch {
while (isActive) {
@@ -91,9 +90,15 @@ object Remote {
handler.removeMessages(0)
GlobalScope.launch {
if (!verifyApk(application)) {
application.startActivity(ApkBrokenActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
val valid = withContext(Dispatchers.IO) {
verifyApk(application)
}
if (!valid) {
application.startActivity(
ApkBrokenActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
return@launch
}
@@ -133,8 +138,8 @@ object Remote {
})
}
private suspend fun verifyApk(application: Application): Boolean {
return withContext(Dispatchers.IO) {
private fun verifyApk(application: Application): Boolean {
return try {
val sp = application.getSharedPreferences(
Constants.PREFERENCE_NAME_APP,
Context.MODE_PRIVATE
@@ -142,21 +147,36 @@ object Remote {
val pkg = application.packageManager.getPackageInfo(application.packageName, 0)
if (sp.getLong(Constants.PREFERENCE_KEY_LAST_INSTALL, 0) == pkg.lastUpdateTime)
return@withContext true
return true
val info = application.applicationInfo
val sources =
info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return@withContext false
info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return false
for (apk in sources) {
if (ZipFile(apk).entries().asSequence().any { it.name.endsWith("libgojni.so") }) {
sp.edit {
putLong(Constants.PREFERENCE_KEY_LAST_INSTALL, pkg.lastUpdateTime)
}
return@withContext true
val regexNativeLibrary = Regex("lib/(\\S+)/libgojni.so")
val availableAbi = Build.SUPPORTED_ABIS.toSet()
val apkAbi =
sources
.asSequence()
.filter { File(it).exists() }
.flatMap { ZipFile(it).entries().asSequence() }
.mapNotNull { regexNativeLibrary.matchEntire(it.name) }
.mapNotNull { it.groups[1]?.value }
.toSet()
if (availableAbi.intersect(apkAbi).isNotEmpty()) {
sp.edit {
putLong(Constants.PREFERENCE_KEY_LAST_INSTALL, pkg.lastUpdateTime)
}
true
} else {
false
}
return@withContext false
} catch (e: Exception) {
Crashes.trackError(e)
false
}
}
}

View File

@@ -1,40 +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? {
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?.getString("name")
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

@@ -20,7 +20,7 @@ object PrefixMerger {
val result = mutableListOf<Result<T>>()
for (pair in pairs) {
if ( pair.first.isEmpty() )
if (pair.first.isEmpty())
continue
if (pair.first[0] == currentCodePoint) {
@@ -56,8 +56,12 @@ object PrefixMerger {
val prefix = it.first.subList(0, diffIndex)
val content = it.first.subList(diffIndex, it.first.size)
result.add(Result(prefix.asCodePointString().replace(REGEX_PREFIX_TRIM, ""),
content.asCodePointString(), it.second))
result.add(
Result(
prefix.asCodePointString().replace(REGEX_PREFIX_TRIM, ""),
content.asCodePointString(), it.second
)
)
}
}

View File

@@ -13,7 +13,7 @@ class ProxySorter(private val groupOrder: Order, private val proxyOrder: Order)
suspend fun sort(proxyGroup: List<ProxyGroup>): List<ProxyGroup> =
withContext(Dispatchers.Default) {
val groups = proxyGroup.groupBy {
if ( it.name == "GLOBAL" )
if (it.name == "GLOBAL")
"GLOBAL"
else
"OTHER"
@@ -30,7 +30,7 @@ class ProxySorter(private val groupOrder: Order, private val proxyOrder: Order)
Order.NAME_DECREASE -> groupSortWithName(false, other)
}
val sorted = if ( global == null )
val sorted = if (global == null)
sortedGroup
else
listOf(global) + sortedGroup

View File

@@ -6,7 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
class QuickSmoothScroller(context: Context, target: Int):
class QuickSmoothScroller(context: Context, target: Int) :
LinearSmoothScroller(context) {
companion object {
const val MAX_OFFSET = 2

View File

@@ -14,14 +14,13 @@ 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 {
} else {
startForegroundServiceCompat(ClashService::class.intent)
}

View File

@@ -4,7 +4,7 @@ fun String.toCodePointList(): List<Int> {
var offset = 0
val result = mutableListOf<Int>()
while ( offset < length ) {
while (offset < length) {
val codePoint = codePointAt(offset)
result.add(codePoint)

View File

@@ -130,4 +130,5 @@
<string name="learn_more_about_split_apks">了解更多关于分包机制</string>
<string name="reinstall_from_google_play">重新从 Google Play 安装</string>
<string name="download_from_github_releases">从 Github Release 下载</string>
<string name="missing_vpn_component">系统 VPN 组件缺失</string>
</resources>

View File

@@ -109,7 +109,7 @@
<string name="boot">Boot</string>
<string name="start_on_boot">Start on Boot</string>
<string name="start_clash_on_system_boot">Start slash on system boot</string>
<string name="start_clash_on_system_boot">Start Clash on system boot</string>
<string name="notification">Notification</string>
<string name="show_traffic">Show Traffic</string>
@@ -172,4 +172,5 @@
<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>
<string name="missing_vpn_component">Missing VPN Components</string>
</resources>

View File

@@ -8,8 +8,8 @@ buildscript {
gMinSdkVersion = 24
gTargetSdkVersion = 29
gVersionCode = 10103
gVersionName = "1.1.3"
gVersionCode = 10110
gVersionName = "1.1.10"
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()

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

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

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

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

@@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.PowerManager
import androidx.core.content.contentValuesOf
import androidx.core.content.getSystemService
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.utils.Log
@@ -17,16 +16,16 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.selects.select
class ClashCore(private val service: Service): CoroutineScope by MainScope() {
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() {
private val receivers = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when ( intent?.action ) {
when (intent?.action) {
Intent.ACTION_SCREEN_ON ->
screenChannel.offer(true)
Intent.ACTION_SCREEN_OFF ->
@@ -53,7 +52,7 @@ class ClashCore(private val service: Service): CoroutineScope by MainScope() {
addAction(Intents.INTENT_ACTION_REQUEST_STOP)
addAction(Intents.INTENT_ACTION_NETWORK_CHANGED)
if ( settings.get(ServiceSettings.NOTIFICATION_REFRESH) ) {
if (settings.get(ServiceSettings.NOTIFICATION_REFRESH)) {
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_SCREEN_OFF)
}
@@ -73,14 +72,14 @@ class ClashCore(private val service: Service): CoroutineScope by MainScope() {
&& service.getSystemService<PowerManager>()!!.isInteractive
launch {
while ( isActive ) {
while (isActive) {
ticker.send(Unit)
delay(1000)
}
}
while ( isActive ) {
while (isActive) {
select<Unit> {
reloadChannel.onReceive {
reload()
@@ -90,7 +89,7 @@ class ClashCore(private val service: Service): CoroutineScope by MainScope() {
Log.i("Clash Notification Status $it")
}
if ( enableRefresh ) {
if (enableRefresh) {
ticker.onReceive {
notification.update()
}

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

@@ -6,12 +6,10 @@ import android.os.Build
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.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.serializer
class ClashNotification(
private val context: Service

View File

@@ -12,7 +12,7 @@ class ClashService : Service() {
override fun onCreate() {
super.onCreate()
if ( ServiceStatusProvider.serviceRunning )
if (ServiceStatusProvider.serviceRunning)
return stopSelf()
Clash.initialize(this)

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")
}
}
@@ -206,10 +206,10 @@ class ProfileBackgroundService : BaseService() {
val entity = profiles.queryProfileById(id) ?: return
val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL)
.setContentTitle(getString(R.string.format_update_failure, entity.name, reason))
.setContentText(reason)
.setContentTitle(getString(R.string.format_update_failure, entity.name))
.setColor(getColor(R.color.colorAccentService))
.setSmallIcon(R.drawable.ic_notification)
.setStyle(NotificationCompat.BigTextStyle().bigText(reason))
.setOnlyAlertOnce(true)
.setGroup(SERVICE_RESULT_CHANNEL)
.build()

View File

@@ -22,7 +22,8 @@ class ProfileProcessor(private val context: Context) {
downloadProfile(
uri,
resolveProfile(entity.id),
resolveBase(entity.id)
resolveBase(entity.id),
newRecord
)
val newEntity = if (entity.type == ClashProfileEntity.TYPE_FILE)
@@ -58,12 +59,13 @@ class ProfileProcessor(private val context: Context) {
}
}
private suspend fun downloadProfile(source: Uri, target: File, baseDir: File) {
private suspend fun downloadProfile(source: Uri, target: File, baseDir: File, newRecord: Boolean) {
try {
target.parentFile?.mkdirs()
baseDir.mkdirs()
if (source.scheme == "content" || source.scheme == "file") {
if (source.scheme.equals("content", ignoreCase = true)
|| source.scheme.equals("file", ignoreCase = true)) {
val parcelFileDescriptor = context.contentResolver.openFileDescriptor(source, "r")
?: throw FileNotFoundException("Unable to open file $source")
@@ -74,8 +76,10 @@ class ProfileProcessor(private val context: Context) {
Clash.downloadProfile(source.toString(), target, baseDir).await()
}
} catch (e: Exception) {
target.delete()
baseDir.deleteRecursively()
if ( newRecord ) {
target.delete()
baseDir.deleteRecursively()
}
throw e
}
}

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

@@ -16,6 +16,7 @@ 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"
}
@@ -57,7 +58,7 @@ class TunService : VpnService(), CoroutineScope by MainScope() {
Clash.initialize(this)
if ( ServiceStatusProvider.serviceRunning )
if (ServiceStatusProvider.serviceRunning)
return stopSelf()
clashCore = ClashCore(this)
@@ -125,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())
@@ -136,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
@@ -145,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 {
@@ -163,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 {
@@ -181,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

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

@@ -39,7 +39,7 @@ fun broadcastClashStopped(context: Context, reason: String?) {
}
fun Intent.enforceSelfPackage(block: () -> Unit) {
if ( `package` != Global.application.packageName )
if (`package` != Global.application.packageName)
return
block()

View File

@@ -5,7 +5,7 @@ import java.net.Inet6Address
import java.net.InetAddress
fun InetAddress.asSocketAddressText(port: Int): String {
return when ( this ) {
return when (this) {
is Inet6Address ->
"[${numericToTextFormat(this.address)}]:$port"
is Inet4Address ->

View File

@@ -5,12 +5,12 @@ import android.content.res.Configuration
import java.util.*
fun Context.createLanguageConfigurationContext(language: String): Context {
if ( language.isBlank() ) {
if (language.isBlank()) {
return this
}
val split = language.split("-")
val locale = if ( split.size == 1 )
val locale = if (split.size == 1)
Locale(split[0])
else
Locale(split[0], split[1])

View File

@@ -6,7 +6,7 @@
<string name="running">正在运行</string>
<string name="format_in_queue">%d 个项目在队列中</string>
<string name="format_update_complete">更新 %s 完成</string>
<string name="format_update_failure">更新 %s 失败\\n%s</string>
<string name="format_update_failure">更新 %s 失败</string>
<string name="process_result">处理结果</string>
<string name="processing_profiles">正在处理配置文件</string>
<string name="recycling_resources">正在回收资源</string>

View File

@@ -68,8 +68,4 @@
<item>255.255.255.254/32</item>
<item>172.31.255.252/30</item> <!-- tun device address -->
</string-array>
<string-array name="default_disallow_application">
<item>com.android.networkstack</item>
</string-array>
</resources>

View File

@@ -8,7 +8,7 @@
<string name="format_in_queue">%d items in Queue</string>
<string name="process_result">Process Result</string>
<string name="format_update_complete">Update %s Completed</string>
<string name="format_update_failure">Update %s Failure\n%s</string>
<string name="format_update_failure">Update %s Failure</string>
<string name="profile_status_channel">Profile Processing Status</string>
<string name="running">Running</string>
<string name="destroying">Destroying</string>