Compare commits

...

19 Commits
1.1.4 ... 1.1.7

Author SHA1 Message Date
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
78 changed files with 549 additions and 374 deletions

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.*

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

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

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

@@ -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,7 +69,10 @@ 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):
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 ->
@@ -74,19 +80,21 @@ suspend fun Pipeline<List<ProxyGroup>>.toAdapterElement(prefixMerged: Map<ProxyE
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

@@ -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
@@ -43,9 +47,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 +72,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) {
@@ -92,8 +92,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
}
@@ -133,30 +135,28 @@ object Remote {
})
}
private suspend fun verifyApk(application: Application): Boolean {
return withContext(Dispatchers.IO) {
val sp = application.getSharedPreferences(
Constants.PREFERENCE_NAME_APP,
Context.MODE_PRIVATE
)
val pkg = application.packageManager.getPackageInfo(application.packageName, 0)
private suspend fun verifyApk(application: Application) = withContext(Dispatchers.IO) {
val sp = application.getSharedPreferences(
Constants.PREFERENCE_NAME_APP,
Context.MODE_PRIVATE
)
val pkg = application.packageManager.getPackageInfo(application.packageName, 0)
if (sp.getLong(Constants.PREFERENCE_KEY_LAST_INSTALL, 0) == pkg.lastUpdateTime)
return@withContext true
if (sp.getLong(Constants.PREFERENCE_KEY_LAST_INSTALL, 0) == pkg.lastUpdateTime)
return@withContext true
val info = application.applicationInfo
val sources =
info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return@withContext false
val info = application.applicationInfo
val sources =
info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return@withContext 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
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
}
return@withContext false
}
return@withContext 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

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

View File

@@ -8,8 +8,8 @@ buildscript {
gMinSdkVersion = 24
gTargetSdkVersion = 29
gVersionCode = 10103
gVersionName = "1.1.3"
gVersionCode = 10107
gVersionName = "1.1.7"
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,7 +59,7 @@ 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()
@@ -74,8 +75,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

@@ -186,7 +186,7 @@ class ProfileService : BaseService() {
ClashProfileEntity(
requireNotNull(request.name),
requireNotNull(request.type),
requireNotNull(request.url).toString(),
requireNotNull(request.url).toString().toLowerCase(Locale.getDefault()),
request.source?.toString(),
false,
0,

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>