From 961f9c7633f9f70de2e89203142e4bb08cc69e6a Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 2 Dec 2019 21:43:19 +0800 Subject: [PATCH] add black/white list --- .../kr328/clash/SettingAccessActivity.kt | 239 +++++++++++++++++- .../github/kr328/clash/view/RadioFatItem.kt | 6 + .../res/layout/activity_setting_access.xml | 16 +- .../main/res/layout/adapter_access_app.xml | 48 ++++ app/src/main/res/menu/menu_setting_access.xml | 16 ++ app/src/main/res/values/strings.xml | 6 + core/src/main/golang/clash | 2 +- service/build.gradle | 5 +- .../kr328/clash/service/IClashService.aidl | 2 + .../clash/service/IClashSettingService.aidl | 14 + .../kr328/clash/service/ClashService.kt | 6 +- .../clash/service/ClashSettingService.kt | 58 +++++ .../github/kr328/clash/service/TunService.kt | 64 ++++- .../service/data/ClashProfileProxyEntity.kt | 6 +- service/src/main/res/values/arrays.xml | 2 +- 15 files changed, 464 insertions(+), 26 deletions(-) create mode 100644 app/src/main/res/layout/adapter_access_app.xml create mode 100644 app/src/main/res/menu/menu_setting_access.xml create mode 100644 service/src/main/aidl/com/github/kr328/clash/service/IClashSettingService.aidl create mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashSettingService.kt diff --git a/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt index e38126f..59ab08c 100644 --- a/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt @@ -1,10 +1,66 @@ package com.github.kr328.clash +import android.content.Context +import android.graphics.drawable.Drawable import android.os.Bundle -import android.view.View +import android.view.* +import android.widget.CheckBox +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.github.kr328.clash.service.ClashSettingService import kotlinx.android.synthetic.main.activity_setting_access.* +import kotlin.concurrent.thread class SettingAccessActivity : BaseActivity() { + private data class AppInfo(val packageName: String, val name: String, val icon: Drawable) + + private class AppListAdapter(val context: Context): + RecyclerView.Adapter() { + + var applications: List = emptyList() + var selected: MutableSet = mutableSetOf() + + class Holder(val view: View) : RecyclerView.ViewHolder(view) { + val name: TextView = view.findViewById(R.id.adapter_access_app_name) + val packageName: TextView = view.findViewById(R.id.adapter_access_app_package_name) + val icon: ImageView = view.findViewById(R.id.adapter_access_app_icon) + val checkbox: CheckBox = view.findViewById(R.id.adapter_access_app_checkbox) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { + return Holder(LayoutInflater.from(context) + .inflate(R.layout.adapter_access_app, parent, false)) + } + + override fun getItemCount(): Int { + return applications.size + } + + override fun onBindViewHolder(holder: Holder, position: Int) { + val current = applications[position] + + holder.name.text = current.name + holder.packageName.text = current.packageName + holder.icon.setImageDrawable(current.icon) + holder.checkbox.isChecked = current.packageName in selected + + holder.view.setOnClickListener { + if ( holder.checkbox.isChecked ) + selected.remove(current.packageName) + else + selected.add(current.packageName) + + notifyItemChanged(position) + } + } + } + + private var showList: Boolean = false + private var listLoaded: Boolean = false + private var hidden: Boolean = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_setting_access) @@ -16,8 +72,9 @@ class SettingAccessActivity : BaseActivity() { activity_setting_access_allow.isChecked = false activity_setting_access_disallow.isChecked = false - activity_setting_access_divider.visibility = View.GONE - activity_setting_access_app_list.visibility = View.GONE + showList = false + + updateListStatus() } activity_setting_access_allow.setOnClickListener { @@ -25,8 +82,9 @@ class SettingAccessActivity : BaseActivity() { activity_setting_access_allow.isChecked = true activity_setting_access_disallow.isChecked = false - activity_setting_access_divider.visibility = View.VISIBLE - activity_setting_access_app_list.visibility = View.VISIBLE + showList = true + + updateListStatus() } activity_setting_access_disallow.setOnClickListener { @@ -34,8 +92,175 @@ class SettingAccessActivity : BaseActivity() { activity_setting_access_allow.isChecked = false activity_setting_access_disallow.isChecked = true - activity_setting_access_divider.visibility = View.VISIBLE - activity_setting_access_app_list.visibility = View.VISIBLE + showList = true + + updateListStatus() + } + + activity_setting_access_app_list.apply { + adapter = AppListAdapter(this@SettingAccessActivity) + layoutManager = LinearLayoutManager(this@SettingAccessActivity) + isNestedScrollingEnabled = false + } + + runClash { + val settings = it.settingService + + runOnUiThread { + when ( settings.accessControlMode ) { + ClashSettingService.ACCESS_CONTROL_MODE_ALLOW_ALL -> + activity_setting_access_allow_all.performClick() + ClashSettingService.ACCESS_CONTROL_MODE_ALLOW -> + activity_setting_access_allow.performClick() + ClashSettingService.ACCESS_CONTROL_MODE_DISALLOW -> + activity_setting_access_disallow.performClick() + } + } + + val exclude = resources.getStringArray(R.array.default_disallow_application).toSet() + + val applications = packageManager.getInstalledApplications(0) + .filterNot { + exclude.contains(it.packageName) + } + .map { app -> + AppInfo(app.packageName, + app.loadLabel(packageManager).toString(), + app.loadIcon(packageManager)) + } + .sortedBy { app -> + app.name + } + val selected = settings.accessControlApps.toMutableSet() + + runOnUiThread { + activity_setting_access_app_list.apply { + (adapter as AppListAdapter).apply { + this.applications = applications + this.selected = selected + + notifyDataSetChanged() + } + } + + listLoaded = true + + updateListStatus() + } } } + + override fun onStop() { + super.onStop() + + runClash { + val mode = when { + activity_setting_access_allow_all.isChecked -> + ClashSettingService.ACCESS_CONTROL_MODE_ALLOW_ALL + activity_setting_access_allow.isChecked -> + ClashSettingService.ACCESS_CONTROL_MODE_ALLOW + activity_setting_access_disallow.isChecked -> + ClashSettingService.ACCESS_CONTROL_MODE_DISALLOW + else -> return@runClash + } + + if ( listLoaded ) + it.settingService.setAccessControl(mode, + (activity_setting_access_app_list.adapter as AppListAdapter) + .selected.toTypedArray()) + } + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_setting_access, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when ( item.itemId ) { + R.id.menu_setting_access_hide -> { + if ( hidden ) { + activity_setting_access_allow_all.visibility = View.VISIBLE + activity_setting_access_allow.visibility = View.VISIBLE + activity_setting_access_disallow.visibility = View.VISIBLE + + item.title = getString(R.string.access_setting_hide_header) + + hidden = false + } + else { + if ( !activity_setting_access_allow_all.isChecked ) + activity_setting_access_allow_all.visibility = View.GONE + if ( !activity_setting_access_allow.isChecked ) + activity_setting_access_allow.visibility = View.GONE + if ( !activity_setting_access_disallow.isChecked ) + activity_setting_access_disallow.visibility = View.GONE + + item.title = getString(R.string.access_setting_show_header) + + hidden = true + } + } + R.id.menu_setting_access_select_all -> { + thread { + val selected = (activity_setting_access_app_list.adapter as AppListAdapter) + .applications.map { it.packageName }.toMutableSet() + + runOnUiThread { + (activity_setting_access_app_list.adapter as AppListAdapter).apply { + this.selected = selected + + notifyItemRangeChanged(0, applications.size) + } + } + } + } + R.id.menu_setting_access_select_invert -> { + thread { + (activity_setting_access_app_list.adapter as AppListAdapter).apply { + selected = (applications.map { it.packageName }.toSet() - selected).toMutableSet() + } + + runOnUiThread { + (activity_setting_access_app_list.adapter as AppListAdapter).apply { + notifyItemRangeChanged(0, applications.size) + } + } + } + } + R.id.menu_setting_access_clean_selection -> { + (activity_setting_access_app_list.adapter as AppListAdapter).apply { + selected = mutableSetOf() + + notifyItemRangeChanged(0, applications.size) + } + } + else -> return super.onOptionsItemSelected(item) + } + + return true + } + + private fun updateListStatus() { + if ( showList ) + activity_setting_access_divider.visibility = View.VISIBLE + else + activity_setting_access_divider.visibility = View.GONE + + + if ( showList ) { + if ( listLoaded ) + activity_setting_access_loading.visibility = View.GONE + else + activity_setting_access_loading.visibility = View.VISIBLE + } + else + activity_setting_access_loading.visibility = View.GONE + + + if ( showList && listLoaded ) + activity_setting_access_app_list.visibility = View.VISIBLE + else + activity_setting_access_app_list.visibility = View.GONE + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/view/RadioFatItem.kt b/app/src/main/java/com/github/kr328/clash/view/RadioFatItem.kt index de74018..2808c80 100644 --- a/app/src/main/java/com/github/kr328/clash/view/RadioFatItem.kt +++ b/app/src/main/java/com/github/kr328/clash/view/RadioFatItem.kt @@ -69,6 +69,12 @@ class RadioFatItem @JvmOverloads constructor( operationClickable.setOnLongClickListener(l) } + override fun performClick(): Boolean { + super.performClick() + + return clickable.performClick() + } + init { with(LayoutInflater.from(context).inflate(R.layout.view_radio_fat_item, this, true)) { titleView = findViewById(R.id.view_radio_fat_item_title) diff --git a/app/src/main/res/layout/activity_setting_access.xml b/app/src/main/res/layout/activity_setting_access.xml index a44c6f7..13444cf 100644 --- a/app/src/main/res/layout/activity_setting_access.xml +++ b/app/src/main/res/layout/activity_setting_access.xml @@ -16,6 +16,8 @@ android:layout_height="60dp" /> + + - + android:layout_height="wrap_content"/> + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_access_app.xml b/app/src/main/res/layout/adapter_access_app.xml new file mode 100644 index 0000000..35d15d6 --- /dev/null +++ b/app/src/main/res/layout/adapter_access_app.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_setting_access.xml b/app/src/main/res/menu/menu_setting_access.xml new file mode 100644 index 0000000..b339004 --- /dev/null +++ b/app/src/main/res/menu/menu_setting_access.xml @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 637ef2f..aee3aa6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,4 +64,10 @@ Allow follow apps access vpn Disallow Disallow follow apps access vpn + Loading + Show Header + Hide Header + Select All + Select Invert + Clear Selection diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index f0f6d92..d5d22cc 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit f0f6d92a2bd94dd743d021cc2e07032aec1fd677 +Subproject commit d5d22cce0c4b046921d87345e3ca0d38cc7ac917 diff --git a/service/build.gradle b/service/build.gradle index 1bf88e6..db1a326 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -26,8 +26,11 @@ android { } } compileOptions { - targetCompatibility = 1.8 sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + kotlinOptions { + jvmTarget = "1.8" } } diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl index 55d92ba..4c88483 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl @@ -3,6 +3,7 @@ package com.github.kr328.clash.service; import com.github.kr328.clash.service.IClashEventObserver; import com.github.kr328.clash.service.IClashEventService; import com.github.kr328.clash.service.IClashProfileService; +import com.github.kr328.clash.service.IClashSettingService; import com.github.kr328.clash.core.event.Event; import com.github.kr328.clash.core.model.ProxyPacket; @@ -10,6 +11,7 @@ interface IClashService { // Services IClashEventService getEventService(); IClashProfileService getProfileService(); + IClashSettingService getSettingService(); // Status ProcessEvent getCurrentProcessStatus(); diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashSettingService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashSettingService.aidl new file mode 100644 index 0000000..31fc505 --- /dev/null +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashSettingService.aidl @@ -0,0 +1,14 @@ +package com.github.kr328.clash.service; + +interface IClashSettingService { + // Set + void setIPv6Enabled(boolean enabled); + void setBypassPrivateNetwork(boolean enabled); + void setAccessControl(int mode, in String[] applications); + + // Get + boolean isIPv6Enabled(); + boolean isBypassPrivateNetwork(); + String[] getAccessControlApps(); + int getAccessControlMode(); +} diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashService.kt index be7c61d..37fd0bd 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashService.kt @@ -13,7 +13,6 @@ import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.event.* import com.github.kr328.clash.core.model.ProxyPacket import com.github.kr328.clash.core.utils.Log -import com.github.kr328.clash.service.data.ClashDatabase import java.io.File import java.io.IOException import java.util.concurrent.Executors @@ -24,6 +23,7 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, private val eventService = ClashEventService(this) private val profileService = ClashProfileService(this, this) + private val settingService = ClashSettingService(this) private lateinit var clash: Clash private lateinit var puller: ClashEventPuller @@ -103,6 +103,10 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, return this@ClashService.profileService } + override fun getSettingService(): IClashSettingService { + return this@ClashService.settingService + } + override fun getCurrentProcessStatus(): ProcessEvent { return clash.process.getProcessStatus() } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashSettingService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashSettingService.kt new file mode 100644 index 0000000..dea5d84 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ClashSettingService.kt @@ -0,0 +1,58 @@ +package com.github.kr328.clash.service + +import android.content.Context +import androidx.core.content.edit + +class ClashSettingService(context: Context): IClashSettingService.Stub() { + companion object { + const val KEY_ACCESS_CONTROL_MODE = "key_access_control_mode" + const val KEY_ACCESS_CONTROL_APPS = "ley_access_control_apps" + const val KEY_IPV6_ENABLED = "key_ipv6_enabled" + const val KEY_BYPASS_PRIVATE_NETWORK = "key_bypass_private_network" + + const val ACCESS_CONTROL_MODE_ALLOW_ALL = 0 + const val ACCESS_CONTROL_MODE_ALLOW = 1 + const val ACCESS_CONTROL_MODE_DISALLOW = 2 + } + + private val preference by lazy { + context.getSharedPreferences("clash_service", Context.MODE_PRIVATE) + } + + override fun setAccessControl(mode: Int, applications: Array?) { + require(mode in 0..3) + + preference.edit { + putInt(KEY_ACCESS_CONTROL_MODE, mode) + putStringSet(KEY_ACCESS_CONTROL_APPS, applications?.toSet() ?: emptySet()) + } + } + + override fun setIPv6Enabled(enabled: Boolean) { + preference.edit { + putBoolean(KEY_IPV6_ENABLED, enabled) + } + } + + override fun setBypassPrivateNetwork(enabled: Boolean) { + preference.edit { + putBoolean(KEY_BYPASS_PRIVATE_NETWORK, enabled) + } + } + + override fun isBypassPrivateNetwork(): Boolean { + return preference.getBoolean(KEY_BYPASS_PRIVATE_NETWORK, true) + } + + override fun isIPv6Enabled(): Boolean { + return preference.getBoolean(KEY_IPV6_ENABLED, true) + } + + override fun getAccessControlApps(): Array { + return preference.getStringSet(KEY_ACCESS_CONTROL_APPS, emptySet())?.toTypedArray()!! + } + + override fun getAccessControlMode(): Int { + return preference.getInt(KEY_ACCESS_CONTROL_MODE, ACCESS_CONTROL_MODE_ALLOW_ALL) + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/TunService.kt b/service/src/main/java/com/github/kr328/clash/service/TunService.kt index c127385..a1fc76a 100644 --- a/service/src/main/java/com/github/kr328/clash/service/TunService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/TunService.kt @@ -8,6 +8,7 @@ import android.content.ServiceConnection import android.net.VpnService import android.os.* import com.github.kr328.clash.core.event.* +import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.net.DefaultNetworkObserver class TunService : VpnService(), IClashEventObserver { @@ -23,6 +24,7 @@ class TunService : VpnService(), IClashEventObserver { private lateinit var fileDescriptor: ParcelFileDescriptor private lateinit var clash: IClashService private lateinit var defaultNetworkObserver: DefaultNetworkObserver + private lateinit var settings: ClashSettingService private val connection = object : ServiceConnection { override fun onServiceDisconnected(name: ComponentName?) { stopSelf() @@ -53,9 +55,10 @@ class TunService : VpnService(), IClashEventObserver { return } + settings = ClashSettingService(this) + fileDescriptor = Builder() - .addAddress(PRIVATE_VLAN4_CLIENT, 30) - .addAddress(PRIVATE_VLAN6_CLIENT, 126) + .addAddress() .addDnsServer(PRIVATE_DEFAULT_DNS) .addBypassApplications() .addBypassPrivateRoute() @@ -120,22 +123,65 @@ class TunService : VpnService(), IClashEventObserver { private fun Builder.addBypassPrivateRoute(): Builder { // IPv4 - resources.getStringArray(R.array.bypass_private_route).forEach { - val address = it.split("/") - addRoute(address[0], address[1].toInt()) + if ( settings.isBypassPrivateNetwork ) { + resources.getStringArray(R.array.bypass_private_route).forEach { + val address = it.split("/") + addRoute(address[0], address[1].toInt()) + } + } + else { + addRoute("0.0.0.0", 0) } // IPv6 - addRoute("::", 0) + if ( settings.isIPv6Enabled ) { + + addRoute("::", 0) + } return this } private fun Builder.addBypassApplications(): Builder { - resources.getStringArray(R.array.default_bypass_application) - .forEach { - runCatching { this.addDisallowedApplication(it) } + when ( settings.accessControlMode ) { + ClashSettingService.ACCESS_CONTROL_MODE_ALLOW_ALL -> { + for ( app in resources.getStringArray(R.array.default_disallow_application) ) { + runCatching { + addDisallowedApplication(app) + } + } } + ClashSettingService.ACCESS_CONTROL_MODE_ALLOW -> { + for ( app in (settings.accessControlApps.toSet() - + resources.getStringArray(R.array.default_disallow_application)) ) { + runCatching { + addAllowedApplication(app) + }.onFailure { + Log.w("Package $app not found") + } + } + } + ClashSettingService.ACCESS_CONTROL_MODE_DISALLOW -> { + for ( app in (settings.accessControlApps.toSet() + + resources.getStringArray(R.array.default_disallow_application)) ) { + runCatching { + addDisallowedApplication(app) + }.onFailure { + Log.w("Package $app not found") + } + } + } + } + + return this + } + + private fun Builder.addAddress(): Builder { + addAddress(PRIVATE_VLAN4_CLIENT, 30) + + if ( settings.isIPv6Enabled ) + addAddress(PRIVATE_VLAN6_CLIENT, 126) + return this } diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileProxyEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileProxyEntity.kt index 90fe56b..2b41ff0 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileProxyEntity.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileProxyEntity.kt @@ -1,11 +1,9 @@ package com.github.kr328.clash.service.data -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.PrimaryKey +import androidx.room.* @Entity(tableName = "profile_select_proxies", + indices = [Index("profile_id")], foreignKeys = [ForeignKey(entity = ClashProfileEntity::class, childColumns = ["profile_id"], parentColumns = ["id"], diff --git a/service/src/main/res/values/arrays.xml b/service/src/main/res/values/arrays.xml index f9e6cf1..af1e81d 100644 --- a/service/src/main/res/values/arrays.xml +++ b/service/src/main/res/values/arrays.xml @@ -40,7 +40,7 @@ 224.0.0.0/3 - + com.github.kr328.clash com.android.networkstack